feat(webgpu-rt): atomic pixel accumulator + raysPerPixel — >1 ray per pixel per bounce (#30) #31

Merged
catbot merged 2 commits from claude/issue-30 into master 2026-06-10 00:46:24 +02:00
Member

Resolves #30.

What

Lifts the wavefront tracer's one-ray-per-pixel-per-bounce cap, both halves of it:

  • Atomic accumulatewfAccum is now array<atomic<u32>> (4 slots/pixel, RGBA as f32 bit patterns; same 16 B/pixel footprint). rtAccumulate does a per-channel f32 add via an atomicCompareExchangeWeak loop, exactly as sketched in the issue. GENERATE clears with atomicStore(…, 0u) (bitcast<u32>(0.0) == 0u), RESOLVE atomicLoads + bitcasts the vec4<f32> it hands runResolve.
  • Capacity — the accumulator alone wasn't enough: the ray/hit/payload buffers hold W·H rays per bounce and rtEmitRay silently drops past rayCapacity, so N shadow rays/pixel would mostly be discarded before they could ever race. New RTPass::raysPerPixel (default 1) scales those buffers to raysPerPixel·W·H rays per bounce. The accumulator stays per-pixel.

Flag-gating question from the issue: not needed

Measured rather than guessed: RTStress (≤1 ray/pixel/bounce) SHADE is ~0.99 ms on this branch vs the ~1.0 ms documented baseline, and a master-vs-branch screenshot of RTStress is byte-identical (magick compare -metric AE = 0). Uncontended, the weak CAS succeeds first try; a plain-add fallback would buy nothing, so no flag.

Test: examples/RTMultiShadow

Five pillars, four colored point lights; the closest-hit emits one shadow ray per light from the same invocation (raysPerPixel = 4, maxDepth = 2), so up to four rays per pixel rtAccumulate in one SHADE pass — the 3DForts #153 shape. Each pillar casts four separable colored shadows; a race or a capacity drop shows as flickering dark noise / a missing shadow color. Two frames captured a second apart differ by 2 px out of 1.85 M (last-ulp CAS-ordering rounding), i.e. no lost updates.

crafter-build test (native): 1 passed.

Screenshots

RTMultiShadow — four colored lights, one shadow ray each per pixel

Resolves #30. ## What Lifts the wavefront tracer's one-ray-per-pixel-per-bounce cap, both halves of it: - **Atomic accumulate** — `wfAccum` is now `array<atomic<u32>>` (4 slots/pixel, RGBA as f32 bit patterns; same 16 B/pixel footprint). `rtAccumulate` does a per-channel f32 add via an `atomicCompareExchangeWeak` loop, exactly as sketched in the issue. GENERATE clears with `atomicStore(…, 0u)` (`bitcast<u32>(0.0) == 0u`), RESOLVE `atomicLoad`s + bitcasts the `vec4<f32>` it hands `runResolve`. - **Capacity** — the accumulator alone wasn't enough: the ray/hit/payload buffers hold W·H rays per bounce and `rtEmitRay` silently drops past `rayCapacity`, so N shadow rays/pixel would mostly be discarded before they could ever race. New `RTPass::raysPerPixel` (default 1) scales those buffers to raysPerPixel·W·H rays per bounce. The accumulator stays per-pixel. ## Flag-gating question from the issue: not needed Measured rather than guessed: RTStress (≤1 ray/pixel/bounce) SHADE is **~0.99 ms** on this branch vs the **~1.0 ms** documented baseline, and a master-vs-branch screenshot of RTStress is **byte-identical** (`magick compare -metric AE` = 0). Uncontended, the weak CAS succeeds first try; a plain-add fallback would buy nothing, so no flag. ## Test: examples/RTMultiShadow Five pillars, four colored point lights; the closest-hit emits one shadow ray **per light** from the same invocation (`raysPerPixel = 4`, `maxDepth = 2`), so up to four rays per pixel `rtAccumulate` in one SHADE pass — the 3DForts #153 shape. Each pillar casts four separable colored shadows; a race or a capacity drop shows as flickering dark noise / a missing shadow color. Two frames captured a second apart differ by 2 px out of 1.85 M (last-ulp CAS-ordering rounding), i.e. no lost updates. `crafter-build test` (native): 1 passed. ## Screenshots ![RTMultiShadow — four colored lights, one shadow ray each per pixel](https://forgejo.catcrafts.net/attachments/c8388816-f3ed-4aea-b3f2-439ed65c875f)
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>
Five pillars, four colored point lights; closest-hit emits one shadow ray
per light from the same invocation (raysPerPixel = 4, maxDepth = 2), so up
to four rays per pixel rtAccumulate in a single SHADE pass — the contention
case #30 exists for. Each pillar casts four separable colored shadows; an
accumulator race or capacity drop shows as flickering dark noise or a
missing shadow color. Two frames a second apart diff at 2 px / 1.85 M
(last-ulp CAS ordering), confirming no lost updates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit c608d75a36 into master 2026-06-10 00:46:24 +02:00
catbot deleted branch claude/issue-30 2026-06-10 00:46:24 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Catcrafts/Crafter.Graphics!31
No description provided.