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

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>
This commit is contained in:
catbot 2026-06-17 17:40:39 +00:00
commit 8a32f0d545
3 changed files with 33 additions and 9 deletions

View file

@ -261,6 +261,16 @@ void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd, RTBuildO
VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR, VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR); VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR);
// The compressed staging is only read by the decompress recorded above; the
// subsequent BLAS build reads the decompressed vertex/index buffers, never
// this. So hand it to the fence-keyed deletion queue (#101/#102) now rather
// than pinning host-visible memory for the mesh's whole life. The recorded
// vkCmdDecompressMemoryEXT still references compressedStaging.address, so it
// must outlive this submit — which the queue guarantees: it retires the
// allocation only after framesInFlight frames have elapsed, by which point
// the decompress submit's fence has cleared.
compressedStaging.DeferredClear();
allowUpdate = options.allowUpdate; allowUpdate = options.allowUpdate;
builtInputCount = asset.vertexCount; builtInputCount = asset.vertexCount;
RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, BlasFlags(options), /*update*/ false, cmd); RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, BlasFlags(options), /*update*/ false, cmd);

View file

@ -75,9 +75,12 @@ export namespace Crafter {
VkImage image; VkImage image;
VkDeviceMemory imageMemory; VkDeviceMemory imageMemory;
VulkanBuffer<PixelType, true> buffer; VulkanBuffer<PixelType, true> buffer;
// Lives until the compressed Update path's cmd buffer completes. // Transient host-visible staging for the compressed Update path. Same
// Same lifetime contract as Mesh::compressedStaging — caller must // lifetime contract as Mesh::compressedStaging: the compressed Update
// not destroy / re-Update before the submit fence is signaled. // releases it via DeferredClear() right after recording the decompress,
// so the fence-keyed deletion queue (#101/#102) frees it once that
// submit's frame has cleared instead of pinning it for the image's life.
// Between Updates the handle is null; the next Update's Resize re-creates it.
VulkanBuffer<std::byte, true> compressedStaging; VulkanBuffer<std::byte, true> compressedStaging;
VkImageView imageView; VkImageView imageView;
VkDescriptorImageInfo descriptor; VkDescriptorImageInfo descriptor;
@ -299,6 +302,16 @@ export namespace Crafter {
VK_PIPELINE_STAGE_2_COPY_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT); VK_ACCESS_2_TRANSFER_READ_BIT);
// Compressed staging is read only by the decompress recorded above;
// the buffer→image copy below reads `buffer` (the decompress dst),
// never this. Release it to the fence-keyed deletion queue
// (#101/#102) now instead of pinning host-visible memory for the
// image's whole life. The recorded vkCmdDecompressMemoryEXT still
// references compressedStaging.address, so it must outlive this
// submit — which the queue guarantees by retiring the allocation
// only after framesInFlight frames (i.e. after the submit's fence).
compressedStaging.DeferredClear();
// Continue with the existing buffer→image upload + layout transitions. // Continue with the existing buffer→image upload + layout transitions.
// We've already inserted the decompress→transfer-read barrier, // We've already inserted the decompress→transfer-read barrier,
// so we skip the FlushDevice host-write barrier the regular Update // so we skip the FlushDevice host-write barrier the regular Update

View file

@ -77,12 +77,13 @@ export namespace Crafter {
// Lifetime contract matches vertexBuffer/indexBuffer: must stay // Lifetime contract matches vertexBuffer/indexBuffer: must stay
// alive until the build submitted on `cmd` completes. // alive until the build submitted on `cmd` completes.
VulkanBuffer<RTAabb, true> aabbBuffer; VulkanBuffer<RTAabb, true> aabbBuffer;
// Lives until the cmd buffer issued by the compressed Build path // Transient host-visible staging for the compressed Build path. Kept as
// completes execution. Kept as a member so the recorded // a member only so the recorded vkCmdDecompressMemoryEXT has a stable
// vkCmdDecompressMemoryEXT references valid memory until the queue // address to reference; the compressed Build releases it via
// submit signals — caller must not re-Build or destroy the Mesh // DeferredClear() right after recording the decompress, so it is freed
// before that submit's fence is signaled (same contract as the // by the fence-keyed deletion queue (#101/#102) once that submit's frame
// existing uncompressed path). // has cleared — no longer pinned for the mesh's life. Between Builds the
// handle is null; the next Build's Resize re-creates it.
VulkanBuffer<std::byte, true> compressedStaging; VulkanBuffer<std::byte, true> compressedStaging;
VkAccelerationStructureGeometryTrianglesDataKHR blasData; VkAccelerationStructureGeometryTrianglesDataKHR blasData;
VkAccelerationStructureGeometryKHR blas; VkAccelerationStructureGeometryKHR blas;