106 lines
5.4 KiB
C++
106 lines
5.4 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// 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 <cstdlib>
|
|
#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<VkImage>(static_cast<std::uintptr_t>(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<VkImageMemoryBarrier, 2> 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;
|
|
}
|