Repository navigation
Thin-feature locks carried by their own depth, and spared only at the lamp (ROADMAP § 1a) - #67
Conversation
… lamp (live.wgsl Lock) A lamp finer than a pixel that no sample hit this frame went with what was behind it: its lock moved with the background, its history was read where the background had been, and the sky a lamp passed over kept its light (the history's pixels around a sparse lamp are dark too, so no sample there contradicted it, and the test for something gone from in front read the g-buffer, where the samples had mostly missed the lamp). The clamp in motion, spared by 0.75 over the nine traced samples around a lock (LOCK_RELAX), bounded the copies. - The lock keeps the view depth of what it is for and where in its traced pixel that is (up_state z, w). A pass before compose, lock_carry, over the previous frame's traced grid, takes each lock to where its point is now by the two cameras, onto the traced sample it falls in (lock_fwd, an atomic max per traced sample; in the merged layout it and the exposure histogram share a new atomic slab, MA). - judge: a live lock nearer than the samples is a candidate for the nearest surface among the nine, with the motion of its own depth. - lock_carry writes the previous locks' depths into the previous g-buffer half (only judge's gone test reads it by then); the gone test is barred by any live lock among the nine. - In motion the clamp spares the history in full within LOCK_REACH (1 output pixel) of the locked thing and nowhere else; LOCK_RELAX is gone. - The passes' grid sizes from one function (pass_grid), the profile and the normal path alike. Against offline (bench-window, the tower at the last moving frame, dE / SSIM / luminance): the play window's orbit 4.30 / 0.843 / 0.99 -> 3.90 / 0.865 / 0.97, a turn 2.12 / 0.914 / 0.84 -> 2.01 / 0.926 / 0.92. The reach brackets the truth (0.5 pixel 4.31 / 0.73, 1.5 pixels 4.17 / 1.17). New test upscale/lamps-parallax-live: main 0.357, now 0.075 (tolerance 0.1). upscale/lamps-in-motion-live removed: main passed it by its trail; lamps in motion keeping half their light is in Known issues. Unchanged: still shots, the slice, bench-ghosting, Tokyo at dusk. Cost +0.10 ms (Bistro 1440p at 0.375, alternated). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 86fef33. Configure here.
| at = L.jit.xy; | ||
| } | ||
| life *= 1.0 - reactive; | ||
| up_state[this_half(q)] = vec4<u32>(pack2x16float(vec2<f32>(now, select(now, f.last.x, f.n >= 1.0))), pack2x16float(vec2<f32>(sd * still, min(max(f.n + still, 1.0), 9.0))), pack2x16float(vec2<f32>(life, min(lz, 6.0e4))), pack2x16float(at)); |
There was a problem hiding this comment.
Carried lock depth never updates
Medium Severity
A carried lock stores the originating camera’s view depth (was.depth) instead of the current camera’s (cur_pixel’s z). The next lock_carry then rebuilds the world point from a new screen position plus that stale depth, and lock_vel / judge treat it as this frame’s depth. After the first miss the lock drifts whenever view depth changes (orbit, dolly, a turn), over the full jitter sequence.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 86fef33. Configure here.
There was a problem hiding this comment.
Fixed in 25c7ecf: Carried now has was (the previous view's depth, which the previous g-buffer half is in: what lock_carry writes there) and depth (this frame's, which the carried lock keeps). Rerun: test-render 190 / 190, the merged layout the 24 known failures; bench-window the tower in the orbit 3.90 (unchanged), the whole frame 2.32 -> 2.30.
Bugbot on #67: `carry` returned the lock's view depth in the previous frame's view, and `compose` stored it as this frame's. The next `lock_carry` rebuilt the point from the new place and that stale depth, and `judge` (`lock_vel`) took it as this frame's: a lock drifted wherever the view depth of what it was for changed (an orbit, a dolly). Now `Carried` has both: `was` (the previous view's, which the previous g-buffer half is in: what `lock_carry` writes there) and `depth` (this frame's, which the lock keeps). Rerun on the fix: cargo test 124 passed; test-render 190 / 190 (parallax 0.075, still 0.1057), merged the 24 known; bench-window the tower in the orbit 3.90 / 0.865 / 0.97 (as before), the whole frame 2.32 -> 2.30; the turn equal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


The fix planned after #60. When no sample hit a sub-pixel lamp in a frame, the lamp moved with the background behind it, which produced the trail when dragging the view. Tier R2: it changes what the live window shows in motion.
The fault
A lamp finer than a pixel that no sample hit this frame went with what was behind it:
The clamp in motion bounded the copies: it was spared by 0.75 over the nine traced samples around a lock (
LOCK_RELAX, #60).The change (
live.wgsl,Lock)up_statez, w).lock_carry, moves the locks. It runs beforecompose, over the previous frame's traced grid. It takes each lock to where its point is now, using the two cameras, onto the traced sample it falls in.lock_fwdholds the result: an atomic max per traced sample, so the longest-lived lock wins.lock_fwdand the exposure histogram share a new atomic slab, so the layout is still 8 storage buffers.judgereads the history where the lamp was. A live lock nearer than the samples counts as a candidate for the nearest surface among the nine, with the motion of its own depth.lock_carrywrites the previous locks' depths into the previous g-buffer half; by then only the gone test reads it. The test is now barred by any live lock among the nine. Before, it was barred only by a lock at half strength, which let a still lamp's own history go at the end of its lock.LOCK_REACH(1 output pixel, the reconstruction's reach) of the locked thing, and nowhere else.LOCK_RELAXis gone.pass_grid); the profile path had a copy of the expression.Merge gate (R2)
cargo test --releasenpm run typecheck,npm testtest:rendernativetest:render,THREERT_MERGED=1--quick --check, Bistro and Kabukichōtest:layer-block_carry, main 1.73–1.80 ms, branch 1.84–1.90; alternated, three runs per build). The perf smoke said FAIL (+1.2 ms still), then +1.2 / +2.4 with the drive 1.3–4.3 ms faster. Main is measured first, then the branch; its reads fall at two levels on this Mac. Alternated, the unchanged trace reads the same on both builds.bench-window, the tower region at the last moving frame (ΔE / SSIM / luminance against offline):
Tests
upscale/lamps-parallax-live. Lamps slide before the black sky, which the slide does not move; the test measures the light beside the field's edge. Main reads 0.357 of the field's mean, the branch 0.075. Tolerance 0.1: the rest is the gone test's narrow window, explained in the test's note.upscale/lamps-in-motion-live. Main passed it only because of its trail: the light the lamps left on the sky made up the mean. With the trail gone it reads 0.066 against 0.1. The real problem, lamps in motion keeping half their light (0.045 on main, 0.050 here, with a wall behind them), is in ROADMAP Known issues. Lock life is ruled out (two and four sequences: 0.052, 0.053), and the test comes with that fix.ROADMAP § 1a records the step.
🤖 Generated with Claude Code
Note
Medium Risk
Changes core temporal upscaler behavior (compose/judge/upscale) and adds a new GPU pass plus binding layout changes that affect WebGPU merged mode.
Overview
Fixes sub-pixel lamp trails when the camera moves by reworking thin-feature locks in the live upscaler: locks now store view depth and sub-pixel position, are reprojected via a new
lock_carrypass (beforecompose), and history/motion uses the lamp’s depth instead of drifting with the background.Shader / temporal behavior:
lock_fwd(atomic) forwards carried locks intocompose;judgetreats live locks as nearer-surface candidates and blocks the “gone” test when any lock is alive; motion clamping dropsLOCK_RELAXfor spatialLOCK_REACH(full unclamp only within 1 output pixel of the locked feature). G-buffer previous-half depths are patched from carried locks for the gone test.Runtime wiring: Live pass count and merged layout go to 25 storage buffers; exposure histogram and
lock_fwdshare a new atomic slab; dispatch grids are centralized inpass_grid(including previous traced size forlock_carry).Tests / docs: Replaces
upscale/lamps-in-motion-livewithupscale/lamps-parallax-live; documents remaining “half light” lamp issue; ROADMAP §1a updated with benchmarks (~+0.10 ms on Bistro).Reviewed by Cursor Bugbot for commit 25c7ecf. Bugbot is set up for automated code reviews on this repo. Configure here.