Implement scene/spec.md — the declarative 3D rendering plugin, plus the m, gfx, wgpu and canvas changes it cannot be correct without, plus the seven acceptance demos in cog-examples.
This is the implementation effort that follows #1. That issue is the wayfinder map whose resolved tickets produced the spec; this one tracks building what the spec describes. Deferred and out-of-scope work hangs under #29 instead and is not part of this effort.
The spec is the contract the implementation is judged against. Where a ticket and the spec disagree, the spec wins — and if the implementation has to deviate, that gets recorded rather than quietly absorbed.
How to work this
Every sub-issue is labelled ready-for-agent and carries native blocked_by edges. Work the frontier: any ticket whose blockers are all closed. There is no assigned order beyond that.
Nothing here is a horizontal layer slice. Each ticket cuts a narrow but complete path and is verifiable on its own — by a test, by a running demo, or by a captured frame — with the two deliberate exceptions noted under Wide refactors below.
Phases
Roughly, though the dependency graph is the real authority:
| Phase |
Tickets |
What it delivers |
m foundations |
#59, #60, #61 |
3D matrix and projection math, bounds/frustum/rays, linear colour constructors |
| gfx/wgpu capability |
#63, #64, #65, #66, #67, #68, #69 |
formats, samplers, 3D pipeline state, explicit passes, the linear frame buffer, storage reflection |
| Colour migration |
#62, #70, #71, #72 |
expand → canvas sRGB → delete the bare constructors → adjudicate the visual shifts |
| Scene core |
#73, #74, #75, #76, #77, #79, #80, #81, #82, #84 |
the plugin, the first pixel, PBR, culling and sorting, debug shapes, helpers, lights, passes, meshes, instancing |
| glTF |
#85, #86, #87, #88, #89, #91, #92 |
assets, loading and residency, node selectors, overrides, the lookup facade, skinning, morphs |
| Demos and docs |
#78, #83, #90, #93, #94, #95, #96, #97, #98 |
the seven acceptance demos, the web build, README and instructions |
The tracer bullet is #74 — a camera and a box, deliberately unlit. It is the narrowest complete path through every layer, and the bundled PBR is split out into #75 so the first pixel is not gated on the BRDF.
Wide refactors
Two changes have a blast radius no vertical slice survives, and both are sequenced expand → migrate → contract rather than forced into a tracer bullet:
- Explicit render passes. #66 adds the pass API beside the implicit default pass so canvas keeps working; #67 migrates canvas onto it and deletes the implicit pass.
- The colour migration. #61 adds the linear constructors beside the bare ones; #70 does canvas's sRGB atlas and the key-colour re-derivation; #71 deletes
NewColor/NewColor8 and classifies all ~57 sites across all three trees by hand. Both consumers replace to the working tree with no version pin, so that sweep is atomic by construction.
Only those two tickets are not independently green, and each is green again the moment its successor lands.
Acceptance
go test ./cmd/scene/... is the one command this effort runs. The assertions live in _test.go files beside each demo and run with no GPU, because culling, sorting and packing all happen in the update-thread flush and the result is published as Passes(dst []PassView) including the frustum.
Everything a number or an ordering can catch is an assertion. Everything only eyes can judge is one falsifiable sentence per demo. The two visible criteria worth naming, both on cameras and both chosen over assertions on purpose — a sign error in the Y flip is exactly the bug an assertion written by the author of the flip will happily confirm:
- the nameplate stays glued to the cube in both viewports, and disappears rather than mirroring when the cube passes behind the camera
- clicking a cube tints that cube and no other, including through the composited texture camera
Desktop is the acceptance bar, with animated (#94) designated the web canary — it touches the most storage-buffer bindings, the budget has no spare, and a single unbound binding silently kills the whole frame with no error anywhere.
There is no CI, no testdata/, no golden images and no frame readback in this repo, and none of that is built here. Golden-image acceptance is #54.
Three things to know before starting
- #58 is a real gate, not a note. The storage-buffer budget is eight of eight against the browser core-adapter floor, with zero headroom, because every reflected binding is emitted
Vertex|Fragment unconditionally. It blocks skinning (#91) and morphs (#92), and an implementation must not add a ninth.
- #68 carries the migration's one falsifiable claim — a pixel-identical frame — and exists to be the bisect point between "the present pass is wrong" and "the migration looks different". Routing canvas's currently gamma-encoded output through an sRGB frame buffer and an OETF present pass may well turn out to double-encode; discovering that there is the ticket doing its job, and its acceptance criteria allow either outcome so long as the resolution is recorded.
- #62 must land before #71 starts. Every judgement the colour migration defers is triggered by looking, and the existing canvas suite is colour-blind — every colour literal in it is fixed-point, so it neither breaks nor verifies. Without captured "before" frames those triggers fire against a memory of how it used to look.
Edges worth revisiting
Three dependency edges are defensible rather than forced, and are the levers if this needs to parallelise harder:
Implement
scene/spec.md— the declarative 3D rendering plugin, plus them,gfx,wgpuandcanvaschanges it cannot be correct without, plus the seven acceptance demos incog-examples.This is the implementation effort that follows #1. That issue is the wayfinder map whose resolved tickets produced the spec; this one tracks building what the spec describes. Deferred and out-of-scope work hangs under #29 instead and is not part of this effort.
The spec is the contract the implementation is judged against. Where a ticket and the spec disagree, the spec wins — and if the implementation has to deviate, that gets recorded rather than quietly absorbed.
How to work this
Every sub-issue is labelled
ready-for-agentand carries nativeblocked_byedges. Work the frontier: any ticket whose blockers are all closed. There is no assigned order beyond that.Nothing here is a horizontal layer slice. Each ticket cuts a narrow but complete path and is verifiable on its own — by a test, by a running demo, or by a captured frame — with the two deliberate exceptions noted under Wide refactors below.
Phases
Roughly, though the dependency graph is the real authority:
mfoundationsThe tracer bullet is #74 — a camera and a box, deliberately unlit. It is the narrowest complete path through every layer, and the bundled PBR is split out into #75 so the first pixel is not gated on the BRDF.
Wide refactors
Two changes have a blast radius no vertical slice survives, and both are sequenced expand → migrate → contract rather than forced into a tracer bullet:
NewColor/NewColor8and classifies all ~57 sites across all three trees by hand. Both consumersreplaceto the working tree with no version pin, so that sweep is atomic by construction.Only those two tickets are not independently green, and each is green again the moment its successor lands.
Acceptance
go test ./cmd/scene/...is the one command this effort runs. The assertions live in_test.gofiles beside each demo and run with no GPU, because culling, sorting and packing all happen in the update-thread flush and the result is published asPasses(dst []PassView)including the frustum.Everything a number or an ordering can catch is an assertion. Everything only eyes can judge is one falsifiable sentence per demo. The two visible criteria worth naming, both on
camerasand both chosen over assertions on purpose — a sign error in the Y flip is exactly the bug an assertion written by the author of the flip will happily confirm:Desktop is the acceptance bar, with
animated(#94) designated the web canary — it touches the most storage-buffer bindings, the budget has no spare, and a single unbound binding silently kills the whole frame with no error anywhere.There is no CI, no
testdata/, no golden images and no frame readback in this repo, and none of that is built here. Golden-image acceptance is #54.Three things to know before starting
Vertex|Fragmentunconditionally. It blocks skinning (#91) and morphs (#92), and an implementation must not add a ninth.Edges worth revisiting
Three dependency edges are defensible rather than forced, and are the levers if this needs to parallelise harder: