DocsDecisions

ADR-0013: Tools and the application runtime (Phase 7)

Status: accepted, 2026-11-09.

Context #

Phase 7 asks for an ImGui debug overlay (entity inspector with editing, asset browser, render graph view, frame-time graph, log console), Tracy integration throughout, and a --headless mode for tests and benchmarks. The engine had no single owner of its subsystems: every test assembled Gfx, Resources, Renderer, the ECS and the rest by hand.

Decisions #

  1. App owns the subsystems and the loop (cinder/app). It builds the job system, the platform/window/swapchain (or nothing in headless mode), Gfx/Resources/Renderer, the physics world, the audio engine (device-less when headless), the animation system, the input map, the ECS world, a scene registry and an in-memory log. run_frame is the canonical frame: poll, fixed steps (ECS tick, kinematics in, physics step, dynamics out, Game.fixed_update), interpolated transforms, Game.update, animation, transform propagation, audio sync, asset streaming, render, overlay, present. Gameplay implements the Game trait, so the sample game, tests and benchmarks run the same code.
  2. Headless is the same loop without a window. Frames render into an RGBA8 offscreen texture that read_frame reads back. Every Cinder executable accepts --headless through parse_app_args, together with --width/--height/--title/--profile/--no-audio/--no-debug-ui.
  3. Subsystems with inbound pointers live in Boxed[T] (cinder/core/boxed.mojo): a heap cell with a stable address, so an App value can be moved and returned while Resources keeps pointing at the same Gfx. Fields are declared in destruction order (overlay and gameplay first, GPU last, the native library at the end).
  4. Tracy with a manual lifetime. The client is compiled with TRACY_MANUAL_LIFETIME and TRACY_DELAYED_INIT; nothing runs (no socket, no thread) until Profiler.start() (the --profile flag). Zones are RAII Zone values; the app wraps fixed steps, animation, audio and rendering, and Renderer.render / AssetServer.update open their own zones. Zone contexts cross the FFI as a packed uint64, so Mojo never sees Tracy's struct. In Tracy 0.15 messages are ___tracy_emit_logString.
  5. The overlay is data-driven. DebugUi renders through the shim's SDL3 + WebGPU ImGui backends into the swapchain frame after the scene. The inspector is built from the scene registry's document snapshot (SceneRegistry.to_doc), so any component that is scene-serialisable is also inspectable; numeric fields are editable and written back through the registry's reader (components with entity references are read-only, since a single-entity edit cannot resolve them). Strings handed to ImGui are copied into per-frame byte buffers, because String has inline storage and moves when a list grows.
  6. The swapchain frame is imported, not owned. Resources.import_texture_view wraps the acquired view in a handle for the renderer and forget_texture_view drops it without releasing, keeping the renderer's typed-handle API.
  7. Event routing. Platform.forward_to_imgui hands every raw SDL event to ImGui before translation; the app skips mouse/keyboard events for the game's InputMap while ImGui wants them.

Consequences #

  • Phase 8's sample game is a Game over App; its benchmarks run with --headless and read App.read_frame / renderer stats.
  • Engine components become inspectable as they gain SceneComponent conformance (Phase 8 registers the sample's components; built-in components follow when the scene format grows past v1).
  • dcimgui passes float v[3] arrays by value in the generated bindings, so the overlay uses one InputFloat per component instead of DragFloat3.
View as MarkdownSource: docs/adr/0013-tools-and-app-runtime.md