> Cinder 0.1.0 documentation: ADR-0013: Tools and the application runtime (Phase 7)
> Canonical page: https://cinder.ash10.dev/docs/adr/0013-tools-and-app-runtime/ · Source: `docs/adr/0013-tools-and-app-runtime.md` in the engine repository (engine commit 47c9352, imported 2026-11-09)
> Index of every page for agents: https://cinder.ash10.dev/llms.txt

# 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`.
