perf(decompress): release compressed staging after submit, not for the resource's life (#67) #106

Merged
catbot merged 2 commits from claude/issue-67 into master 2026-06-17 19:41:20 +02:00
Member

Problem

compressedStaging — the host-visible buffer holding the GDeflate streams for the GPU decompress path — was kept alive per compressed Build/Update for the whole life of the mesh/image. The single vkCmdDecompressMemoryEXT that reads it retires almost immediately, but the staging stayed mapped and pinned host-visible memory forever afterward.

  • implementations/Crafter.Graphics-Mesh.cpp (compressed Mesh::Build)
  • interfaces/Crafter.Graphics-ImageVulkan.cppm (compressed ImageVulkan::Update)

Fix

Release the staging via VulkanBuffer::DeferredClear() immediately after recording the decompress (option (a) from the issue). The recorded vkCmdDecompressMemoryEXT still references compressedStaging.address, so the buffer must outlive the submit — which is exactly the guarantee the fence-keyed deferred-deletion queue from #101/#102 provides: it retires the allocation only once framesInFlight frames have elapsed, by which point the submit's fence has cleared. The subsequent BLAS build / buffer→image copy read the decompressed outputs, never the staging, so this is safe.

Between Builds/Updates the handle is null; the next call's Resize re-creates it.

Testing

New MeshDecompressStagingRelease test drives the real VK_EXT_memory_decompression / GDeflate path on a headless device and asserts:

  • After Build the mesh owns no staging handle (released).
  • Exactly that one allocation is handed to the deletion queue, tagged with the enqueue frame.
  • The decompress + BLAS build complete with zero validation errors and a non-zero BLAS address (proof the staging outlived the submit — no use-after-free).
  • The GPU-decompressed vertex/index data reads back byte-equal to the source.
  • The deferred entry retires (and frees) only once framesInFlight frames elapse, not before.

Falls back to a graceful skip on hardware without the extension (the CPU-decode path never allocates the staging).

crafter-build test --jobs=1
20 passed

(--jobs=1 serializes the run to avoid a pre-existing parallel git pull race on the shared Vulkan-Headers external-dep cache — unrelated to this change.)

Builds on #101 / #102.

Resolves #67

🤖 Generated with Claude Code

## Problem `compressedStaging` — the host-visible buffer holding the GDeflate streams for the GPU decompress path — was kept alive per compressed Build/Update for the **whole life** of the mesh/image. The single `vkCmdDecompressMemoryEXT` that reads it retires almost immediately, but the staging stayed mapped and pinned host-visible memory forever afterward. - `implementations/Crafter.Graphics-Mesh.cpp` (compressed `Mesh::Build`) - `interfaces/Crafter.Graphics-ImageVulkan.cppm` (compressed `ImageVulkan::Update`) ## Fix Release the staging via `VulkanBuffer::DeferredClear()` immediately after recording the decompress (option (a) from the issue). The recorded `vkCmdDecompressMemoryEXT` still references `compressedStaging.address`, so the buffer **must** outlive the submit — which is exactly the guarantee the fence-keyed deferred-deletion queue from #101/#102 provides: it retires the allocation only once `framesInFlight` frames have elapsed, by which point the submit's fence has cleared. The subsequent BLAS build / buffer→image copy read the *decompressed* outputs, never the staging, so this is safe. Between Builds/Updates the handle is null; the next call's `Resize` re-creates it. ## Testing New `MeshDecompressStagingRelease` test drives the real `VK_EXT_memory_decompression` / GDeflate path on a headless device and asserts: - After Build the mesh owns no staging handle (released). - Exactly that one allocation is handed to the deletion queue, tagged with the enqueue frame. - The decompress + BLAS build complete with **zero validation errors** and a non-zero BLAS address (proof the staging outlived the submit — no use-after-free). - The GPU-decompressed vertex/index data reads back byte-equal to the source. - The deferred entry retires (and frees) only once `framesInFlight` frames elapse, not before. Falls back to a graceful skip on hardware without the extension (the CPU-decode path never allocates the staging). ``` crafter-build test --jobs=1 20 passed ``` (`--jobs=1` serializes the run to avoid a pre-existing parallel `git pull` race on the shared Vulkan-Headers external-dep cache — unrelated to this change.) Builds on #101 / #102. Resolves #67 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The compressed Mesh::Build / ImageVulkan::Update paths kept their host-visible
`compressedStaging` (holding the GDeflate streams) alive for the whole life of
the mesh/image, pinning host-visible memory long after the single GPU decompress
that reads it has retired.

Release it via VulkanBuffer::DeferredClear() right after recording the decompress.
The recorded vkCmdDecompressMemoryEXT still references compressedStaging.address,
so it must outlive the submit — exactly what the fence-keyed deletion queue
(#101/#102) guarantees: it retires the allocation only after framesInFlight frames
have elapsed, by which point the submit's fence has cleared. Between Builds/Updates
the handle is null; the next call's Resize re-creates it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MeshDecompressStagingRelease drives the real VK_EXT_memory_decompression /
GDeflate path on a headless device and asserts the staging is handed to the
deferred-deletion queue (not pinned), the build is validation-clean with
byte-correct decompressed data, and the entry retires only after framesInFlight
frames. Skips the GPU-path assertions when the extension is absent (CPU fallback
never allocates the staging).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit 2c8a293b57 into master 2026-06-17 19:41:20 +02:00
catbot deleted branch claude/issue-67 2026-06-17 19:41:20 +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!106
No description provided.