Skip to content

Live: local lights guided by light tiles - #87

Merged
yujiaxu-ops merged 8 commits into
mainfrom
light-tiles
Nov 14, 2026
Merged

yujiaxu-ops merged 8 commits into
mainfrom
light-tiles

Conversation

@yujiaxu-ops

@yujiaxu-ops yujiaxu-ops commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

The live kernel chose among shadow-casting local lights uniformly. With many of them, most behind walls, few candidates found a light that reaches the point.

Now each camera pixel reports the light its shadow ray found, a pass (light_tiles) makes each 8 x 8 tile's reports a list, and the next frame's paths take candidates from their tile's list beside the uniform ones, which then stand for the lights the list lacks. Unbiased whatever a list holds. The scheme is the one MegaLights and RTXPT guide their draws by; written from their published descriptions, no code taken.

Compiled in only for scenes with more than 8 shadow-casting local lights (F_LISTS). Other scenes get main's kernel.

Measured (M5 Pro, dE / flicker of the held frame, lower is better)

shot main this
Bistro 1.82 / 0.443 1.82 / 0.443
Kabukichō 4.65 / 0.70 4.65 / 0.71
still-rooms (new: 96 lamps, 12 rooms) 0.67 / 0.138 0.63 / 0.069
still-rooms-wide (new: 768 lamps) 1.01 / 0.126 0.58 / 0.081

The wide shot's reference was rendered with a light tree on another branch (kept in the local cache): main's offline chooses among the 768 lamps uniformly and is noisy at 4096 samples. Against main's own reference: dE 0.90 -> 0.71.

Cost: Bistro's trace 8.5 ms on both; Kabukichō's perf smoke passes; lamp-rooms' frame 9.05 -> 9.90 ms (trace +0.63, the pass 0.22).

Checks

  • cargo test --release: 143 passed. test-render: 192 / 192 (two new: light/many-shadowed, -live), with the loop guard.
  • render-identity against main: 170 outputs byte-identical, 21 within their run-to-run spread, 1 changed (the new live test, the only one with lists).
  • Loop guard at 5 limits on Bistro and both lamp-rooms scenes: no stop.
  • After review: the lists' indices and counts are stored as float values, not integers' bits (a denormal may be flushed to zero by a driver).

Not done

  • The reports and the lists live in the kernel's direct buffer (its fourth component, and after its pixels): the kernel has no binding to spare. To move with the binding layout's rework.
  • Lists are read at the same screen position, not reprojected: under fast camera motion the guide lags. Only still shots were measured.
  • Only camera hits report. Later vertices read their pixel's list.
  • The candidate counts (2, 1 past the camera hit) were set on the two lamp-rooms scenes, made for this.
  • A scene with lists pays more than its candidates explain (about 0.3 ms of the trace on lamp-rooms with none drawn): not understood.
  • Emissive triangles are not guided (main's tree, as before).

🤖 Generated with Claude Code


Note

Medium Risk
Touches core live tracing and light sampling with temporal feedback; biased if weighting is wrong, but designed unbiased and gated behind F_LISTS with scenes ≤8 lights unchanged.

Overview
Live path tracing now biases shadowed local-light resampling using per-tile history, similar in spirit to MegaLights/RTXPT, when a scene has more than eight shadow-casting local lights (F_LISTS).

Camera-hit shadow rays record the chosen light in the direct buffer’s fourth component. A new first live pass light_tiles aggregates those reports into sorted, deduplicated 8×8 tile lists (grid offset jitters each frame). On the next frame, pathtrace.wgsl adds list-driven RIS candidates alongside uniform picks, with weights so the estimator stays unbiased even if lists are stale or incomplete.

Host changes wire ltile params, enlarge the direct buffer for list storage, skip the pass when F_LISTS is off, and reset guidance after viewport resets. Benchmark scenes (lamp-rooms, lamp-rooms-wide) and render tests (light/many-shadowed, live variant) plus bench-window still-rooms shots document the noise/flicker wins on many-lamp interiors without affecting Bistro/Kabukichō-style scenes.

Reviewed by Cursor Bugbot for commit d418e3c. Bugbot is set up for automated code reviews on this repo. Configure here.

yujiaxu-ops and others added 6 commits November 15, 2026 02:07
scenes/lamp-rooms.ts (4 x 3 rooms, 96 spot and point lamps with ranges, no
sky) and lamp-rooms-wide.ts (12 x 8, 768), and their bench-window shots
still-rooms / still-rooms-wide: the case the other shots lack, where a light
sampler has to find the lamps a point can see.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Each camera pixel reports the local light its resampled shadow ray found; a
pass makes each 8 x 8 tile's reports a list; the next frame's paths take 4
candidates from their tile's list (2 past the camera hit) beside the uniform
ones, which then stand for the lights the list lacks. Unbiased whatever a
list holds. Off in scenes whose local lights are summed exactly.

still-rooms dE / flicker 0.67 / 0.138 -> 0.63 / 0.068; still-rooms-wide (768
lamps) 1.01 / 0.126 -> 0.58 / 0.081; Bistro and Kabukicho unchanged.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The lists' kernel code is compiled only for scenes with more than 8 local
lights (FEAT bit F_LISTS): others get main's kernel (Bistro trace 8.50 /
8.50 ms). Each list carries a 64-bit mask of its lights' low index bits, so
most uniform candidates skip the search. 2 list candidates by default.
light/many-shadowed(-live): 24 shadowed point lights, 10 hidden, analytic.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Lists are fed by the camera hit's resampling, which covers shadow-casting
lights only: a scene without more than 8 of them (Kabukicho: none) keeps
main's kernel. The mask saved nothing in the trace and cost the pass 0.11 ms.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
- The resampling's weight is one expression with or without a list (a
  list's candidate weighed relative to a uniform one's).
- RIS_EXACT is a constant in gpu.rs, held to the shader's by a test.
- THREERT_LT_LIST is gone (LIST_CANDIDATES).
- The lists' place in the direct buffer comes from layout::light_lists.
- light/many-shadowed at 1024 spp (64 left ~2 % of noise in its patch).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit b6831e2. Configure here.

Comment thread renderer/shaders/live.wgsl Outdated
let list = L.lt.z + (wg.y * L.lt.w + wg.x) * 64u;
let at = less.x + less.y + less.z + less.w;
if (first && at < 63u) { light_rw[list + at] = vec4<f32>(bitcast<f32>(key), bitcast<f32>(same.x + same.y + same.z + same.w), 0.0, 0.0); }
if (li == 0u) { light_rw[list + 63u] = vec4<f32>(bitcast<f32>(min(all.x + all.y + all.z + all.w, 63u)), 0.0, 0.0, 0.0); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Light indices stored as denormal floats

Medium Severity

Light-tile keys and unique counts are packed with bitcast into the vec4<f32> direct buffer. Integer values 1..63 are IEEE-754 denormal bit patterns, and WGSL allows implementations to flush denormals to zero. On a flush-to-zero device the stored count reads back as 0, so lt_list_n stays empty and guiding silently falls back to uniform resampling. Apple silicon (where this was measured) typically preserves f32 denormals; Vulkan/NVIDIA and some mobile GPUs often do not.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b6831e2. Configure here.

yujiaxu-ops and others added 2 commits November 15, 2026 03:40
A small integer's bits are a denormal float, which a driver may flush to
zero (the lists would read empty and the guiding stop, silently). Found in
review.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@yujiaxu-ops
yujiaxu-ops merged commit 1aae0b7 into main Nov 14, 2026
9 checks passed
@yujiaxu-ops
yujiaxu-ops deleted the light-tiles branch November 14, 2026 19:54
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