feat(webgpu-rt): atomic pixel accumulator + raysPerPixel for >1 ray/pixel/bounce (#30)

wfAccum becomes array<atomic<u32>> (4 slots/pixel, f32 bit patterns — same
16 B/pixel footprint) and rtAccumulate CASes each channel, so N rays for one
pixel may resolve in the same SHADE pass without the read-modify-write
racing. GENERATE clears with atomicStore (bitcast(0.0) == 0u); RESOLVE
atomicLoads + bitcasts the vec4 it hands runResolve.

Capacity half: RTPass::raysPerPixel (default 1) scales the wavefront
ray/hit/payload buffers to raysPerPixel·W·H rays per bounce so the
per-light emits actually fit instead of being dropped by rtEmitRay's
capacity guard. The accumulator stays per-pixel.

No flag-gating: uncontended the CAS succeeds first try — RTStress SHADE
stays at its documented ~1.0 ms and master-vs-branch renders are
byte-identical, so single-ray consumers pay nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-09 22:45:25 +00:00
commit 27d7e84cb3
6 changed files with 89 additions and 19 deletions

View file

@ -21,12 +21,17 @@ compute pass, dispatch sizes driven by `dispatchWorkgroupsIndirect`.
- **RESOLVE** (1 thread/pixel, 8×8): reads accum slot, runs user `resolve_main`
if present else passthrough; writes outImage.
## Buffers (rtState, sized to 2*W*H rays)
## Buffers (rtState; per-bounce ray capacity = `RTPass::raysPerPixel`·W·H)
- `wfRaysA`,`wfRaysB`: array<WfRay>, ping/pong. WfRay = origin,tMin,dir,tMax,
pixel,flags,cullMask,missIndex,sbtOffset,payloadSlot,kind,_pad.
pixel,flags,cullMask,missIndex,sbtOffset,payloadSlot,kind,_pad. Each holds
one bounce's rays; `raysPerPixel` (default 1) scales them so closest-hit
can emit several rays per pixel in one bounce before rtEmitRay drops.
- `wfHits`: array<HitResult> (sized = ray capacity).
- `wfPayload`: array<Payload> — declared in CODEGEN region after user Payload.
- `wfAccum`: array<vec4<f32>> per pixel (W*H).
- `wfAccum`: array<atomic<u32>>, 4 slots per pixel (W*H, RGBA as f32 bit
patterns — 16 B/pixel). `rtAccumulate` CASes each channel, so a SHADE
invocation may emit several rays for the same pixel in one bounce (one
shadow ray per light, issue #30) without the accumulates racing.
- `wfCounters`: atomic counters: emitA, emitB, trace dispatch args, etc.
- `wfIndirect`: INDIRECT dispatch-args buffer.
@ -74,6 +79,15 @@ maxDepth=1 (primary only). Sponza maxDepth=2 (primary + shadow).
- [x] device limits (maxBufferSize / maxStorageBufferBindingSize /
maxComputeWorkgroupsPerDimension) + timestamp-query feature
- [x] megakernel dead path removed (RT pipeline builds only wavefront)
- [x] atomic pixel accumulator (#30) — `rtAccumulate` is a per-channel
f32 CAS over `array<atomic<u32>>`, lifting the old one-ray-per-pixel-
per-bounce cap so closest-hit can emit one shadow ray per light in a
single bounce (multi-light shadowing; 3DForts #153). Same 16 B/pixel
footprint; uncontended CAS succeeds first try, so single-ray scenes
(VulkanTriangle/Sponza/RTStress) are unaffected. Capacity side:
`RTPass::raysPerPixel` (default 1) scales the ray/hit/payload buffers
so those N rays/pixel actually fit a bounce instead of being dropped
by rtEmitRay's capacity guard. Exercised by `examples/RTMultiShadow`.
- [~] binding packing (Phase 7): SKIPPED — target device reports 64 storage
buffers/stage (≥12), so the merge is unnecessary (issue makes it
conditional on <12). NOTE: this only holds because dom-webgpu.js now