Skip to content

feat(engine): ContentLayer registry + FrameStep program — renderer-unification plan 02#420

Merged
rulkens merged 31 commits into
mainfrom
worktree-renderer-unification-02-registry-program
Jul 9, 2026
Merged

feat(engine): ContentLayer registry + FrameStep program — renderer-unification plan 02#420
rulkens merged 31 commits into
mainfrom
worktree-renderer-unification-02-registry-program

Conversation

@rulkens

@rulkens rulkens commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Executes renderer-unification plan 02 (registry + program) AND plan 03 (pick fold-in) via subagent-driven development, on one branch per user request. 29 commits, each task individually reviewed; final whole-branch review in progress.

Replaces the two-registry frame (HDR_PASSES / UI_PASSES + bespoke volume prepass) with one flat ContentLayer registry drawn by a FrameStep data program through a strategy-parameterized executor; then folds picking in as a drawPick aspect executed by a parallel per-slab pickProgram with a pure CPU frontmostPick resolver. Net effect: frame order and pick order are DATA; both byte-stash camera couplings are deleted.

Plan 02 — COMPLETE (pending visual gate)

All 11 tasks + interstitial EngineSliceState rename. Entanglement radar: all 8 locked axes survived; 1 doc finding fixed (blend documented as forward contract). Suite green throughout.

Plan 03 — COMPLETE (pending visual gate)

All 12 tasks. Radar: all three named braids verified dissolved (lastFrameUniformBytes AND lastFrameCam extinct; camera travels as values; no frame-ordering coupling); F2 residue (pick-override double-home) fixed behaviour-neutrally; the dead milkyWayPickVisible/lastFrameCam chain deleted.

Documented behaviour deltas (degenerate scenes only — plan §Documented behaviour deltas)

  1. All-hidden scene: click now clears a stale selection (empty pass decodes null) where nothing dispatched before.
  2. Zero-catalog scene with rings: click and hover now agree (rings were hoverable but not clickable).
  3. Pre-first-frame window: no-bytes-yet gate becomes engine-not-ready; same pose frame 1 renders.
  4. MW-only/zero-catalog: lastFrameCam→(deleted); the MW pick gate now derives from the pick-time camera — latent stale-camera bug fixed.

⚠️ PENDING USER VISUAL GATES (do not merge before these)

  1. Plan 02: default URL — orbit, toggle volumes/filaments/milky-way, select a galaxy (ring + labels + marker lines), focus a cluster; then ?gpuTimings — DebugPanel rows under the new slot names (scalar-volume, per-layer hdr rows, hdr→swap, per-layer swap rows, pick). Frame visually identical to main.
  2. Plan 03: hover/click on points, disks, structure rings, Milky Way; empty-space click clears; double-click focuses; ?showPickBuffer overlay matches the scene while orbiting.

Follow-ups filed (deliberately NOT in this behaviour-neutral PR)

  • P3 radar F1: pick-busy has two homes (driver flag + program inFlight) and pick()'s null conflates busy/background — pre-existing semantics; fix = busy-distinct return or internal latest-wins queue, then delete state.picking.pickInFlight.
  • P3 radar F3: settings→uniform-value projection enumerated twice (visual draw args vs pickUniformBytesOf) — fold into a shared projection after plan 04's shape is known.
  • pickProgram's cosmo/near0 ternaries dissolve into RenderTargetSpec pick rows when targets join the table (never grow a third arm).
  • Expose scalar-volume in the debug-toggle list (executor gate already covers it).
  • Layer↔pipeline blend parity test once plan 04 introduces within-target blend divergence.
  • Disk renderer Init bags carry dead context/canvas fields.
  • Destroy-reachability test for the wireInput-minted handle trio.

Note: the plan-03 "shader diff empty" parity check shows two comment-only hunks (sweep rewording); zero functional WESL changes, selectionEncoding parity test untouched.

🤖 Generated with Claude Code

…end, RenderTargetSpec, ContentLayer, CompositeStep, FrameStep, RenderStrategy

Verbatim from the renderer-unification spec's locked Core-concepts blocks
(plan 02, Task 1). No consumers yet — the registry and executor land in
later tasks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 8, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
skymap 277e687 Commit Preview URL

Branch Preview URL
Jul 09 2026, 09:07 AM

rulkens and others added 28 commits July 9, 2026 00:50
deriveSlabs instantiates the spec's two-row table per frame (near-field
f64 row layerless until the zoom-to-earth fold; cosmological f32 row
wraps today's vp). slabViewOf narrows a row to the per-step SlabView the
executor threads into layer draws. ReadyFrameContext gains slabs,
populated in deriveFrameContext beside vp so there is exactly one
derivation per frame. f32→f64 widening is exact, so the COSMO round-trip
is byte-equal and slabViewOf needs no special case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…EAR0 provisional-semantics note

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_LAYERS registry

Mechanical conversion per the migration table: slab COSMO, target hdr,
blend additive on every row; renderer refs move from the PassDeps bag to
state.gpu.*; vp/viewport reads go through the per-step SlabView. The old
encoders keep driving the frame through a transitional HDR_PASSES filter
until the program flip; UI_PASSES converts next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…deleted

CONTENT_LAYERS now carries all fourteen rows (nine hdr additive + five
swap over); UI_PASSES joins HDR_PASSES as a derived transitional filter.
Pass/PassDeps types are gone — layers read state.gpu.* directly, so
RenderFrameInput and RunFrameDeps shed their renderer fields and the
deps bag no longer threads through renderFrame/runFrame/startLoop.
startLoop keeps its fail-fast renderer-readiness check (an independent
boot invariant, not deps forwarding), repointed at state.gpu.*.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ming slots

frameProgram(tone) emits the five-step main program (flow compute,
volume render, hdr render, hdr→swap tonemap composite, swap render);
timedSlotsOf derives the GPU-timing slot list from the program + layer
registry so slot names can never drift from what the frame actually
runs. Dead code until the renderFrame flip wires it in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…E table

Runs a FrameStep program into one encoder: the step-kind switch is the
frame's only switch. Render steps select layers by (target, slab), get
one SlabView each, and honour the disabledPasses one-way override;
composites gate on a per-frame touched set and delegate to the
Compositor; first pass against a target clears with its table value,
later passes load. merged vs perLayerTimed rides as data — the timing
fork is a strategy, not two encoder functions. encodeFlowCompute slims
to (encoder, state) reading its own gates; its args bag is gone. Dead
code until the renderFrame flip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…icate-name trap

Two unrelated types shared the name EngineState: the engine's runtime
state bag (108 importers) and the Redux 'engine' slice shape (3). The
slice is now EngineSliceState so the names can never be confused at an
import site again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
renderFrame shrinks to focus-uniform write + executeFrame(frameProgram,
CONTENT_LAYERS) + submit; strategy derives from timingService.enabled.
scalarVolumeLayer joins the registry (volume target, before the hdr
group); deriveVolumeLiveness is the ONE shared projection both volume
layers' gates consume, dissolving the prepass/upsample double-gate that
could drift. Timing slots now derive from the program (TIMED_SLOTS);
TIMED_SLOT_NAMES deleted; DebugPanel reads the same derived list. The
engine-ready seam narrows compositor alongside the other required GPU
handles. Old encoders are no longer called — deleted next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… frame recorder

encodeHdrSingle/Split, encodeUiOverlay, encodeVolumePrepass and
encodeVolumes are unreferenced since the program flip; their transitional
HDR_PASSES/UI_PASSES registry views go with them (engine.ts's debug
name list reads CONTENT_LAYERS directly). encodeVolumes' assertions
live on in executeFrame/scalarVolumeLayer tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t the deleted encodeUiOverlay

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
createRenderTargets holds the spec rows (hdr rgba16float/1, volume
rgba16float/3, swap swapFormat/1), allocates and resizes every offscreen
texture in one place, and serves views by id ('swap' throws — the
executor resolves it per frame). postProcess and volumeOffscreen
dissolve; the readiness gate's two checks become the one renderTargets
check; runFrame's hand-enumerated resize pair becomes a single resize
call; the volume scale divisor now lives only on the 'volume' row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…chain format, always

Every renderer factory now declares its colour target as an explicit
targetFormat (positional or named per each factory's existing style);
no factory reads GpuContext.format for its target. flowFieldRenderer's
hdrFormat and instancedQuad's config format rename for uniformity, and
the shared instancedQuad factory narrows from ctx to device — it only
ever used .device. The registry target↔renderer-profile invariant is
now legible at every construction site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…PassDeps/Pass mention

Fifty files of prose caught up with the registry+program reality:
hdr/swap-target layer groups instead of HDR_PASSES/UI_PASSES, executeFrame
program steps instead of the deleted encoders, <name>Layer filenames.
Two draw-order claims corrected against the actual registry order.
Review nits folded in: typed vi.fn house form in executeFrame tests,
Readonly clear-value table, discriminating ctx-vs-target formats in the
four UI-overlay format tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…act; grouping is (target, slab)

The radar found ContentLayer.blend has no frame-path reader (the
pipeline bakes the real blend state; the executor groups by target and
slab). The field's docs now say so honestly, drawPick-style, and name
the layer↔pipeline parity check as the guardrail once a target's layers
stop agreeing on blend.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First non-zero raw pick value in near→far slab order, mirroring the
visible far→near OVER composite as a CPU fold: near content occludes
far, so the nearest slab's hit wins. Plan-03 Task 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Derives a ready frame context from the last rendered pose
(cameraRuntime.lastPose.current + projection) through the same
side-effect-free deriveFrameContext math the frame uses, with the PICK
source mask as visibleSourceMask. At rest this reproduces the frame's
camera exactly — the prerequisite for deleting the pick uniform
byte-stash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assembles the packed-field subset (selection sentinel, catalog + bias
settings, LOD fade constants) and delegates to packPointUniforms, which
stays the single byte-layout truth — drift between the pick and visual
uniforms is structurally impossible. The packer's settings param narrows
to the packed subset via Omit; the three draw-only fields were never
read by it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hoverPickDriver (via a uniformBytes thunk on its deps), wireInput's
click path, and drawPickDebugOverlay all derive their bytes through
pickFrameContext → slabViewOf(COSMO) → pickUniformBytesOf instead of
reading the per-frame byte stash. Null-guard shapes preserved (not-ready
now skips exactly where null-bytes used to). The stash is now written
but never read — deleted next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
state.picking.lastFrameUniformBytes is gone: every consumer rebuilds the
uniform at pick time from the slab view, so the stash was write-only.
PointRenderer.draw sheds its buffer return (it existed only to feed the
stash); the lastFrameCam snapshot now updates on every draw — the
preserve-on-null rule lost its trigger with the bytes gone, and the
unconditional stash is the more accurate input to the MW pick gate.
Doc sweep rewords every stashed-bytes mention to built-at-pick-time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e surface

Extracts the uniform upload, the three pick overrides, the group binds
and the per-source loop from recordPickPass; pick()/renderForDebug()
delegate to it ahead of the ring/disk/MW fold-ins, so behaviour is
unchanged until the pickProgram cutover. drawPoints uploads and binds
group(0) even with zero sources — the ring and MW pick pipelines read
the camera through that prefix, pinned by test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…a stash dissolved

proceduralDiskRenderer's five cached camera fields (written by the
visual draw, replayed by pickDisks) are gone; the pick uniform packs
from caller-supplied values with an unchanged layout. The instance
replay stays — that is content (the last-drawn disk LOD set), not
camera. The interim pickRenderer fold-in unpacks the camera from the
uniform bytes its callers already pass; that call site dies at the
pickProgram cutover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pointSprites (drawPoints with mask-filtered sources + rebuilt uniform
bytes — the group(0) prefix every other pick pipeline reads),
proceduralDisks (pickDisks with the slab-view camera), milkyWay
(pickMilkyWay; visibility rides the enabled gate against the pick
camera) and structureMarkers (pickRing). Registry-driven only — no
name list; the rows are dead until the pickProgram executes them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Registry-driven (drawPick presence + enabled, grouped by slab, resolved
near→far with frontmostPick + unpackPick), one encoder one submit,
mapAsync-throttled with the ported inFlight/destroy guards. Owns its
pick targets lazily per slab — pick:near0 never allocates at N=1.
drawPick gains a documented postcondition: leave group(0) bound to the
shared camera prefix — pickRenderer.bindCamera is the narrow restore
surface and the disks row restores after its own slot-0 bind, keeping
the program name-blind. Dead until the cutover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n pickProgram

hoverPickDriver slims to { state, pickProgram, store, resolveDeps } —
the collectTargets/viewport/pointSize/timing/uniformBytes thunks all
dissolve into the program. Click resolution takes bare texel coords;
the debug overlay calls renderForDebug() and keeps its post-frame
placement as a latency choice, not a data dependency. The old
pickRenderer.pick path is now uncalled by production and dies next.

Documented behaviour deltas (degenerate scenes only, per the plan):
1. All-hidden scene: an empty pick pass runs and decodes null — a click
   now clears a stale selection where nothing dispatched before.
2. Zero-catalog scene with visible rings: click and hover now agree
   (both pick); rings were hoverable but not clickable.
3. Pre-first-frame window: the no-bytes-yet gate becomes engine-not-
   ready; lastPose seeding means the same pose frame 1 renders.
4. MW-only/zero-catalog: lastFrameCam updates every draw (was preserved
   -on-null) — the MW pick gate answers for the actually-rendered pose;
   a latent-bug fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pick(), renderForDebug(), recordPickPass and its fold-in tail, the
readback textures/staging/inFlight machinery and the fold-in renderer
constructor params are gone — the pickProgram owns the pick lifecycle.
Surface: { label, drawPoints, bindCamera, destroy }. collectPickTargets
dies with them (mask filter lives in pointSprites' drawPick; the hasAny
rule in the program's registry filter). Deleted test coverage accounted:
ported to pickProgram/proceduralDiskRenderer suites or died with the
fold-in order they asserted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…chain

The pick cutover moved the MW pick gate onto milkyWayLayer.enabled with
the pick-time context, leaving milkyWayPickVisible callerless and
lastFrameCam — its only reader gone — a write-only mirror of the frame
camera: the last vestige of the frame→pick coupling this phase deletes.
Both go, along with PickFrameCam (single-consumer type). Draw and pick
now share ONE gate, which is stronger than the old mirrored-pair
can't-drift claim. EnginePickingState stays (pickInFlight + pointerDown
remain live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he complete pick image

The sentinel, the padded point size and the pick-pass flag were written
twice: once into the bytes, again as post-upload overrides in
drawPoints (the two docblocks already disagreed on the count).
pickUniformBytesOf now packs all three (byte-identical to the old
end-state, pinned by test); drawPoints uploads verbatim and sheds its
pointSizePx param. PICK_PADDING_PX moves to a GPU-free data module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… verified

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rulkens and others added 2 commits July 9, 2026 10:26
… the program, no dead-symbol references

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/feature-done audit: tests 3938/3938 (623 files) PASS, typecheck PASS,
all checkboxes ticked (user visual gates for both plans passed
2026-07-09), no new TODO markers, no unowned deferred items — follow-up
register lives in PR #420 body + .superpowers/sdd/progress.md. Spec +
00-index stay in place for plan 04 (fold zoom-to-earth on the #386
branch). picking-gpu-subsystem backlog item stays — orthogonal
GPU-resource-ownership migration, out of this spec's scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rulkens rulkens marked this pull request as ready for review July 9, 2026 09:06
@rulkens rulkens merged commit ffc9c21 into main Jul 9, 2026
1 of 2 checks passed
@rulkens rulkens deleted the worktree-renderer-unification-02-registry-program branch July 9, 2026 09:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant