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

Merged
catbot merged 1 commit from claude/issue-66 into master 2026-06-17 19:36:44 +02:00
Member

Problem

Each Mesh keeps a dedicated DEVICE_LOCAL scratch buffer for its BLAS build, persistent for the mesh's whole lifetime. For static (allowUpdate == false) meshes — the ones that proliferate in many-mesh scenes — the scratch is dead weight the moment the build's GPU work completes: it can never be reused by a refit. That's real, accumulating VRAM waste (not hot-path, no per-frame scratch alloc).

Fix

Key the release on allowUpdate (already recorded at Mesh.cpp:196): after recording a fresh build for a static mesh, call scratchBuffer.DeferredClear().

The old hazard — "free after build is a use-after-free because the GPU is still building" — is solved by the fence-keyed deferred-deletion queue (#101/#102): DeferredClear() nulls the handle immediately but retires the allocation only once the build's frame has passed its fence. The build command recorded just above still reads scratchBuffer.address safely.

  • Static meshes (allowUpdate == false): scratch released after the build. A later Refit takes the rebuild fallback, whose Resize sees the nulled handle and re-Creates a fresh scratch.
  • Refit-capable meshes (allowUpdate == true): scratch retained exactly as today — an in-place UPDATE reuses it each frame (build scratch >= update scratch, so no resize).
  • An UPDATE build never reaches the release path (guarded by !update).

Testing

Extended the BLASBuildOptions hardware test (real headless Vulkan device, RTX 4090) with assertions that allowUpdate meshes retain their scratch, a static mesh releases it after the build, and a static-mesh refit rebuild re-creates then re-releases it.

crafter-build test -> 19 passed; BLASBuildOptions reports all 24 checks PASS with 0 Vulkan validation errors (the strongest proof there's no UAF from the early release).

Resolves #66

## Problem Each `Mesh` keeps a dedicated DEVICE_LOCAL scratch buffer for its BLAS build, persistent for the mesh's whole lifetime. For **static** (`allowUpdate == false`) meshes — the ones that proliferate in many-mesh scenes — the scratch is dead weight the moment the build's GPU work completes: it can never be reused by a refit. That's real, accumulating VRAM waste (not hot-path, no per-frame scratch alloc). ## Fix Key the release on `allowUpdate` (already recorded at `Mesh.cpp:196`): after recording a **fresh** build for a static mesh, call `scratchBuffer.DeferredClear()`. The old hazard — "free after build is a use-after-free because the GPU is still building" — is solved by the fence-keyed deferred-deletion queue (#101/#102): `DeferredClear()` nulls the handle immediately but retires the allocation only once the build's frame has passed its fence. The build command recorded just above still reads `scratchBuffer.address` safely. - **Static meshes** (`allowUpdate == false`): scratch released after the build. A later `Refit` takes the rebuild fallback, whose `Resize` sees the nulled handle and re-`Create`s a fresh scratch. - **Refit-capable meshes** (`allowUpdate == true`): scratch retained exactly as today — an in-place UPDATE reuses it each frame (build scratch >= update scratch, so no resize). - An UPDATE build never reaches the release path (guarded by `!update`). ## Testing Extended the `BLASBuildOptions` hardware test (real headless Vulkan device, RTX 4090) with assertions that allowUpdate meshes retain their scratch, a static mesh releases it after the build, and a static-mesh refit rebuild re-creates then re-releases it. `crafter-build test` -> **19 passed**; BLASBuildOptions reports all 24 checks PASS with **0 Vulkan validation errors** (the strongest proof there's no UAF from the early release). Resolves #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>
catbot merged commit 39b882a9cb into master 2026-06-17 19:36:44 +02:00
catbot deleted branch claude/issue-66 2026-06-17 19:36:44 +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!104
No description provided.