//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // The swapchain images are created with TRANSFER_DST where the surface // allows it, so a pass can blit or clear into them: the swapchain used to be // STORAGE | TRANSFER_SRC only, which left a compute shader as the only way to // put an offscreen image on screen. This checks the usage the Window records, // then clears the swapchain image from a pass, reads the frame back and // checks the colour. // // Needs a live Wayland compositor and a Vulkan device. The validation check // only means something with CRAFTER_GRAPHICS_VALIDATION set. #include #include "vulkan/vulkan.h" // PNG decode for the read-back frame. The library only compiles the writer // (Window::CaptureNextFrame); STB_IMAGE_STATIC keeps the reader private to this file. #define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_STATIC #define STBI_ONLY_PNG #include "../../lib/stb_image.h" import Crafter.Graphics; import std; using namespace Crafter; namespace fs = std::filesystem; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } // Clears the frame's swapchain image with vkCmdClearColorImage, a transfer // write. struct ClearPass : RenderPass { VkClearColorValue colour = {{0.2f, 0.4f, 0.8f, 1.0f}}; void Record(VkCommandBuffer cmd, std::uint32_t, Window& window) override { const VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; vkCmdClearColorImage(cmd, window.images[window.currentBuffer], VK_IMAGE_LAYOUT_GENERAL, &colour, 1, &range); } VkPipelineStageFlags SwapchainStage() const override { return VK_PIPELINE_STAGE_TRANSFER_BIT; } }; } // namespace int main() { Device::Initialize(); Device::validationErrorCount = 0; Window window(320, 240, "SwapchainTransferDst test"); VkSurfaceCapabilitiesKHR caps; Device::CheckVkResult(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(Device::physDevice, window.vulkanSurface, &caps)); const bool surfaceAllows = (caps.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0; Check((window.swapchainUsage & (VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT)) == (VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT), "the swapchain keeps STORAGE and TRANSFER_SRC"); Check(((window.swapchainUsage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0) == surfaceAllows, std::format("TRANSFER_DST is used exactly when the surface allows it ({})", surfaceAllows ? "it does" : "it doesn't")); if (!surfaceAllows) { std::println("SKIP the clear-and-read-back check (this surface has no TRANSFER_DST)"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } window.StartInit(); window.FinishInit(); ClearPass clear; window.passes.push_back(&clear); for (int i = 0; i < Window::numFrames * 2; ++i) window.Render(); Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); const fs::path out = fs::temp_directory_path() / "crafter-swapchain-transfer-dst.png"; std::error_code ec; fs::remove(out, ec); window.CaptureNextFrame(out); window.Render(); if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) { Check(Device::validationErrorCount == 0, std::format("no Vulkan validation errors while clearing and capturing ({} seen)", Device::validationErrorCount)); } int width = 0, height = 0, channels = 0; stbi_uc* pixels = stbi_load(out.string().c_str(), &width, &height, &channels, STBI_rgb_alpha); Check(pixels != nullptr, "the frame was read back"); if (pixels != nullptr) { const stbi_uc* p = pixels + (static_cast(height / 2) * width + width / 2) * 4; // 0.2, 0.4, 0.8 in a UNORM image. const bool cleared = std::abs(p[0] - 51) <= 2 && std::abs(p[1] - 102) <= 2 && std::abs(p[2] - 204) <= 2; Check(cleared, std::format("the cleared colour reached the frame (got {}, {}, {})", p[0], p[1], p[2])); stbi_image_free(pixels); } Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }