From c1fec9e8afa8f1853ecf8e8c67cca739026cb6c5 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 16:53:51 +0000 Subject: [PATCH] perf(ui): scope inter-dispatch barrier to swapchain image (#48) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UIRenderer::Dispatch inserted a queue-wide VkMemoryBarrier (SHADER_WRITE -> SHADER_READ|WRITE) before every dispatch after the first, flushing/invalidating all shader caches even though the only hazard is the read-modify-write of the single swapchain storage image that every UI pass shares. Narrow it to a VkImageMemoryBarrier scoped to that image's subresource (COMPUTE -> COMPUTE, GENERAL -> GENERAL, no layout transition). Same correctness — later passes still observe earlier passes' writes — but only this image's caches are flushed; unrelated resources are no longer invalidated. This is a narrower cache flush only: the execution dependency stays compute->compute queue-wide, so passes still do not overlap. Eliminating the barriers entirely requires pass fusion (#47) and is intentionally out of scope here. Co-Authored-By: Claude Opus 4.8 --- implementations/Crafter.Graphics-UI.cpp | 36 +++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/implementations/Crafter.Graphics-UI.cpp b/implementations/Crafter.Graphics-UI.cpp index e8ce871..602deca 100644 --- a/implementations/Crafter.Graphics-UI.cpp +++ b/implementations/Crafter.Graphics-UI.cpp @@ -182,15 +182,41 @@ void UIRenderer::Dispatch(GraphicsCommandBuffer cmd, const GraphicsComputeShader const void* push, std::uint32_t pushBytes, std::uint32_t gx, std::uint32_t gy, std::uint32_t gz) { if (!firstDispatchThisFrame_) { - VkMemoryBarrier mb { - .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, - .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, - .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, + // Every UI pass read-modify-writes the same swapchain storage image + // (later passes overdraw earlier ones), so each dispatch must observe + // the previous dispatch's writes. The only resource the hazard touches + // is that one image, so scope the dependency to its subresource with a + // VkImageMemoryBarrier rather than a queue-wide VkMemoryBarrier: same + // correctness, but only this image's shader caches are flushed — + // unrelated buffers/images are no longer invalidated. The image stays + // in VK_IMAGE_LAYOUT_GENERAL (bound as a storage image), so this is a + // pure memory dependency, no layout transition. + // + // The execution dependency is still COMPUTE->COMPUTE over the whole + // queue, so the passes do NOT overlap — this is a narrower cache flush + // only. Eliminating the barriers entirely requires fusing the passes + // into one dispatch (tracked in #47); do not attempt that here. + VkImageMemoryBarrier ib { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = window_->images[window_->currentBuffer], + .subresourceRange = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, }; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - 0, 1, &mb, 0, nullptr, 0, nullptr); + 0, 0, nullptr, 0, nullptr, 1, &ib); } firstDispatchThisFrame_ = false; shader.Dispatch(cmd, push, pushBytes, gx, gy, gz); -- 2.54.0