Merge pull request 'perf(image): batch final mip-chain layout transition (#70)' (#100) from claude/issue-70 into master

This commit is contained in:
catbot 2026-06-16 20:31:16 +02:00
commit e86facc2af
3 changed files with 202 additions and 4 deletions

View file

@ -31,6 +31,41 @@ 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:
@ -160,10 +195,17 @@ export namespace Crafter {
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);
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);
// 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);
}
}
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);
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);
}
@ -297,9 +339,16 @@ export namespace Crafter {
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);
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);
// 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);
}
}
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);
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);
}