Skip to content

Runtime Boot

Teo, Lim edited this page Oct 8, 2026 · 12 revisions

Runtime Boot

The loader is the common runtime shell for NWBLot projects. It owns crash capture, logging, frame initialization, graphics/window setup, project-selectable filesystem mounting, asset codec registration, shader archive lookup, project callback creation, frame-loop execution, and ordered shutdown.

Startup Sequence

The runtime startup path is:

  1. Create a persistent crash-reporting arena.
  2. Install crash capture before command-line parsing and before logging setup.
  3. Parse loader options such as log server address, crash upload token, and GPU debug options.
  4. Initialize logging through the logger client/server path or standalone logging.
  5. Ask project code for frame client size and window title.
  6. Create Core::Frame.
  7. Attach telemetry upload callback when a logger client is available.
  8. Set the window title and pipeline cache directory.
  9. Call ConfigureProjectRuntime, copy any project filesystem factory into graphics, then apply loader graphics options.
  10. Initialize frame, graphics, input, window, CPU/GPU task schedulers, telemetry, and platform state.
  11. Resolve the runtime resource mount directory.
  12. Mount the cooked graphics volume from res through CreateFilesystem.
  13. Register auto-collected asset codecs.
  14. Create AssetManager over the mounted volume.
  15. Load shader archive records from the cooked shader archive index.
  16. Build ProjectRuntimeContext.
  17. Create project callbacks through CreateProjectEntryCallbacks.
  18. Call IProjectEntryCallbacks::onStartup.
  19. Register the project update callback with the frame.
  20. Show the frame and enter the frame loop.

The loader treats invalid project frame dimensions or an empty window title as fatal startup errors. It also fails startup if the graphics volume cannot be mounted or if shader archive records cannot be loaded.

The same startup handoff in chart form:

flowchart TD
    Crash["Install crash capture"]
    Options["Parse loader options"]
    Logging["Initialize logging"]
    Frame["Create Core::Frame"]
    Configure["ConfigureProjectRuntime"]
    Services["Initialize frame, graphics, input, CPU/GPU tasks, telemetry"]
    Mount["Resolve resource directory<br/>mount graphics IFilesystem"]
    Assets["Register codecs<br/>create AssetManager"]
    Shaders["Load shader archive records"]
    Context["Build ProjectRuntimeContext"]
    Project["Create project callbacks<br/>call onStartup"]
    Loop["Show frame<br/>enter frame loop"]

    Crash --> Options --> Logging --> Frame --> Configure --> Services --> Mount --> Assets --> Shaders --> Context --> Project --> Loop
Loading

Crash Capture Comes First

Crash capture is installed before most other runtime work. That matters because early failures during option parsing, logger setup, device creation, or resource mounting should still produce a useful package.

The loader later applies crash metadata after options are known:

  • runtime = loader
  • gpu_debug = true|false
  • crash upload destination and token when provided

GPU crash reports are routed through a registered GPU crash sink. The crash package may contain vendor GPU dumps when available, and it should still contain a text GPU crash report when richer diagnostics are unavailable.

Frame Creation

Core::Frame is the owner of the runtime loop. It owns:

  • graphics and the active backend
  • platform window lifecycle
  • input dispatch
  • clipboard and native text-input services
  • project update callback
  • project object arena
  • graphics object arena
  • CpuTaskScheduler and GpuTaskScheduler
  • telemetry capture and upload plumbing
  • frame graph registry
  • render pass list

Project code does not run the platform loop directly. It supplies callbacks and world creation/destruction functions; the frame calls those callbacks at the appropriate points.

Presentation Override

Projects can request HDR10 presentation in ConfigureProjectRuntime before the device is initialized. The loader-level --sdr flag overrides that request and forces the normal SDR path; for example, use python -m launcher run testbed -- --sdr.

The final mode is negotiated against the active Vulkan surface; unsupported HDR10 surfaces fall back to SDR. See HDR10 Presentation.

Resource Mounting

Runtime content is mounted from the res directory. The loader resolves the mount directory by probing candidate res locations in order and choosing the first one that actually contains a graphics volume segment file:

  1. <current working directory>/res
  2. <executable directory>/res
  3. <executable directory>/../res

If none of them contains a graphics segment, it falls back to the literal res. This is why the runtime can find cooked data whether it is launched from the repository root or from the executable output directory.

The loader then mounts through CreateFilesystem:

VolumeMountDesc graphicsMount(frame.projectObjectArena());
graphicsMount.volumeName = "graphics";
graphicsMount.mountDirectory = resourceMountDirectory;
UniquePtr<IFilesystem> graphicsFilesystem = CreateFilesystem(
    frame.projectObjectArena(), graphicsMount, startupContext.filesystemFactory
);

The mounted filesystem becomes the runtime binary source. A loader-local FilesystemAssetBinarySource implements IAssetBinarySource by forwarding virtual-path reads to IFilesystem::readFile. See Filesystem.

Runtime asset loading therefore looks like this:

  1. Project or engine system requests a typed asset by virtual path.
  2. AssetManager asks the binary source for bytes.
  3. IFilesystem reads those bytes from the mounted cooked volume.
  4. AssetRegistry picks the codec for the requested asset type.
  5. The codec builds a typed asset object from binary payload data.

Shader Archive Lookup

Shader source is not compiled at runtime. The asset pipeline writes compiled shader bytecode and a shader archive index. During startup the loader loads shader archive records into memory and installs a resolver callback in ProjectRuntimeContext.

The resolver maps:

  • shader name (Name)
  • variant name (AStringView)
  • stage name (Name)

to a cooked virtual path. Renderer systems use that callback when they need to load a shader asset for a material, skinning pass, deferred pass, CSG pass, AVBOIT pass, custom UI pass, or shadow pass.

Known physical stages load through ShaderAssetLoader::Load<TShader>(...), deriving the stage and registered codec from concrete types such as ComputeShader, PixelShader, or VertexShader. Genuinely dynamic stage requests use LoadForStage(...). Archive roles such as mesh_compute and mesh_object_vertex select lookup keys independently of the physical compute/vertex type. Concrete admission validates that type against the exact recorded SPIR-V entry point; IShader supplies shared entry-point and bytecode access without per-frame virtual stage dispatch.

Project Runtime Context

The loader builds ProjectRuntimeContext after assets and shader archive records are ready. The context exposes:

  • Core::GraphicsRuntime& graphics
  • Core::InputDispatcher& input
  • Core::IClipboardService& clipboard
  • Core::ITextInputService& textInput
  • Core::Alloc::GlobalArena& objectArena
  • Core::CpuTaskScheduler& cpuTasks
  • Core::GpuTaskScheduler& gpuTasks
  • Core::CpuTaskScope& tasks
  • Core::Assets::AssetManager& assetManager
  • Core::Filesystem::IFilesystem& filesystem
  • Core::Telemetry::FrameGraphRegistry& frameGraphRegistry
  • const Core::Perf::Session& perfSession
  • shader path resolver callback
  • telemetry capture callback
  • telemetry upload flush callback
  • perf capture callback
  • request quit callback

Project code should treat this context as the bridge into engine services. It should not construct its own graphics backend, asset manager, or frame loop.

Frame Loop Handoff

After project startup succeeds, the loader sets a project update callback on the frame. Each frame then follows the frame-owned path:

  1. process platform/window/input state
  2. call the project update callback
  3. prepare registered render passes
  4. render registered render passes
  5. present the backbuffer
  6. collect timing, telemetry, and frame graph data as configured

The frame does not tick the ECS world itself. The loader and frame hold no reference to the world, and ProjectRuntimeContext does not expose it. ECS systems advance because the project ticks the world it owns from inside the update callback (Testbed does this in onUpdate). The render passes registered with the frame — MeshSkinningSystem, RendererSystem, and UiLayerSystem — are the exception: they run in the prepare/render steps above.

If project update returns failure or requests quit, the frame loop exits through the frame-owned shutdown path.

Ordered Shutdown

Shutdown order matters because render passes, ECS systems, asset objects, graphics resources, and backend command work reference each other.

On normal exit, the loader joins its project CPU task scope, calls the project's onShutdown, and joins that scope again. Testbed destroys its world inside this callback: it removes frame graph contributors and render passes, calls GraphicsRuntime::waitForIdle() and requires a successful join or terminal device loss, joins the world CPU task scope while clearing the ECS world, and releases the world. Project callbacks and engine-owned objects then leave scope in a controlled order.

Clone this wiki locally