perf(rt): batch the WebGPU TLAS instance upload into one writeBuffer (#131) #150

Merged
catbot merged 1 commit from claude/issue-131 into master 2026-06-18 16:00:11 +02:00
Member

Problem

In BuildTLASUpload (implementations/Crafter.Graphics-RenderingElement3D-WebGPU.cpp), GPU-driven (transformOwnedByGpu) runs uploaded one element at a time — a separate FlushDeviceRange of the 16 strided metadata bytes per element — each paying per-call WebGPU validation / encode / JS-boundary cost. The CPU-driven arm already batched contiguously.

Found by an adversarially-verified performance audit. Currently latent: transformOwnedByGpu is set nowhere in-repo.

Fix

Upload the whole active instance range in a single writeBuffer. Pushing the (stale) transform bytes for GPU-driven slots is harmless: the only supported way to drive a transform from the GPU is the manual Upload -> physics compute pass -> Build sequence (see the file header), and that compute pass runs after the upload and rewrites the transform on the GPU before the TLAS build reads it. Nothing reads instanceBuffer between Upload and that compute pass.

For the in-repo (all-CPU) usage the GPU bytes uploaded are byte-for-byte identical to before: with a single CPU-driven run the old loop already emitted exactly this one FlushDeviceRange(0, 0, primitiveCount*64). The only behavioral change is that the latent GPU-driven path now batches into that same single write.

Testing

  • crafter-build test — 24 passed.
  • wasm build compiles cleanly.
  • Exercised the WebGPU path directly: the RTStress example (8³ = 512 ray-traced instances) renders correctly, confirming both transforms and per-instance metadata upload correctly through the new single-write path.

Screenshots

RTStress 512-instance ray-traced grid

Resolves #131

🤖 Generated with Claude Code

## Problem In `BuildTLASUpload` (`implementations/Crafter.Graphics-RenderingElement3D-WebGPU.cpp`), GPU-driven (`transformOwnedByGpu`) runs uploaded one element at a time — a separate `FlushDeviceRange` of the 16 strided metadata bytes per element — each paying per-call WebGPU validation / encode / JS-boundary cost. The CPU-driven arm already batched contiguously. Found by an adversarially-verified performance audit. Currently latent: `transformOwnedByGpu` is set nowhere in-repo. ## Fix Upload the whole active instance range in a single `writeBuffer`. Pushing the (stale) transform bytes for GPU-driven slots is harmless: the only supported way to drive a transform from the GPU is the manual `Upload -> physics compute pass -> Build` sequence (see the file header), and that compute pass runs after the upload and rewrites the transform on the GPU before the TLAS build reads it. Nothing reads `instanceBuffer` between Upload and that compute pass. For the in-repo (all-CPU) usage the GPU bytes uploaded are **byte-for-byte identical** to before: with a single CPU-driven run the old loop already emitted exactly this one `FlushDeviceRange(0, 0, primitiveCount*64)`. The only behavioral change is that the latent GPU-driven path now batches into that same single write. ## Testing - `crafter-build test` — 24 passed. - wasm build compiles cleanly. - Exercised the WebGPU path directly: the RTStress example (8³ = 512 ray-traced instances) renders correctly, confirming both transforms and per-instance metadata upload correctly through the new single-write path. ## Screenshots ![RTStress 512-instance ray-traced grid](https://forgejo.catcrafts.net/attachments/40322359-8e30-435c-8b20-dc14eeeb723f) Resolves #131 🤖 Generated with [Claude Code](https://claude.com/claude-code)
BuildTLASUpload pushed GPU-driven (transformOwnedByGpu) runs one element
at a time — a separate FlushDeviceRange of the 16 strided metadata bytes
per element — each paying WebGPU validation / encode / JS-boundary cost,
while the CPU-driven arm already batched contiguously.

Upload the whole active instance range in a single writeBuffer instead.
Pushing the (stale) transform bytes for GPU-driven slots is harmless: the
only supported way to drive a transform from the GPU is the manual
Upload -> physics compute pass -> Build sequence, and that compute pass
runs after this upload and rewrites the transform on the GPU before the
TLAS build reads it. For the in-repo (all-CPU) usage the bytes uploaded
are identical to before — with one CPU-driven run the old loop already
emitted exactly this single FlushDeviceRange(0, 0, primitiveCount*64).

Verified: native suite (24 passed) and the RTStress wasm example (512
ray-traced instances) render correctly through the new path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit cd3a55a914 into master 2026-06-18 16:00:11 +02:00
catbot deleted branch claude/issue-131 2026-06-18 16:00:11 +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!150
No description provided.