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

@ -103,6 +103,13 @@ export namespace Crafter {
// its own composite→swapchain pass afterwards. Ignored (0) for the
// default canvas path.
std::uint32_t outTexHandle = 0;
// Per-bounce ray budget as a multiple of the pixel count. The
// wavefront ray/hit/payload buffers hold raysPerPixel·W·H rays, so
// a closest-hit may emit up to raysPerPixel rays per pixel within
// one bounce (e.g. one shadow ray per light — rtAccumulate is
// atomic, issue #30) before rtEmitRay starts dropping. Memory
// scales linearly; keep at 1 for single-ray-per-pixel pipelines.
std::uint32_t raysPerPixel = 1;
RTPass(PipelineRTWebGPU* p) : pipeline(p) {}
@ -121,7 +128,8 @@ export namespace Crafter {
handlesPtr,
static_cast<std::int32_t>(handlesCount),
static_cast<std::int32_t>(maxDepth),
outTexHandle);
outTexHandle,
static_cast<std::int32_t>(raysPerPixel));
}
};
}

View file

@ -222,6 +222,11 @@ namespace Crafter::WebGPU {
// `outTexHandle` is the destination texture for an HDR-output pipeline
// (one loaded with hdrOutputFormat != 0); ignored (pass 0) for the
// default canvas path.
// `raysPerPixel` scales the wavefront ray/hit/payload buffers to
// raysPerPixel·W·H rays per bounce, so closest-hit can emit up to that
// many rays per pixel within one bounce (e.g. one shadow ray per light)
// without rtEmitRay dropping past capacity. 1 = the historical
// single-ray-per-pixel footprint.
__attribute__((import_module("env"), import_name("wgpuDispatchRT")))
extern "C" void wgpuDispatchRT(std::uint32_t pipelineHandle,
const void* pushPtr, std::int32_t pushBytes,
@ -230,7 +235,8 @@ namespace Crafter::WebGPU {
std::int32_t gx, std::int32_t gy,
const void* handlesPtr, std::int32_t handlesCount,
std::int32_t maxDepth,
std::uint32_t outTexHandle);
std::uint32_t outTexHandle,
std::int32_t raysPerPixel);
// GPU TLAS-build dispatch. Two sequential compute passes:
// 1. tlasBuildMain — per-instance world AABB + identity permutation