perf(tlas): dirty-track the per-frame TLAS instance+metadata upload (#118) #140
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Catcrafts/Crafter.Graphics!140
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "claude/issue-118"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
RenderingElement3D::BuildTLASrebuilt the host instance+metadata buffers with an O(n) copy of every 64 BVkAccelerationStructureInstanceKHR+ metadata entry every frame, unconditionally (outside thetopologyChangedblock), then flushed the whole high-water capacity (VK_WHOLE_SIZE) on both buffers. At the millions-of-instances target the copy dominates the CPU frame, and the whole-buffer flush costs on non-coherent BAR/VRAM. This covers both halves of the deliverable (rebuild + flush) — one dirty-set drives both.Fix
RenderingElement3D::hostDataVersion+MarkHostDataDirty()(bumps a global monotonic counter).TlasWithBuffer::uploadedVersionrecords, per frame, the version last copied into each slot. A slot is copied only when its element advanced past the recorded version. Version0("untracked") reads dirty every frame, so callers that don't opt in keep the exact pre-#118 copy-every-frame behaviour — fully backward compatible. Globally-unique versions make this correct under relocation (Remove's swap-and-pop, and a remove+add that nets the same count on the refit path) without tracking element identity.[first, last]envelope feeds a newVulkanBuffer::FlushDevice(cmd, access, stage, offset, bytes)overload (flush + barrier over just that span) forinstanceBuffer, and the existing rangedFlushDevice(offset, bytes)formetadataBuffer. When nothing is dirty, both — and theHOST->buildbarrier — are skipped. That barrier only ever ordered host writes (VK_PIPELINE_STAGE_HOST_BIT), never the application's compute-written GPU-owned transform, so skipping it changes no GPU-ordering guarantee.Constraint honoured
transformOwnedByGputransforms are still never host-copied. The new field/method are mirrored on the WebGPU class for source portability (the WebGPU build re-uploads its small mirror wholesale and ignores the version).Tests
New
TLASInstanceDirtyTrackingdrives the real RT device (RTX 4090, full validation layers) and reads back the host-mapped buffers to assert:MarkHostDataDirtydoes not upload (clean slot skipped);Full suite: 21 passed (
crafter-build test). Library also builds clean forwasm32-wasip1(DOM/WebGPU).Resolves #118
BuildTLAS rebuilt the host instance+metadata buffers with an O(n) copy of every 64 B VkAccelerationStructureInstanceKHR + metadata entry every frame, unconditionally, then flushed the whole high-water capacity (VK_WHOLE_SIZE) on both buffers. At the millions-of-instances target that copy dominates the CPU frame, and the whole-buffer flush costs on non-coherent BAR/VRAM. Add a generation counter so only changed host-authored fields are copied, and feed the same dirty span into the ranged FlushDevice(offset, bytes) overload: - RenderingElement3D::hostDataVersion + MarkHostDataDirty() (bumps a global monotonic counter). TlasWithBuffer::uploadedVersion records, per frame, the version last copied into each slot. A slot is copied only when its element advanced past the recorded version; version 0 ("untracked") reads dirty every frame, so callers that don't opt in keep the prior copy-every-frame behaviour. Globally-unique versions make this correct under relocation (Remove's swap-and-pop, and remove+add that nets the same count on the refit path) without tracking element identity. The reset on every topology change covers buffer reallocation and the reshuffled element->slot mapping. - The dirty [first, last] envelope drives both the copy and the flush: a new VulkanBuffer::FlushDevice(cmd, access, stage, offset, bytes) overload flushes + barriers just that span for instanceBuffer, and the ranged FlushDevice(offset, bytes) for metadataBuffer. When nothing is dirty both are skipped — the skipped HOST->build barrier only ever ordered host writes, never the application's compute-written GPU-owned transform (that compute->build ordering is the caller's, and is unchanged). Constraint honoured: transformOwnedByGpu transforms are still never host-copied. The API field/method are mirrored on the WebGPU class for source portability (the WebGPU build re-uploads its small mirror wholesale and ignores the version). New test TLASInstanceDirtyTracking drives the real RT device and reads back the host-mapped buffers to assert: tracked elements upload once then skip until re-marked, untracked elements always upload, and relocation on the refit path re-uploads exactly the moved slots — with zero validation-layer errors over the ranged flush. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>