Crafter.Graphics/interfaces/Crafter.Graphics-ImageVulkan.cppm
catbot b4fa6e596e 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>
2026-06-17 19:40:35 +00:00

432 lines
24 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0215-1301 USA
*/
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
export module Crafter.Graphics:ImageVulkan;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import Crafter.Asset;
import :Decompress;
import :VulkanBuffer;
export namespace Crafter {
// Builds the batched final layout transition for a freshly-generated mip
// chain (mipLevels >= 2). After the blit loop, levels [0, mipLevels-1) sit
// in TRANSFER_SRC_OPTIMAL — each was read as a blit source — while the last
// level is still in TRANSFER_DST_OPTIMAL: it was written by the final blit
// and is never read, so it never needed a DST->SRC barrier of its own.
// Both groups move to the consumer `layout` in one vkCmdPipelineBarrier
// (two VkImageMemoryBarrier entries sharing src=TRANSFER / dst=consumer
// stage), shaving one barrier call off the previous N+1-per-chain count.
// The interleaved per-level barriers stay one-at-a-time — each is mandated
// because the next blit reads the level it transitions. Writes the entries
// into `out` and returns the count (always 2).
std::uint32_t BuildMipChainFinalBarriers(VkImage image, VkImageLayout layout, std::uint32_t mipLevels, std::array<VkImageMemoryBarrier, 2>& out) {
auto fill = [&](VkImageMemoryBarrier& b, VkImageLayout oldLayout, std::uint32_t baseMip, std::uint32_t count, VkAccessFlags srcAccess) {
b = {};
b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
b.oldLayout = oldLayout;
b.newLayout = layout;
b.image = image;
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
b.subresourceRange.baseMipLevel = baseMip;
b.subresourceRange.levelCount = count;
b.subresourceRange.baseArrayLayer = 0;
b.subresourceRange.layerCount = 1;
b.srcAccessMask = srcAccess;
b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
};
// Levels [0, mipLevels-1): blit sources, currently TRANSFER_SRC.
fill(out[0], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 0, mipLevels - 1u, VK_ACCESS_TRANSFER_READ_BIT);
// Final level: the last blit's destination, still TRANSFER_DST.
fill(out[1], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels - 1u, 1u, VK_ACCESS_TRANSFER_WRITE_BIT);
return 2;
}
template <typename PixelType>
class ImageVulkan {
public:
std::uint16_t width;
std::uint16_t height;
std::uint8_t mipLevels;
VkImage image;
VkDeviceMemory imageMemory;
VulkanBuffer<PixelType, true> buffer;
// Transient host-visible staging for the compressed Update path. Same
// lifetime contract as Mesh::compressedStaging: the compressed Update
// 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;
VkImageView imageView;
VkDescriptorImageInfo descriptor;
// Pipeline stage that samples this image after upload. Set once at
// Create and reused by every Update so the upload barrier's dst (and
// the next-upload entry barrier's src) scope actually covers the real
// consumer. Defaults to RT — the font atlas overrides it to COMPUTE,
// since UI text is rendered by a compute shader, not an RT pipeline.
VkPipelineStageFlags consumerStage = VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR;
// Whether this image is continuously re-uploaded from a persistent
// CPU-side staging map. False (the default) marks a static texture
// uploaded once — e.g. Sponza albedo: the first Update's buffer→image
// copy is the only reader of the host-visible staging `buffer`, so it
// is released to the fence-keyed deletion queue (#101/#102) right after
// the copy instead of pinning HOST_VISIBLE / small-BAR memory for the
// image's whole life (issue #114). True keeps `buffer` alive: a streamed
// image (the FontAtlas) re-fills buffer.value on the CPU and re-uploads
// every frame, so the persistent map must survive — its uploads go
// through UpdateRegion, which never releases the staging.
bool streamed = false;
void Create(std::uint16_t width, std::uint16_t height, std::uint8_t mipLevels, VkCommandBuffer cmd, VkFormat format, VkImageCreateFlags flags, VkImageLayout layout, VkPipelineStageFlags consumerStage = VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, bool streamed = false) {
this->width = width;
this->height = height;
this->mipLevels = mipLevels;
this->consumerStage = consumerStage;
this->streamed = streamed;
buffer.Create(
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
width * height
);
VkImageCreateInfo imageInfo = {};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = width;
imageInfo.extent.height = height;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = mipLevels;
imageInfo.arrayLayers = 1;
imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = flags;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
Device::CheckVkResult(vkCreateImage(Device::device, &imageInfo, nullptr, &image));
VkMemoryRequirements memRequirements;
vkGetImageMemoryRequirements(Device::device, image, &memRequirements);
VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = Device::GetMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Device::CheckVkResult(vkAllocateMemory(Device::device, &allocInfo, nullptr, &imageMemory));
vkBindImageMemory(Device::device, image, imageMemory, 0);
VkImageViewCreateInfo viewInfo = {};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = format;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = mipLevels;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
Device::CheckVkResult(vkCreateImageView(Device::device, &viewInfo, nullptr, &imageView));
// Final transition to shader read-only layout
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_UNDEFINED, layout, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, consumerStage, 0, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels);
descriptor = { .imageView = imageView, .imageLayout = layout };
}
void Update(VkCommandBuffer cmd, VkImageLayout layout) {
buffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
TransitionImageLayout(cmd, image, layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, consumerStage, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, 0, mipLevels);
VkBufferImageCopy region{};
region.bufferOffset = 0;
region.bufferRowLength = 0;
region.bufferImageHeight = 0;
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;
region.imageOffset = {0, 0, 0};
region.imageExtent = { width, height, 1};
vkCmdCopyBufferToImage(
cmd,
buffer.buffer,
image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1,
&region
);
if(mipLevels > 1) {
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, 0, 1);
for (std::uint16_t i = 1; i < mipLevels; ++i) {
std::uint16_t mipWidth = width >> i;
std::uint16_t mipHeight = height >> i;
std::uint16_t previousMipWidth = width >> (i - std::uint16_t(1));
std::uint16_t previousMipHeight = height >> (i - std::uint16_t(1));
VkImageBlit blit = {};
blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blit.srcSubresource.mipLevel = i - 1;
blit.srcSubresource.baseArrayLayer = 0;
blit.srcSubresource.layerCount = 1;
blit.srcOffsets[0] = { 0, 0, 0 };
blit.srcOffsets[1] = { (int32_t)previousMipWidth, (int32_t)previousMipHeight, 1 };
blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blit.dstSubresource.mipLevel = i;
blit.dstSubresource.baseArrayLayer = 0;
blit.dstSubresource.layerCount = 1;
blit.dstOffsets[0] = { 0, 0, 0 };
blit.dstOffsets[1] = { (int32_t)mipWidth, (int32_t)mipHeight, 1 };
vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
// The final blit's destination is never read again, so it
// skips the DST->SRC barrier and is taken straight to the
// consumer layout by the batched final transition below.
if (i + 1 < mipLevels) {
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, i, 1);
}
}
std::array<VkImageMemoryBarrier, 2> finalBarriers;
std::uint32_t finalCount = BuildMipChainFinalBarriers(image, layout, mipLevels, finalBarriers);
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, 0, 0, nullptr, 0, nullptr, finalCount, finalBarriers.data());
} else {
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels);
}
ReleaseStaging();
}
// Upload only the sub-rectangle [x, x+w) × [y, y+h) of the staging
// buffer into the image, instead of the whole extent. The staging
// buffer keeps the full image row stride, so bufferRowLength stays
// `width` and bufferOffset just points at the rect's first texel —
// imageOffset/imageExtent then carve out the rect on the GPU side.
// Layout transitions stay whole-image (cheap); only the copy extent
// shrinks. Single mip level only: the partial-upload path is for
// CPU-streamed atlases (FontAtlas) which carry no mips, so there is
// no blit chain to regenerate.
void UpdateRegion(VkCommandBuffer cmd, VkImageLayout layout, std::uint32_t x, std::uint32_t y, std::uint32_t w, std::uint32_t h) {
buffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
TransitionImageLayout(cmd, image, layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, consumerStage, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, 0, mipLevels);
VkBufferImageCopy region{};
region.bufferOffset = (static_cast<VkDeviceSize>(y) * width + x) * sizeof(PixelType);
region.bufferRowLength = width;
region.bufferImageHeight = 0;
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;
region.imageOffset = { static_cast<std::int32_t>(x), static_cast<std::int32_t>(y), 0 };
region.imageExtent = { w, h, 1 };
vkCmdCopyBufferToImage(cmd, buffer.buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels);
}
// GPU compressed-asset Update: stage compressed bytes, decompress
// into `buffer` via VK_EXT_memory_decompression, then copy buffer→image
// and transition to `layout`. Falls back to CPU decode + the existing
// Update path when Device::memoryDecompressionSupported is false.
// Caller is responsible for the dimensions matching: asset.sizeX/sizeY
// must equal this->width/height (set by Create), and asset.pixelStride
// must equal sizeof(PixelType).
void Update(const CompressedTextureAsset& asset, VkCommandBuffer cmd, VkImageLayout layout) {
if (asset.pixelStride != sizeof(PixelType)) {
throw std::runtime_error("ImageVulkan::Update(compressed): pixel stride mismatch");
}
if (!Device::memoryDecompressionSupported) {
std::span<PixelType> dst{ buffer.value, static_cast<std::size_t>(width) * height };
std::array<std::span<std::byte>, 1> outputs = {
std::as_writable_bytes(dst),
};
Compression::DecompressCPU(asset.blob, outputs);
Update(cmd, layout);
return;
}
// Re-create the staging-into-image buffer with MEMORY_DECOMPRESSION
// permission so the GPU codec can write into it. Keeps it
// HOST_VISIBLE (matches the existing path) — on UMA / ReBAR that's
// a fast path, on older systems the decompress writes traverse
// PCIe but correctness is unchanged.
buffer.Resize(
VK_BUFFER_USAGE_TRANSFER_SRC_BIT
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
static_cast<std::uint32_t>(width) * height);
compressedStaging.Resize(
VK_BUFFER_USAGE_TRANSFER_SRC_BIT
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
static_cast<std::uint32_t>(asset.blob.bytes.size()));
std::memcpy(compressedStaging.value, asset.blob.bytes.data(), asset.blob.bytes.size());
compressedStaging.FlushDevice();
std::vector<VkDecompressMemoryRegionEXT> regions;
for (const Compression::RegionMeta& r : asset.blob.regions) {
if (r.decompressedSize == 0) continue;
std::span<const std::byte> streamBytes(
asset.blob.bytes.data() + r.srcOffset,
static_cast<std::size_t>(r.compressedSize));
Decompress::ExpandStreamToTileRegions(
streamBytes,
compressedStaging.address + r.srcOffset,
buffer.address,
regions);
}
Decompress::DecompressOnGPU(
cmd,
regions,
VK_PIPELINE_STAGE_2_COPY_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.
// We've already inserted the decompress→transfer-read barrier,
// so we skip the FlushDevice host-write barrier the regular Update
// would emit (no host write happened).
TransitionImageLayout(cmd, image, layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, consumerStage, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, 0, mipLevels);
VkBufferImageCopy region{};
region.bufferOffset = 0;
region.bufferRowLength = 0;
region.bufferImageHeight = 0;
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;
region.imageOffset = {0, 0, 0};
region.imageExtent = { width, height, 1 };
vkCmdCopyBufferToImage(cmd, buffer.buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
if (mipLevels > 1) {
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, 0, 1);
for (std::uint16_t i = 1; i < mipLevels; ++i) {
std::uint16_t mipWidth = width >> i;
std::uint16_t mipHeight = height >> i;
std::uint16_t previousMipWidth = width >> (i - std::uint16_t(1));
std::uint16_t previousMipHeight = height >> (i - std::uint16_t(1));
VkImageBlit blit = {};
blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blit.srcSubresource.mipLevel = i - 1;
blit.srcSubresource.baseArrayLayer = 0;
blit.srcSubresource.layerCount = 1;
blit.srcOffsets[0] = { 0, 0, 0 };
blit.srcOffsets[1] = { (int32_t)previousMipWidth, (int32_t)previousMipHeight, 1 };
blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blit.dstSubresource.mipLevel = i;
blit.dstSubresource.baseArrayLayer = 0;
blit.dstSubresource.layerCount = 1;
blit.dstOffsets[0] = { 0, 0, 0 };
blit.dstOffsets[1] = { (int32_t)mipWidth, (int32_t)mipHeight, 1 };
vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
// The final blit's destination is never read again, so it
// skips the DST->SRC barrier and is taken straight to the
// consumer layout by the batched final transition below.
if (i + 1 < mipLevels) {
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, i, 1);
}
}
std::array<VkImageMemoryBarrier, 2> finalBarriers;
std::uint32_t finalCount = BuildMipChainFinalBarriers(image, layout, mipLevels, finalBarriers);
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, 0, 0, nullptr, 0, nullptr, finalCount, finalBarriers.data());
} else {
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels);
}
ReleaseStaging();
}
void Destroy() {
vkDestroyImageView(Device::device, imageView, nullptr);
vkDestroyImage(Device::device, image, nullptr);
vkFreeMemory(Device::device, imageMemory, nullptr);
// Free any staging this image still owns: a streamed image's
// persistent `buffer`, or either staging buffer on an image
// destroyed before its first upload released them (issue #114).
// ReleaseStaging()/Update already null released handles, so these
// are no-ops then; Clear() is gated on a live handle so a static
// texture (whose `buffer` was deferred-cleared) can't double-free.
if (buffer.buffer != VK_NULL_HANDLE) buffer.Clear();
if (compressedStaging.buffer != VK_NULL_HANDLE) compressedStaging.Clear();
}
private:
// Release the host-visible staging `buffer` after a one-shot upload,
// unless this is a streamed image (FontAtlas) whose persistent map must
// survive. The recorded buffer→image copy still references buffer.buffer,
// so route through the fence-keyed deletion queue (#101/#102) instead of
// an immediate Clear(): DeferredClear nulls the handle (so re-entry and
// Destroy treat the staging as already gone) while the allocation
// outlives this submit's frame. Issue #114.
void ReleaseStaging() {
if (!streamed) {
buffer.DeferredClear();
}
}
void TransitionImageLayout(VkCommandBuffer cmd, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask, std::uint32_t mipLevel, std::uint32_t count) {
VkImageMemoryBarrier barrier = {};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = oldLayout;
barrier.newLayout = newLayout;
barrier.image = image;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier.subresourceRange.baseMipLevel = mipLevel;
barrier.subresourceRange.levelCount = count;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;
barrier.srcAccessMask = srcAccessMask;
barrier.dstAccessMask = dstAccessMask;
vkCmdPipelineBarrier(cmd, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}
};
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM