From 8cca1dae47e153b87557afa35fba97582ccef93c Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 16:01:23 +0000 Subject: [PATCH] fix(image): scope sampled-image upload barriers to the real consumer stage (#54) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ImageVulkan hardcoded VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR as the consuming stage in every layout transition. The UI font atlas is sampled by a *compute* shader, so the upload barrier's dst scope never covered the real consumer — a correctness gap, not just lost overlap. Parameterize the consuming stage per image: store it once at Create (defaulting to RT, preserving existing RT/example callers) and reuse it in every Update / UpdateRegion / compressed-Update transition. FontAtlas passes VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT. Co-Authored-By: Claude Opus 4.8 --- .../Crafter.Graphics-FontAtlas.cpp | 5 +++- interfaces/Crafter.Graphics-ImageVulkan.cppm | 27 ++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/implementations/Crafter.Graphics-FontAtlas.cpp b/implementations/Crafter.Graphics-FontAtlas.cpp index c3479a7..051d04a 100644 --- a/implementations/Crafter.Graphics-FontAtlas.cpp +++ b/implementations/Crafter.Graphics-FontAtlas.cpp @@ -36,7 +36,10 @@ void FontAtlas::Initialize(GraphicsCommandBuffer cmd) { kAtlasSize, kAtlasSize, /*mipLevels*/ 1, cmd, VK_FORMAT_R8_UNORM, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + 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 ); std::memset(image.buffer.value, 0, kAtlasSize * kAtlasSize); #else diff --git a/interfaces/Crafter.Graphics-ImageVulkan.cppm b/interfaces/Crafter.Graphics-ImageVulkan.cppm index 7d6a12b..9ba72a6 100644 --- a/interfaces/Crafter.Graphics-ImageVulkan.cppm +++ b/interfaces/Crafter.Graphics-ImageVulkan.cppm @@ -46,11 +46,18 @@ export namespace Crafter { VulkanBuffer 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; - void Create(std::uint16_t width, std::uint16_t height, std::uint8_t mipLevels, VkCommandBuffer cmd, VkFormat format, VkImageCreateFlags flags, VkImageLayout layout) { + 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) { this->width = width; this->height = height; this->mipLevels = mipLevels; + this->consumerStage = consumerStage; buffer.Create( VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, @@ -99,14 +106,14 @@ export namespace Crafter { 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, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, 0, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); + 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, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, 0, mipLevels); + 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; @@ -156,9 +163,9 @@ export namespace Crafter { 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); } - TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); + TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); } else { - TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); + 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); } } @@ -173,7 +180,7 @@ export namespace Crafter { // 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, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, 0, mipLevels); + 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(y) * width + x) * sizeof(PixelType); @@ -187,7 +194,7 @@ export namespace Crafter { region.imageExtent = { w, h, 1 }; vkCmdCopyBufferToImage(cmd, buffer.buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); + 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 @@ -254,7 +261,7 @@ export namespace Crafter { // 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, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, 0, mipLevels); + 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; @@ -292,9 +299,9 @@ export namespace Crafter { vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); 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); } - TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); + TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); } else { - TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); + 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); } }