/* 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 02110-1301 USA */ // Regression test for issue #70: ImageVulkan's mip-chain upload used to issue // N+1 layout-transition barriers per chain, one vkCmdPipelineBarrier per call. // The final blit's destination level is never read again, so its dedicated // TRANSFER_DST -> TRANSFER_SRC barrier is redundant; the chain now folds that // last level straight into the final transition, which batches two // VkImageMemoryBarrier entries into a single vkCmdPipelineBarrier: // - levels [0, mipLevels-1): TRANSFER_SRC_OPTIMAL -> consumer layout // - level mipLevels-1 : TRANSFER_DST_OPTIMAL -> consumer layout // BuildMipChainFinalBarriers is the pure builder for that batched barrier, so // this test drives it directly with a sentinel image handle — no GPU device // needed at runtime, mirroring MemoryTypeFallback / UploadStrategy. #include #include "vulkan/vulkan.h" import Crafter.Graphics; import std; using namespace Crafter; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } // A non-null sentinel so we can assert the builder propagates the image handle // into every barrier without owning a real VkImage. VkImage SentinelImage() { return reinterpret_cast(static_cast(0xC0FFEE)); } // Common invariants every emitted barrier must satisfy regardless of which // level group it covers. void CheckCommon(const VkImageMemoryBarrier& b, VkImageLayout layout, std::string_view tag) { Check(b.sType == VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, std::format("{}: sType", tag)); Check(b.image == SentinelImage(), std::format("{}: image handle propagated", tag)); Check(b.newLayout == layout, std::format("{}: newLayout is the consumer layout", tag)); Check(b.dstAccessMask == VK_ACCESS_SHADER_READ_BIT, std::format("{}: dst access is shader read", tag)); Check(b.srcQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED, std::format("{}: src queue ignored", tag)); Check(b.dstQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED, std::format("{}: dst queue ignored", tag)); Check(b.subresourceRange.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT, std::format("{}: color aspect", tag)); Check(b.subresourceRange.baseArrayLayer == 0, std::format("{}: base array layer 0", tag)); Check(b.subresourceRange.layerCount == 1, std::format("{}: single array layer", tag)); } } // namespace int main() { constexpr VkImageLayout LAYOUT = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; for (std::uint32_t mipLevels : {2u, 3u, 5u, 12u}) { std::array barriers; std::uint32_t count = BuildMipChainFinalBarriers(SentinelImage(), LAYOUT, mipLevels, barriers); // The whole point: the final transition is a single batched call of two // barriers, never one-per-level. Check(count == 2, std::format("mip={}: batched into exactly two barriers", mipLevels)); const VkImageMemoryBarrier& src = barriers[0]; const VkImageMemoryBarrier& dst = barriers[1]; CheckCommon(src, LAYOUT, std::format("mip={} src-group", mipLevels)); CheckCommon(dst, LAYOUT, std::format("mip={} dst-group", mipLevels)); // Group 0: the blit sources, [0, mipLevels-1), coming from TRANSFER_SRC. Check(src.oldLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, std::format("mip={}: source group transitions from TRANSFER_SRC", mipLevels)); Check(src.srcAccessMask == VK_ACCESS_TRANSFER_READ_BIT, std::format("mip={}: source group src access is transfer read", mipLevels)); Check(src.subresourceRange.baseMipLevel == 0, std::format("mip={}: source group starts at level 0", mipLevels)); Check(src.subresourceRange.levelCount == mipLevels - 1, std::format("mip={}: source group covers all but the last level", mipLevels)); // Group 1: the final blit's destination, still TRANSFER_DST, never read. Check(dst.oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, std::format("mip={}: last level transitions from TRANSFER_DST", mipLevels)); Check(dst.srcAccessMask == VK_ACCESS_TRANSFER_WRITE_BIT, std::format("mip={}: last level src access is transfer write", mipLevels)); Check(dst.subresourceRange.baseMipLevel == mipLevels - 1, std::format("mip={}: last level starts at the final mip", mipLevels)); Check(dst.subresourceRange.levelCount == 1, std::format("mip={}: last level covers a single mip", mipLevels)); // The two groups must tile [0, mipLevels) with no gap and no overlap, or // some level would be left in a transfer layout when the shader samples. std::uint32_t covered = src.subresourceRange.levelCount + dst.subresourceRange.levelCount; Check(covered == mipLevels, std::format("mip={}: the two groups cover every level exactly once", mipLevels)); Check(src.subresourceRange.baseMipLevel + src.subresourceRange.levelCount == dst.subresourceRange.baseMipLevel, std::format("mip={}: groups are contiguous (no gap/overlap)", mipLevels)); } if (failures != 0) { std::println("{} check(s) failed", failures); return EXIT_FAILURE; } std::println("all checks passed"); return EXIT_SUCCESS; }