Lift picking out of the render frame (pointer-driven hover + click)#362
Merged
Conversation
Hover + click picking stop corrupting the visual uniform buffer and re-pointing on the next frame. Pick renderer owns its uniform buffer; the frame stashes its packed PointUniforms image; both pick paths read that snapshot and derive targets live. Drops the pointermove->full-frame re-render. Single packPointUniforms source of truth guards byte-layout drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9 tasks (8 impl + entanglement-radar review). packPointUniforms keystone, pick owns its buffer, frame-tail snapshot, pointer-driven hover driver, click re-pointed, runFrame slimmed. Contract code per plan-style.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…f truth draw() now calls the pure packer and returns the packed ArrayBuffer (null when no catalogs). UNIFORM_BYTES lives in packPointUniforms.ts (re-exported from pointRenderer to keep the public import path) to avoid a circular import. Task 1 of docs/superpowers/plans/2026-06-22-pick-out-of-frame.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Frame stashes its last packed uniform image here; the pick paths read it so a pick reproduces the last frame's camera without re-running the per-frame camera drivers. Initialised null. Task 2 of the pick-out-of-frame plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures draw()'s returned ArrayBuffer into state.picking.lastFrameUniformBytes (null return preserves the prior value). The pass owns the stash so the renderer stays ignorant of EngineState. Task 3 of the pick-out-of-frame plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uption) The pick pass no longer scribbles on pointRenderer.uniformBuffer and relies on the next frame to repair it. It allocates its own pickUniformBuffer, uploads the caller-supplied last-frame image (state.picking.lastFrameUniformBytes), and applies the 3 pick overrides there. createPickRenderer drops its pointRenderer arg; PointRenderer no longer exposes uniformBuffer. pick/renderForDebug gain a required uniformBytes; pointSizePx becomes required. Callers (runFrame, click) thread the snapshot through. Decoupling regression test asserts the pick never writes an external buffer. Task 4 of the pick-out-of-frame plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uler) Standalone Option-1 driver: in-flight coalescer + trailing-edge re-fire to catch the resting position. Reads only lastFrameUniformBytes off state; targets, viewport, pointSize, timing are live thunks. No rAF, no requestRender (hover feeds only the InfoCard, no scene halo). Task 5 of the pick-out-of-frame plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the hardcoded point-size default duplicate flagged in the Task 4 review; the sole caller always passes state.settings.galaxyCatalogs.sizePx. Single source of truth. Task 6 of the pick-out-of-frame plan (the snapshot read itself landed with Task 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… frame Canvas pointermove now calls hoverPickDriver.onPointerMove instead of scheduler.requestRender — a hover over a static scene no longer re-renders ~2.5M points. wireInput constructs the driver (live target/viewport/size/timing thunks) after pickRenderer is ready. pointerleave/pointerdown keep their wakes. Task 7 of the pick-out-of-frame plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…PickDebugOverlay The in-frame hover-pick block is gone (the pointer-driven driver owns it). The pick-debug overlay moves to its own helper (still frame-coupled — it composites on the swap chain — but out of the orchestrator body), reading the snapshot bytes with a null guard. Retired the now-unread latestMouseCss/ lastPickedMouseCss fields + MousePos type; updated the renderFrame docblock. Task 8 of the pick-out-of-frame plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Final-review regression: the in-frame hover block skipped picks while pointerDown (orbiting, not hovering); the new driver's maybeFire lacked that gate, so hover picks fired per-readback during a drag and dispatched spurious hover selections. Re-add 'if (pointerDown) return' + a drag-skip test; pointerDown is live state again. Sweeps the stale renderScheduler docblock and corrects the spec sketch (the omission's origin). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DoD audit READY: full suite 3028 green, typecheck clean both projects, all 57 checkboxes ticked, entanglement-radar all green (two-writer coupling gone, packPointUniforms sole packer, no re-braid), final whole-branch review APPROVE after the drag-gate regression fix. Relocates plan + spec to completed/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
skymap | a085147 | Commit Preview URL Branch Preview URL |
Jun 22 2026, 11:16 AM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
GPU picking used to live inside the render frame and worked by corrupting the visual render's shared uniform buffer —
pickRenderer.recordPickPassscribbled three fields intopointRenderer.uniformBufferand relied on the next visual frame to repair them. Consequences: a hover over a static scene re-rendered ~2.5M points (everypointermovecalledrequestRenderpurely to un-corrupt the buffer), and both pick paths were wedged afterqueue.submit().This PR makes picking an independent, pointer-driven concern. The frame hands picking exactly one thing — a CPU copy of the uniform bytes it last rendered with (
state.picking.lastFrameUniformBytes) — and the pick renderer uploads that to its own buffer.Core principle
Snapshot only what can't be re-derived live. The camera pose is derived once per frame (single-writer, clock advances once per frame), so a pick fired at pointer time can't legally re-derive
viewProj— that's the only thing snapshotted. Targets/viewport/point-size stay derived live at pick time (the click path already proved this correct).Changes
packPointUniforms— the 176-byte layout becomes one tested pure function (single source of truth; a layout mismatch is the class of bug that silently freezes iOS WebGPU). Byte-offset table test guards drift.pointRenderer.uniformBuffer(which is no longer public). The pick-debug overlay is fixed for free.draw's packed bytes intostate.picking.lastFrameUniformBytes.hoverPickDriver— pointer-driven scheduler: in-flight coalescer + trailing-edge re-fire, drag-gated (pointerDown), no rAF, norequestRender. Hover over a static scene no longer re-renders the scene.runFrameslimmed — dead in-frame hover block deleted; pick-debug overlay extracted todrawPickDebugOverlay; retiredlatestMouseCss/lastPickedMouseCss/MousePos.Verification
packPointUniformssole packer, no re-braid, dep bags narrow.completed/.Out of scope (unchanged)
Pick encoding/
resolvePick/selectionEncoding, the pick GPU pipeline/WGSL, touch/pen, double-click→focus.Suggested manual smoke test
Hover a galaxy → InfoCard updates; click → selects; drag → no hover flicker/storm; toggle the pick-buffer debug overlay → still draws.
🤖 Generated with Claude Code