Engine Lifecycle
Purpose
Section titled “Purpose”Engine owns the window, input, renderer, the engine servers and a SceneTree, and drives the
game loop through Silk.NET’s window events. Games subclass it, put nodes in the tree (usually a scene
loaded from a file) and let the tree run the frame; three optional legacy hooks remain for code that draws
or updates by hand.
Key types
Section titled “Key types”| Type | File | Notes |
|---|---|---|
Engine | Engine.cs | public abstract class Engine : IDisposable; Tree (SceneTree), Root, Servers |
EngineOptions | Engine.cs | struct with required GameName, RenderingBackend = Vulkan, WindowSize = 800×600, IconPath, VSync = true, EnableValidation = DefaultEnableValidation (true in Debug, false in Release), EnableFrameCapture, WindowVisible = true, MaxFrames (0 = until closed), FixedDeltaTime (0 = wall clock), PhysicsTicksPerSecond = 60, Physics3D/Physics2D (physics settings; Physics2D.PixelsPerMeter = 100), DebugCollisionShapes, SteamAppId (0 = no Steam), SteamWriteDevAppIdFile (development runs: steam_appid.txt), Audio (AudioOptions: Enabled = true, Device = Auto, 48 kHz, 10 ms, BusLayoutPath), EnableUi = true (registers the UiServer, M8), Ui (UiServerOptions), DevOverlayVisible = false (the RmlUi dev overlay; F12 toggles Engine.DevOverlayVisible, see Developer overlay) |
FrameCapture | FrameCapture.cs | RGBA8 pixels of a rendered frame, SavePng(path) |
GameTime | GameTime.cs | FrameCount, DeltaTime, FramesPerSecond, FramesTimeMs |
FPSCounter | FPSCounter.cs | 500 ms sampling window |
ExitCode | ExitCode.cs | Ok = 0, Error = 1 |
RenderingBackend | RenderingBackend.cs | Vulkan only |
Writing a game
Section titled “Writing a game”Game projects normally do not subclass Engine: GameHost : Engine runs a project.mfproj (window, physics,
input map, autoloads, main scene) — see Projects & GameHost. Subclassing Engine (as the
editor and render tests do) remains supported:
public sealed class Game() : Engine(new EngineOptions { GameName = "My Game" }){ protected override void OnLoad() { base.OnLoad(); // input, renderer, servers Tree.ChangeSceneToFile("Content/Scenes/Main.mscene"); // the tree processes and renders it }
// Optional legacy hooks (virtual since M2): OnUpdate, OnShadowPass, OnRenderMainPass.}- Call
base.OnLoad()first (it creates input, renderer, theRenderServer— and theSteamServerwhenSteamAppIdis set, andMultiplayerApiasEngine.Multiplayer— and routes input into the tree). - Call
base.OnClose()last (it frees the scene tree, disposes the servers, clears the resource cache, then disposes input and the renderer). - Behaviour lives in node types (
OnProcess,OnPhysicsProcess,OnInput, …), see Scene graph & nodes. A game can still keep everything in the legacy hooks: an empty tree costs nothing. Run()blocks until the window closes and returns anExitCode.Quit(code)records the code and requests shutdown; the window closes at the end of that iteration’s render (SDL raisesClosingsynchronously insideClose(), andOnClosedisposes the renderer, so closing mid-update would dispose it under a running frame).Run()then returns the code — covered by theQuitWithErrorReturnsErrorExitCoderender test.MaxFramescloses the same way.Engine.FramebufferSizeis the drawable size in pixels;Window.Sizeis in points. For a camera’s aspect ratio insideOnRenderMainPass, useIVulkanContext.SwapchainExtent— the image actually being rendered (it can lag the window by a frame during a resize).
Startup
Section titled “Startup”- Constructor: stores options → selects SDL (
SdlWindowing/SdlInput.RegisterPlatform(),Window.PrioritizeSdl()) →VulkanLoaderBootstrap.Initialize()(macOS probe +SDL_Vulkan_LoadLibraryhandoff, see Build & platforms) → creates the window fromWindowOptions.DefaultVulkan(title"{GameName} ({RenderingBackend})", API 1.2) → subscribesLoad,FramebufferResize,Update,Render,Closing. Load: centres the window on its monitor (skipped when none is reported, e.g. a sleeping macOS display), thenOnLoad()(base):Window.CreateInput()(SDL input) →new VulkanRenderer(Window, { EnableValidation, VSync, EnableFrameCapture })→Servers.Register(new RenderServer(Renderer))(+SteamServer, +MultiplayerApi.Attach(Tree)) →AudioServer.Create(options.Audio, Tree)whenAudio.Enabled(never fails startup: no device means the silent null device; see Audio) →PhysicsServer3D,PhysicsServer2D(Physics) →UiServerand theDevOverlaywith its built-in panels (Developer overlay) →new InputRouter(InputContext, Tree)→SetWindowIcon(IconPath)(StbImageSharp, RGBA). TheSceneTreeitself is created in the constructor (no GPU needed), so nodes can be built beforeOnLoad; visuals that enter the tree before the render server exists get their GPU objects lazily.
Deterministic runs and frame capture
Section titled “Deterministic runs and frame capture”Tests and QA tools set FixedDeltaTime (every update gets that delta instead of wall-clock time) and
MaxFrames (the window closes after that many rendered frames; RenderedFrameCount excludes skipped
frames). CaptureFrame() — legal from OnUpdate, a node’s OnProcess or a render hook, and only with
EnableFrameCapture — copies the frame being built back to the CPU; after EndFrame the engine calls
protected virtual OnFrameCaptured(FrameCapture). See Testing.
The frame
Section titled “The frame”Update and Render are separate Silk.NET window events. The scene tree ticks after the legacy
OnUpdate hook, and the shadow pass runs with the command buffer
open but no render pass active.
sequenceDiagram
autonumber
participant W as Silk Window
participant E as Engine
participant G as Game (subclass)
participant R as VulkanRenderer
W->>E: Update(delta)
E->>E: FPSCounter.Update(), fill GameTime
E->>G: OnUpdate(gameTime)
E->>E: Tree.Tick(gameTime) — physics steps, OnProcess, deferred/QueueFree, transform sync, frame servers (UiServer: UI update + render into its command list)
W->>E: Render(delta)
alt minimised (WindowState or 0×0 drawable)
E->>E: ScreenGizmos.Clear() — nothing is drawn
Note over E,W: IsEventDriven = true: the loop blocks on window events until restored
else
E->>R: BeginFrame() (slot fence wait, acquire, begin cmd buffer)
alt FrameStarted
E->>G: OnShadowPass(gameTime) — no render pass active
E->>R: RenderServer.RenderShadows(Root) — the tree's shadow casters
E->>R: BeginRenderPass() — HDR scene target: clear color (linear) + depth
E->>R: RenderServer.RenderMain(Root) — set 0 (camera + lights), sky, then the tree's visuals
E->>G: OnRenderMainPass(gameTime) — anything drawn by hand
else swapchain out of date / being rebuilt
E->>E: ScreenGizmos.Clear()
end
E->>R: EndFrame() — UI layers offscreen, tonemap into the swapchain, then the overlay pass in OverlayOrder (canvas, screen gizmos, UI layers, dev overlay)#59; end pass, submit, present
end
Minimise
Section titled “Minimise”While the window is minimised (WindowState.Minimized, or Engine.FramebufferSize is 0×0) OnRender
renders nothing (the screen gizmos are discarded), and switches the window to IsEventDriven so Silk’s loop
blocks in SDL_WaitEvent instead of spinning; the first render after restore switches it back. The
renderer likewise refuses to rebuild a 0×0 swapchain and keeps the request pending (no busy wait).
Covered by the render test MinimiseAndRestoreIsCleanAndResumesRendering (the host’s --minimize <frame>: minimise,
stay minimised 1.5 s while a timer pushes SDL window events so updates keep running, restore): no frames are rendered
while minimised, validation stays clean, and frames captured after the restore have the right size. (Where the window
system cannot minimise, e.g. a bare Xvfb, the run only proves that nothing breaks.)
MaxFPS and VSync
Section titled “MaxFPS and VSync”Engine.MaxFPSsets bothWindow.FramesPerSecondandWindow.UpdatesPerSecond(≤ 0 → unlimited). It has no effect while VSync is on.Renderer.VSyncchanges the present mode by forcing a swapchain rebuild. See Vulkan renderer.
GameTime and FPSCounter
Section titled “GameTime and FPSCounter”FPSCounter.Update() counts frames and, every ≥ 500 ms, computes Fps = frames / seconds and
Ms = 1000 / Fps (integer). It is ticked from the Update event, so it measures update rate,
not presented frames.
Engine.LastFrameCpuMilliseconds is the CPU cost of the last rendered frame: its updates (OnUpdate, the
tree tick) plus RenderFrame (draw-list build, command recording, submit), minus the time the renderer was blocked
on the GPU or the swapchain (IVulkanContext.LastFrameWaitMilliseconds: the frame slot’s fence, acquire, present and
a capture’s read-back). It does not depend on GPU speed, so the render tests’ frame-rate gate uses it on CPU Vulkan
devices (Testing).
Shutdown
Section titled “Shutdown”Closing → OnClose() (base): dispose the input router → Tree.Shutdown() (frees every node, so
visuals release their GPU objects and audio players stop) → Servers.Dispose() (reverse registration order — registered Render, Steam, Audio, Physics 3D/2D, UI: the UI server
releases RmlUi and its GPU objects first, the physics servers their worlds, the audio server stops the device and
streaming thread; the render server releases anything still alive and its ShadowSystem last) → ResourceLoader.ClearCache() →
dispose input → dispose renderer.
Run() returns the exit code (Ok unless Quit(code) set another). Dispose() disposes the window.
Invariants
Section titled “Invariants”Rendererisnull!untilbase.OnLoad()runs.Servers.Renderis null untilbase.OnLoad()runs; nodes reach servers throughTree.Servers(there is no staticNode.Initializeany more). See Scene graph & nodes.- Anything drawn by hand must be recorded inside
OnShadowPassorOnRenderMainPass; the command buffer is only valid whileIVulkanContext.FrameStartedis true.
Known issues
Section titled “Known issues”EngineOptions.RenderingBackendis ignored;VulkanRendereris always created.- FPS counts updates, not presents.
- README/CLAUDE.md show
Game(in EngineOptions options); a parameterless primary constructor that passes options toEnginedirectly works too. - The tree runs one fixed-step loop per update event, so physics process follows
UpdatesPerSecondframes (accumulated), not a separate thread.
Related docs
Section titled “Related docs”Architecture overview · Vulkan renderer · Demo · Build & platforms · Scene graph & nodes