perf(image): release static texture staging after upload (#114)

ImageVulkan kept its host-visible staging `buffer` (sized w*h, persistently
mapped) alive for the whole life of the image and Destroy() never freed it, so
static textures (e.g. Sponza albedo) pinned HOST_VISIBLE / small-BAR memory
forever.

Update now releases the staging via VulkanBuffer::DeferredClear() right after
recording the buffer→image copy — the same fence-keyed deletion queue
(#101/#102) the compressed Mesh path already uses — so it is freed once the
upload submit's frame has cleared. A new `streamed` flag (set by FontAtlas)
keeps the persistent map for images re-uploaded from a CPU-side staging buffer
every frame; its uploads go through UpdateRegion, which never releases. Destroy
now also frees any staging the image still owns, gated on a live handle so a
released static texture can't double-free.

Adds tests/ImageStagingRelease driving the real upload + readback on a headless
device: asserts the staging is enqueued (not pinned), the image reads back
byte-equal (released staging outlived the submit), the entry retires only after
framesInFlight frames, and a streamed image keeps then frees its staging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-17 19:40:35 +00:00
commit b4fa6e596e
4 changed files with 314 additions and 3 deletions

View file

@ -39,7 +39,11 @@ void FontAtlas::Initialize(GraphicsCommandBuffer cmd) {
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
// The atlas is sampled by the UI text *compute* shader, not an RT
// pipeline — scope the upload barriers to the real consumer.
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
// Streamed: glyphs are rasterized into buffer.value on the CPU and
// re-uploaded every frame via UpdateRegion, so the persistent staging
// map must outlive the first upload rather than being released (#114).
/*streamed*/ true
);
std::memset(image.buffer.value, 0, kAtlasSize * kAtlasSize);
#else