Starting a new project

Copy-paste/checklist for getting things rolling.

Stack environment

# Check for a recent one at https://gitlab.com/keid/meta/-/tree/main/resolvers
resolver: https://gitlab.com/keid/meta/-/raw/main/resolvers/keid-engine-2024-10-02.yaml

packages:
- . # this one

extra-deps: []
# hacking time!
# - ../keid/engine/core
# - ../keid/engine/render-basic

# let the ghcup in
system-ghc: true

# your users will thank you later
ghc-options:
  "$everything": -split-sections -j2
  "$locals": -split-sections -j8

Entry point

app/Main.hs

module Main (main) where

-- Prelude used with Keid, from `rio` package
import RIO -- rio

-- From `rio-app` package
import Engine.App (engineMain)

-- Initial stage, from project library or executable
import Stage.StageName (stackStage)

main :: IO ()
main = engineMain stackStage

Global

Render

A bunch of keid-render-basic types for now. The aliases are shared with downstream stage components.

module Global.Render where

import Engine.Stage.Component qualified as Stage
import Engine.Types qualified as Engine
import Render.Basic qualified as Basic

type Frame = Engine.Frame RenderPasses Pipelines
type Stage = Engine.Stage RenderPasses Pipelines
type StageFrameRIO fr rs = Engine.StageFrameRIO RenderPasses Pipelines fr rs
type StageResources fr rs = Stage.Resources RenderPasses Pipelines fr rs
type StageScene fr rs = Stage.Scene RenderPasses Pipelines fr rs

type RenderPasses = Basic.RenderPasses

type Pipelines = Basic.Pipelines

component :: Basic.Rendering st
component = Basic.rendering_

Assets

TBD

Stage

Setup

app/Stage/<StageName>.hs:

module Stage.StageName where

-- the prelude
import RIO

import Engine.Stage.Component qualified as Stage
import Engine.Types (StackStage(..))

import Global.Render qualified as Render
import Stage.StageName.Resources qualified as Resources
import Stage.StageName.Scene qualified as Scene
import Stage.StageName.Types (Stage)

stackStage :: StackStage
stackStage = StackStage stage

stage :: StageName.Stage
stage =
  Stage.assemble
    "Main"
    Render.component
    Resources.component
    (Just Scene.component)

Types

module Stage.StageName.Types where

import RIO

import Render.DescSets.Set0 qualified as Set0
import Vulkan.Core10 qualified as Vk

import Global.Render qualified as Render
import Global.Resource.Assets (Assets)

type Stage = Render.Stage FrameResources RunState
type Frame = Render.Frame FrameResources
type Scene = Render.StageScene RunState FrameResources
type RecordCommands a = Vk.CommandBuffer -> FrameResources -> Word32 -> Render.StageFrameRIO FrameResources RunState a

type Resources = Stage.Resources Render.RenderPasses Render.Pipelines RunState FrameResources

type UpdateBuffers a = RunState -> FrameResources -> Render.StageFrameRIO FrameResources RunState a

-- Double-buffered resources used in rendering
data FrameResources = FrameResources
  { frSceneUI :: Set0.FrameResource '[Set0.Scene]
  , frSceneWorld :: Set0.FrameResource '[Set0.Scene]
  }

-- Inter-frame persistent resources
data RunState = RunState
  { rsAssets :: Assets
  , rsSceneUI :: Set0.Process
  , rsSceneWorld :: Set0.Process
  }

Resources

module Stage.StageName.Resources where

import RIO

import Control.Monad.Trans.Resource (ResourceT)
import Engine.Stage.Component qualified as Stage
import Engine.Types (StageRIO, StageSetupRIO)
import Engine.Vulkan.Types (Queues)
import Resource.Region (ReleaseKey)
import Resource.Region qualified as Region
import Vulkan.Core10 qualified as Vk

import Global.Render qualified
import Stage.StageName.Types qualified as StageName

component :: StageName.Resources
component = Stage.Resources
  { rInitialRS = initialRunState
  , rInitialRR = initialRecycledResources
  }

initialRunState :: StageSetupRIO (ReleaseKey, StageName.RunState)
initialRunState = Region.run do
  -- Or, use a loader stage and request from arguments.
  rsAssets <- Assets.load

  ortho <- Camera.spawnOrthoPixelsCentered
  rsSceneUI <- Worker.spawnMerge1 mkSceneUI ortho

  pure StageName.RunState{..}

initialRecycledResources
  :: Queues Vk.CommandPool
  -> Global.Render.RenderPasses
  -> Global.Render.Pipelines
  -> ResourceT (StageRIO StageName.RunState) StageName.FrameResources
initialRecycledResources _pools _renderpasses _pipelines = do
  assets <- gets Game.rsAssets
  let combined = fmap snd assets.aCombined

  frSceneUI <- Scene.allocate
    pipelines.layout
    combined
    Nothing
    Nothing
    mempty
    Nothing

  frSceneWorld <- Scene.allocate
    pipelines.layout
    combined
    Nothing
    Nothing
    mempty
    Nothing

  pure StageName.FrameResources{..}

Scene

module Stage.StageName.Scene where

import RIO

import Engine.Stage.Component qualified as Stage
import Engine.Types qualified as Engine
import Engine.Vulkan.Swapchain qualified as Swapchain
import Render.Basic qualified as Basic
import Render.Pass (usePass)
import Resource.Region qualified as Region

import  Stage.StageName.Types qualified as StageName

component :: StageName.Scene
component = Stage.Scene
  { scBeforeLoop = pure () -- Set up event network
  , scUpdateBuffers = updateBuffers
  , scRecordCommands = recordCommands
  }

updateBuffers :: StageName.UpdateBuffers ()
updateBuffers StageName.RunState{..} StageName.FrameResources{..} = do
  Scene.observe rsSceneWorld frSceneWorld
  Scene.observe rsSceneUi frSceneUi

recordCommands :: StageName.RecordCommands ()
recordCommands cb StageName.FrameResources{} imageIndex = do
  Engine.Frame{fRenderpass, fSwapchainResources, fPipelines} <- asks snd
  usePass (Basic.rpForwardMsaa fRenderpass) imageIndex cb do
    Swapchain.setDynamicFullscreen cb fSwapchainResources