Repository navigation
The sampler: shuffled, Owen-scrambled Sobol (Burley 2020, as Cycles) - #81
Conversation
… a dimension id a decision The kernel drew white noise from one PCG stream; a draw's place in it depended on the path's branches. Now shaders/sample.wgsl: Owen-scrambled Sobol with Burley's hash (Vegdahl's constants), four dimensions padded by dimension ids. Each decision at a vertex has an id (rng_dim: path, vertex, slot; rng_sub: a loop's iteration or a light), which seeds the scramble; the sample index is the sample offline and the frame live (16 bits). - Offline every draw is Sobol: RMSE x0.29-0.46 at 16-256 spp (Cornell box, Light Room, Bistro), means within 0.01 %. - Live only the camera path's first vertex is Sobol, a hash elsewhere (as NVIDIA's RTXPT): all-Sobol cost 2.1 ms of Bistro's 8.4 ms live trace; this costs nothing measurable, and the live shots (bench-window) are unchanged. - Sobol dimensions 1-3 unrolled with the direction numbers inline (no loop, no memory). - The loop guard skips counted loops (for (var i = N; i < M; i++), literals), as Tint does. - sample.rs: the same sampler on the CPU; tests of the (0, m, 2)-net property and of the shader against it on the GPU. 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 a37eb18. Configure here.
| }; | ||
| let d = &g.device; | ||
| let seeds = [0u32, 0x1234_5678, hash_hp(99)]; | ||
| let src = format!("{}\nvar<private> cur_depth: u32;\n@group(0) @binding(0) var<storage, read_write> out: array<u32>;\nconst SEEDS = array<u32, 3>({}u, {}u, {}u);\n@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id: vec3<u32>) {{\n let i = id.x;\n RNG_SEED = SEEDS[i % 3u];\n RNG_INDEX = i;\n let r = rng4(7u + i % 5u);\n for (var k = 0u; k < 4u; k++) {{ out[i * 4u + k] = bitcast<u32>(r[k]); }}\n}}\n", include_str!("../shaders/sample.wgsl"), seeds[0], seeds[1], seeds[2]); |
There was a problem hiding this comment.
GPU sampler test omits LIVE
Low Severity
The standalone shader in the_shader_is_this includes sample.wgsl, which reads LIVE inside rng_white, but never declares that override. pathtrace.wgsl provides it in the real kernel; this test module does not. On a GPU the module fails to compile, so the CPU/GPU sampler check never runs; without a GPU the test skips and the mismatch stays hidden.
Reviewed by Cursor Bugbot for commit a37eb18. Configure here.
There was a problem hiding this comment.
Right: since rng_white the test's shader needed LIVE, and the test only runs where there is a GPU. Fixed in 6e670ad (override LIVE = false); it passes on this Mac's GPU again.
…y test's LIVE; lamp-inside's converged value - rng_start mixes the render's seed (P.size.w: THREERT_SEED offline) into each pixel's again: renders had stopped changing with THREERT_SEED. - Delta tracking's steps (media_track, media_tr: a loop of no fixed length) draw from a hash, offline too, as pbrt-v4 does: a new Sobol dimension a step is QMC's worst case; a dense grid's region mean varied 0.9 % between seeds at 4096 spp (0.3 % now, as white noise). - The parity test's shader declares LIVE (Bugbot): it had stopped compiling when rng_white came. - volume/lamp-inside expects 6.72, its converged value (6.70-6.74 at 32768 spp, both samplers); the 6.607 it had came unconverged from the original repository (the extraction-day build reads 6.697). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


The kernel used to draw white noise from a single PCG stream, so a draw's position in that stream depended on which branches the path took. It now uses shuffled, Owen-scrambled Sobol (Burley 2020, the same design as Cycles' Sobol-Burley), in
shaders/sample.wgsl.Dimensions. The sequence has four Sobol dimensions; higher dimensions come from padding by dimension id. Each decision at a path vertex has its own id:
rng_dimbuilds the id from the path, the vertex and a named slot (D_*);rng_subbuilds the id for one iteration of a loop or for one light.The id seeds the scramble.
Sample index. Offline, the index is the sample number; live, it is the frame. It is 16 bits wide.
Offline: every draw comes from Sobol, except the steps of delta tracking. Those form a loop with no fixed length, so each step draws from a hash instead, as pbrt-v4 does.
Live: only the camera path's first vertex uses Sobol; everything else uses a hash, as NVIDIA's RTXPT does. Using Sobol for every draw cost 2.1 ms of Bistro's 8.4 ms live trace.
Sobol dimensions 1–3 are unrolled, with the direction numbers written inline.
The loop guard now skips counted loops, i.e.
for (var i = N; i < M; i++)where N and M are literals. Tint does the same. Without this, the sampler's own loops would use up the guard's iteration budget.sample.rsis the same sampler on the CPU. Its tests check:Checks
Offline convergence. Raw HDR at 320×180 with no denoiser. Each build's error is measured against the other build's render at 4096 spp. The table gives RMSE of this branch divided by RMSE of main:
At 4096 spp the means of the two builds agree within 0.01 %.
Live cost. Bistro's trace takes the same time as on main: 8.60 ms against 8.61 ms natively, 47.5 ms against 47.6 ms merged.
Live quality. bench-window shows the same results on every shot:
Atomic tests. test-render passes 190/190 natively. Merged, the same 24 known software-traversal failures as main, with identical names.
Unit tests.
cargo test --releasewith the guard: 142 passed.Browser. web-hello passes in Chrome.
Fixed after the first review (6e670ad):
THREERT_SEED) had stopped affecting renders. It is mixed into each pixel's seed again.LIVE(Bugbot).volume/lamp-insidenow expects 6.72, its converged value. Both samplers give 6.70–6.74 at 32768 spp. The old 6.607 came from the original repository and was never converged: the build from the day of the extraction also reads 6.697.With these fixes, test-render passes 190/190 natively, and merged shows the same 24 failures as main.
The blue-noise pixel distribution (Cycles 4.2) comes in its own PR.
🤖 Generated with Claude Code
Note
Medium Risk
Wide-reaching changes to how every path-tracing random draw is produced, though live cost is unchanged and tests/parity checks cover the new sampler.
Overview
Replaces the path kernel’s PCG white-noise stream (draw order tied to branch paths) with shuffled, Owen-scrambled Sobol (Burley 2020 / Cycles-style) in new
sample.wgsl, compiled into the kernel viaKERNEL_FILES.Each stochastic decision now uses a stable dimension id (
rng_dim/rng_subandD_*slots) so samples stratify by index offline (per spp) and live (per frame). Offline draws are Sobol everywhere; live uses Sobol only on the camera path’s first vertex and hashed RNG elsewhere (RTXPT-style) to avoid trace cost.pathtrace.wgslandtranslucent.wgsldroprnd()/rng_stateand threadrdthrough lighting, BSDF, media, cache, and fog paths.Adds
sample.rswith matching CPU RNG plus (0,m,2)-net, table, GPU parity, and decorrelation tests. The loop guard skips provably finite literalforloops so Sobol’s own loops don’t burn the guard budget. Docs (RUNTIME.md,ROADMAP.md) document the scheme;volume/lamp-insidetightens its offline radiance expectation after better convergence.Reviewed by Cursor Bugbot for commit 6e670ad. Bugbot is set up for automated code reviews on this repo. Configure here.