> Cinder 0.1.0 documentation: ADR-0009: Graphics layer shape (typed handles, error scopes for shader diagnostics)
> Canonical page: https://cinder.ash10.dev/docs/adr/0009-gfx-layer/ · Source: `docs/adr/0009-gfx-layer.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-0009: Graphics layer shape (typed handles, error scopes for shader diagnostics)

Status: accepted, 2026-11-08.

## Context

Phase 3 adds the Mojo-side graphics layer over wgpu-native. Three decisions needed recording: how
GPU objects are owned and referenced, how shader compile errors are obtained given the gaps in
wgpu-native 29.0.1.1, and how the typed pipeline layer talks to the raw bindings.

## Decisions

1. **Pooled ownership behind generational handles.** Every wgpu object the engine creates lives in a
   `Pool` inside `cinder.gfx.Resources` and is referenced by a phantom-tagged `Handle[Tag]`
   (`BufferHandle`, `TextureHandle`, ... `ComputePipelineHandle`). Stale handles raise
   `invalid handle`; shutdown releases whatever is left alive and reports the count as
   `leaked_at_shutdown`. Swapchain views are the only raw `WGPUTextureView`s outside the pool, because
   their lifetime is one frame and owned by the surface.

   *Amended 2026-11-09 (Phase 9 defect sweep):* the `leaked_at_shutdown` field could never be read
   (it was written in the destructor), so it was removed. Shutdown now appends one `gfx.resources`
   warning to `Gfx.diagnostics` listing what was still alive, and `Resources.live_counts()` is the
   check to run before teardown. Views imported with `import_texture_view` (the swapchain view and
   the App's offscreen target) are tracked in the pool but never released by it:
   `destroy_texture_view` refuses them and `forget_texture_view` drops them. `destroy_texture`
   refuses while views of the texture are alive, and `adopt_*`/`create_*` release the new object
   when the pool is full.
2. **Shader diagnostics through validation error scopes, not compilation info.**
   `wgpuShaderModuleGetCompilationInfo` is unimplemented in wgpu-native 29 (it aborts the process).
   `ShaderManager.compile_source` therefore pushes a `Validation` error scope, creates the module,
   pops the scope through the shim queue (`cn_wgpu_device_pop_error_scope`) and parses naga's report,
   which marks every location as `wgsl:LINE:COLUMN`. The message text is kept whole; the parsed
   locations drive editor-friendly `path:line:col` output. The event message cap was raised to 1000
   bytes so a full naga report survives the queue. This is reconsidered when the pinned wgpu-native
   implements compilation info.
3. **Typed pipeline layer builds raw descriptors on the stack and keeps every pointee alive with
   `keep`.** `cinder.gfx.pipeline` owns the translation from engine structs (`LayoutEntry`,
   `BindingResource`, `VertexLayout`, `RenderPipelineDesc`) to the wgpu descriptor graph. Pointees are
   plain `List`s and locals; `std.benchmark.keep` after the C call is the lifetime fence (see
   `docs/MOJO_NOTES.md`). Build functions (`build_render_pipeline`, `build_compute_pipeline`) are
   separate from `create_*` so hot reload can rebuild a pipeline and swap it with `replace_*` under the
   same handle.
4. **Swapchain reconfiguration is lazy and recovery is bounded.** `Swapchain.resize` only records the
   size; `acquire` reconfigures when needed, skips frames while the window has no area (minimised)
   and retries an `Outdated`/`Lost`/`Timeout` surface exactly once after reconfiguring before
   raising. Device loss is separate (`Gfx.lost`) and left to the application loop.
5. **Debug draw is a CPU line accumulator with a doubling GPU buffer.** One line-list pipeline, one
   upload per frame, no per-call allocation in steady state (`grows` counter is asserted flat).
6. **Depth convention baked in.** `DepthState.reverse_z` uses `GreaterEqual`, and
   `DepthAttachment.cleared_reverse_z` clears to 0.0, matching the reverse-Z infinite projection in
   `cinder.core.math.mat4`.

## Consequences

- Hot reload of shaders and pipelines is possible without invalidating user-held handles.
- Shader error reporting is exact in location and text but is one message per report rather than per
  naga diagnostic span; naga already concatenates spans into one report.
- The command layer (`cinder.gfx.commands`) exposes `wait_for_queue` and `read_buffer` for tests and
  tools only; the frame loop never blocks on them.
- Verified by `tests/gfx/test_resources.mojo`, `test_shaders.mojo` and `test_render.mojo` (offscreen
  triangle with compute-updated uniform, pixel readback).
