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 #
-
Pooled ownership behind generational handles. Every wgpu object the engine creates lives in a
Poolinsidecinder.gfx.Resourcesand is referenced by a phantom-taggedHandle[Tag](BufferHandle,TextureHandle, ...ComputePipelineHandle). Stale handles raiseinvalid handle; shutdown releases whatever is left alive and reports the count asleaked_at_shutdown. Swapchain views are the only rawWGPUTextureViews 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_shutdownfield could never be read (it was written in the destructor), so it was removed. Shutdown now appends onegfx.resourceswarning toGfx.diagnosticslisting what was still alive, andResources.live_counts()is the check to run before teardown. Views imported withimport_texture_view(the swapchain view and the App's offscreen target) are tracked in the pool but never released by it:destroy_texture_viewrefuses them andforget_texture_viewdrops them.destroy_texturerefuses while views of the texture are alive, andadopt_*/create_*release the new object when the pool is full. -
Shader diagnostics through validation error scopes, not compilation info.
wgpuShaderModuleGetCompilationInfois unimplemented in wgpu-native 29 (it aborts the process).ShaderManager.compile_sourcetherefore pushes aValidationerror 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 aswgsl:LINE:COLUMN. The message text is kept whole; the parsed locations drive editor-friendlypath:line:coloutput. 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. -
Typed pipeline layer builds raw descriptors on the stack and keeps every pointee alive with
keep.cinder.gfx.pipelineowns the translation from engine structs (LayoutEntry,BindingResource,VertexLayout,RenderPipelineDesc) to the wgpu descriptor graph. Pointees are plainLists and locals;std.benchmark.keepafter the C call is the lifetime fence (seedocs/MOJO_NOTES.md). Build functions (build_render_pipeline,build_compute_pipeline) are separate fromcreate_*so hot reload can rebuild a pipeline and swap it withreplace_*under the same handle. -
Swapchain reconfiguration is lazy and recovery is bounded.
Swapchain.resizeonly records the size;acquirereconfigures when needed, skips frames while the window has no area (minimised) and retries anOutdated/Lost/Timeoutsurface exactly once after reconfiguring before raising. Device loss is separate (Gfx.lost) and left to the application loop. -
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 (
growscounter is asserted flat). -
Depth convention baked in.
DepthState.reverse_zusesGreaterEqual, andDepthAttachment.cleared_reverse_zclears to 0.0, matching the reverse-Z infinite projection incinder.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) exposeswait_for_queueandread_bufferfor tests and tools only; the frame loop never blocks on them. - Verified by
tests/gfx/test_resources.mojo,test_shaders.mojoandtest_render.mojo(offscreen triangle with compute-updated uniform, pixel readback).