//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // An ImageVulkan that isn't square gets every level of its mip chain. The // chain was blitted with each level `size >> level` wide and high, which is // 0 for the shorter side once the level passes its log2: a 256×4 texture's // levels 3 to 8 were blitted with no height, so nothing wrote them and they // held whatever memory they got. A level is max(1, size >> level). // // A 256×4 and a 4×256 texture of one colour each get their full chain (9 // levels, as the longer side needs); every texel of every level must come // back that colour. Needs a Vulkan device and no window. The validation // check only means something with CRAFTER_GRAPHICS_VALIDATION set. #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; } VkCommandBuffer BeginCmd() { VkCommandBufferAllocateInfo allocInfo { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = Device::commandPool, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1, }; VkCommandBuffer cmd = VK_NULL_HANDLE; Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &allocInfo, &cmd)); VkCommandBufferBeginInfo beginInfo { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, }; Device::CheckVkResult(vkBeginCommandBuffer(cmd, &beginInfo)); return cmd; } void SubmitWait(VkCommandBuffer cmd) { Device::CheckVkResult(vkEndCommandBuffer(cmd)); VkSubmitInfo submitInfo { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &cmd, }; Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE)); Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd); } // RGBA8 texel as a 4-byte word, matching VK_FORMAT_R8G8B8A8_UNORM. using Texel = std::uint32_t; constexpr Texel kColour = 0xFF3366CCu; constexpr std::uint8_t kLevels = 9; std::uint32_t LevelSize(std::uint16_t size, std::uint32_t level) { return std::max(1u, static_cast(size) >> level); } void CheckChain(std::uint16_t width, std::uint16_t height) { ImageVulkan image; // Room for every level, back to back. std::uint32_t total = 0; for (std::uint32_t level = 0; level < kLevels; ++level) total += LevelSize(width, level) * LevelSize(height, level); VulkanBuffer readback; readback.Create(VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, total, VK_MEMORY_PROPERTY_HOST_CACHED_BIT); std::fill_n(readback.value, total, 0u); readback.FlushDevice(); VkCommandBuffer cmd = BeginCmd(); image.Create(width, height, kLevels, cmd, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); std::fill_n(image.buffer.value, static_cast(width) * height, kColour); image.Update(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); const VkImageMemoryBarrier toSource { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_SHADER_READ_BIT, .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, .oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = image.image, .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, kLevels, 0, 1 }, }; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toSource); std::array regions {}; std::uint32_t offset = 0; for (std::uint32_t level = 0; level < kLevels; ++level) { regions[level] = { .bufferOffset = static_cast(offset) * sizeof(Texel), .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 1 }, .imageExtent = { LevelSize(width, level), LevelSize(height, level), 1 }, }; offset += LevelSize(width, level) * LevelSize(height, level); } vkCmdCopyImageToBuffer(cmd, image.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.buffer, kLevels, regions.data()); const VkBufferMemoryBarrier toHost { .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = VK_ACCESS_HOST_READ_BIT, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .buffer = readback.buffer, .offset = 0, .size = VK_WHOLE_SIZE, }; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, &toHost, 0, nullptr); SubmitWait(cmd); readback.FlushHost(); std::vector wrong; offset = 0; for (std::uint32_t level = 0; level < kLevels; ++level) { const std::uint32_t count = LevelSize(width, level) * LevelSize(height, level); if (!std::all_of(readback.value + offset, readback.value + offset + count, [](Texel t) { return t == kColour; })) wrong.push_back(level); offset += count; } std::string list; for (std::uint32_t level : wrong) list += std::format(" {}", level); Check(wrong.empty(), std::format("every level of a {}×{} texture's chain has its colour (wrong:{})", width, height, wrong.empty() ? " none" : list)); image.Destroy(); } } // namespace int main() { Device::Initialize(); Device::validationErrorCount = 0; Device::framesInFlight = 2; Device::frameCounter = 0; CheckChain(256, 4); CheckChain(4, 256); // Square, as before. CheckChain(256, 256); Device::DrainDeletions(); if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) { Check(Device::validationErrorCount == 0, std::format("no Vulkan validation errors across the run ({} seen)", Device::validationErrorCount)); } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }