perf(mesh): release per-mesh scratch buffer for static BLAS (#66)

A static (!allowUpdate) mesh can never refit, so its per-mesh DEVICE_LOCAL
scratch buffer is dead weight the moment the build's GPU work completes —
yet it was kept alive for the mesh's whole lifetime. In many-mesh scenes
(where static meshes dominate) this is real, accumulating VRAM waste.

Release it via scratchBuffer.DeferredClear() right after recording a fresh
build for a static mesh. The old "free-after-build is a UAF, the GPU is
still building" hazard is gone: the fence-keyed deferred-deletion queue
(#101/#102) retires the allocation only once the build's frame has passed
its fence. A later Refit on a static mesh takes the rebuild fallback, whose
Resize sees the nulled handle and re-Creates the scratch. Refit-capable
(allowUpdate) meshes keep their scratch as before — an in-place UPDATE
reuses it each frame.

Extends the BLASBuildOptions hardware test to assert the scratch is retained
for allowUpdate meshes and released for static ones (including across a
static-mesh refit rebuild), with the validation layer confirming zero errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-17 17:36:10 +00:00
commit 8b008b49d5
2 changed files with 33 additions and 0 deletions

View file

@ -152,6 +152,23 @@ namespace {
self.buildFlags = flags;
self.builtPrimitiveCount = primitiveCount;
// Static (!allowUpdate) meshes can never refit, so their per-mesh
// scratch is dead weight the moment this build's GPU work completes —
// release it instead of keeping a persistent DEVICE_LOCAL allocation
// per mesh (the waste that dominates many-mesh scenes, issue #66). The
// fresh build recorded above still reads scratchBuffer.address from
// this command buffer, so a plain Clear() would be a use-after-free;
// DeferredClear() retires the allocation only once the build's frame
// has passed its fence (the queue from #101/#102). A later Refit on a
// static mesh takes the rebuild fallback, whose Resize sees the nulled
// handle and re-Creates the scratch. Refit-capable meshes keep theirs:
// an in-place UPDATE reuses it every frame (build scratch ≥ update
// scratch, so no resize). Only applies to a fresh build — an UPDATE
// never reaches here for a static mesh.
if (!update && !self.allowUpdate) {
self.scratchBuffer.DeferredClear();
}
}
void RecordBLASBuild(Mesh& self, std::uint32_t vertexCount, std::uint32_t indexCount, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) {