577 lines
28 KiB
C++
577 lines
28 KiB
C++
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
// Issue #160: the swapchain gets as many images as its present mode needs,
|
||
|
|
// and the frame loop takes however many that is. CreateSwapchain used to ask
|
||
|
|
// for exactly Window::numFrames (3) images whatever the surface said, read
|
||
|
|
// them into a three-element array, and key everything a frame in flight
|
||
|
|
// owns, its command buffer, its fence and the descriptor-heap copy holding
|
||
|
|
// "its" swapchain image, by the acquired image index. A swapchain of any
|
||
|
|
// other size fell back to FIFO (asked for MAILBOX or IMMEDIATE) or threw,
|
||
|
|
// and a surface whose minimum is above three got a create info below it.
|
||
|
|
// Mesa's MAILBOX needs at least four images: Turnip on phoc, and llvmpipe on
|
||
|
|
// any Wayland compositor.
|
||
|
|
//
|
||
|
|
// Checked here:
|
||
|
|
//
|
||
|
|
// - Window::SwapchainMinImageCount on the numbers drivers report.
|
||
|
|
// - For every present mode the surface offers, SetPresentMode makes a
|
||
|
|
// swapchain of that mode, with at least the images the window asks for
|
||
|
|
// (Window::SwapchainCapabilities, the mode's own count where the device
|
||
|
|
// has swapchain_maintenance1).
|
||
|
|
// - Every frame draws into the image it presents. A UI pass fills the
|
||
|
|
// window with a colour that differs from frame to frame, and a transfer
|
||
|
|
// pass after it copies the middle texel of the frame's swapchain image
|
||
|
|
// into the frame slot's buffer, read back when the slot comes round
|
||
|
|
// again. With an image count other than numFrames, frames and images
|
||
|
|
// pair up differently from frame to frame: a heap copy that kept one
|
||
|
|
// image would have the UI draw into another frame's image, and the texel
|
||
|
|
// would hold an older colour.
|
||
|
|
// - A SetPresentMode or Resize from a listener of the frame being recorded
|
||
|
|
// takes effect after that frame's present: the rest of the frame indexes
|
||
|
|
// the swapchain it acquired from.
|
||
|
|
// - A swapchain slot listed during a frame is written for that frame (heap
|
||
|
|
// mode, where the descriptor can be read back and compared with the UI's).
|
||
|
|
// - Two windows sharing one heap each draw their own colours: a window
|
||
|
|
// writes only its own swapchain slots.
|
||
|
|
// - No Vulkan validation errors, which only means something with
|
||
|
|
// CRAFTER_GRAPHICS_VALIDATION set and the layer loaded.
|
||
|
|
//
|
||
|
|
// The GPU this process picks may give every mode numFrames images (NVIDIA
|
||
|
|
// does), which leaves the case that matters untried, and a process runs one
|
||
|
|
// binding model. So the frame checks also run in child processes, picked
|
||
|
|
// with CRAFTER_GRAPHICS_DEVICE, on a GPU whose surface needs more images for
|
||
|
|
// some mode (this process's own, else llvmpipe, which most Linux machines
|
||
|
|
// have), in each binding model the parent didn't run there: on the
|
||
|
|
// descriptor heap, and with CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP on descriptor
|
||
|
|
// sets, the path Turnip takes. Without such a GPU that part is a note, not a
|
||
|
|
// failure. `--frames heap|sets` runs the frame checks alone on whichever GPU
|
||
|
|
// the environment picks.
|
||
|
|
//
|
||
|
|
// Needs a live Wayland compositor and a Vulkan device, like FrameLoopSync.
|
||
|
|
|
||
|
|
#include "vulkan/vulkan.h"
|
||
|
|
#include "vulkan/vk_enum_string_helper.h"
|
||
|
|
#include <cstdlib>
|
||
|
|
#include <cstring>
|
||
|
|
|
||
|
|
import Crafter.Graphics;
|
||
|
|
import Crafter.Event;
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The test binary's own directory: the build places the UI kernels (both
|
||
|
|
// variants) next to it, and the test runner's working directory is not that
|
||
|
|
// directory.
|
||
|
|
fs::path ExeDir() {
|
||
|
|
return fs::read_symlink("/proc/self/exe").parent_path();
|
||
|
|
}
|
||
|
|
|
||
|
|
int RunCommand(const std::string& cmd) {
|
||
|
|
int status = std::system(cmd.c_str());
|
||
|
|
if (status == -1) return -1;
|
||
|
|
return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f);
|
||
|
|
}
|
||
|
|
|
||
|
|
struct ModeCase {
|
||
|
|
PresentMode mode;
|
||
|
|
VkPresentModeKHR vk;
|
||
|
|
std::string_view name;
|
||
|
|
};
|
||
|
|
constexpr ModeCase kModes[] = {
|
||
|
|
{ PresentMode::Fifo, VK_PRESENT_MODE_FIFO_KHR, "FIFO" },
|
||
|
|
{ PresentMode::Mailbox, VK_PRESENT_MODE_MAILBOX_KHR, "MAILBOX" },
|
||
|
|
{ PresentMode::Immediate, VK_PRESENT_MODE_IMMEDIATE_KHR, "IMMEDIATE" },
|
||
|
|
};
|
||
|
|
|
||
|
|
std::vector<VkPresentModeKHR> OfferedModes(VkPhysicalDevice gpu, VkSurfaceKHR surface) {
|
||
|
|
std::uint32_t count = 0;
|
||
|
|
Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, surface, &count, nullptr));
|
||
|
|
std::vector<VkPresentModeKHR> modes(count);
|
||
|
|
Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, surface, &count, modes.data()));
|
||
|
|
return modes;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool Offers(const std::vector<VkPresentModeKHR>& offered, VkPresentModeKHR mode) {
|
||
|
|
return std::ranges::find(offered, mode) != offered.end();
|
||
|
|
}
|
||
|
|
|
||
|
|
// The most images any present mode under test asks for, for a swapchain of
|
||
|
|
// `surface` on `gpu`, sized as a window on it would size it.
|
||
|
|
std::uint32_t MostImagesAsked(VkPhysicalDevice gpu, VkSurfaceKHR surface, bool modePassedAlong) {
|
||
|
|
const std::vector<VkPresentModeKHR> offered = OfferedModes(gpu, surface);
|
||
|
|
std::uint32_t most = 0;
|
||
|
|
for (const ModeCase& m : kModes) {
|
||
|
|
if (Offers(offered, m.vk)) {
|
||
|
|
most = std::max(most, Window::SwapchainMinImageCount(
|
||
|
|
Window::SwapchainCapabilities(gpu, surface, m.vk, modePassedAlong)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return most;
|
||
|
|
}
|
||
|
|
|
||
|
|
void CheckMinImageCount() {
|
||
|
|
const auto asked = [](std::uint32_t minImageCount, std::uint32_t maxImageCount) {
|
||
|
|
VkSurfaceCapabilitiesKHR capabilities{};
|
||
|
|
capabilities.minImageCount = minImageCount;
|
||
|
|
capabilities.maxImageCount = maxImageCount;
|
||
|
|
return Window::SwapchainMinImageCount(capabilities);
|
||
|
|
};
|
||
|
|
Check(asked(2, 8) == Window::numFrames, "a minimum below numFrames asks for numFrames (NVIDIA: 2, at most 8)");
|
||
|
|
Check(asked(3, 0) == 3, "a minimum of numFrames asks for numFrames (Mesa's FIFO: 3, no maximum)");
|
||
|
|
Check(asked(4, 0) == 4, "a minimum above numFrames asks for the minimum (Mesa's MAILBOX: 4, no maximum)");
|
||
|
|
Check(asked(5, 6) == 5, "a minimum above numFrames within the maximum asks for the minimum");
|
||
|
|
Check(asked(1, 2) == 2, "a maximum below numFrames caps the request");
|
||
|
|
Check(asked(2, 0) == Window::numFrames, "no maximum leaves numFrames as it is");
|
||
|
|
}
|
||
|
|
|
||
|
|
// A colour for frame `n` that differs from every frame's within 200 of it,
|
||
|
|
// exact in eight bits.
|
||
|
|
std::array<std::uint8_t, 3> Colour(std::uint64_t n) {
|
||
|
|
return {
|
||
|
|
static_cast<std::uint8_t>(40 + (n * 7) % 200),
|
||
|
|
static_cast<std::uint8_t>(250 - (n * 13) % 200),
|
||
|
|
static_cast<std::uint8_t>((n * 29) % 256),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Copies the middle texel of the frame's swapchain image into the frame
|
||
|
|
// slot's buffer, after the UI pass drew it: what the frame presents. Added
|
||
|
|
// only for an 8-bit RGBA or BGRA swapchain, whose texel fits the buffer.
|
||
|
|
struct ReadbackPass : RenderPass {
|
||
|
|
std::array<VulkanBuffer<std::uint32_t, true>, Window::numFrames> texels;
|
||
|
|
|
||
|
|
void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override {
|
||
|
|
const VkBufferImageCopy region {
|
||
|
|
.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
|
||
|
|
.imageOffset = { static_cast<std::int32_t>(window.width / 2), static_cast<std::int32_t>(window.height / 2), 0 },
|
||
|
|
.imageExtent = { 1, 1, 1 },
|
||
|
|
};
|
||
|
|
vkCmdCopyImageToBuffer(cmd, window.images[window.currentImage], VK_IMAGE_LAYOUT_GENERAL,
|
||
|
|
texels[frameIdx].buffer, 1, ®ion);
|
||
|
|
const VkBufferMemoryBarrier copied {
|
||
|
|
.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 = texels[frameIdx].buffer,
|
||
|
|
.offset = 0,
|
||
|
|
.size = VK_WHOLE_SIZE,
|
||
|
|
};
|
||
|
|
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT,
|
||
|
|
0, 0, nullptr, 1, &copied, 0, nullptr);
|
||
|
|
}
|
||
|
|
|
||
|
|
VkPipelineStageFlags SwapchainStage() const override {
|
||
|
|
return VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// One window filled with a colour per frame through a UIRenderer on `heap`,
|
||
|
|
// and what each frame presented read back. Holds listeners that capture it:
|
||
|
|
// build it in place and don't move it.
|
||
|
|
struct ColourScene {
|
||
|
|
Window& window;
|
||
|
|
GraphicsDescriptorHeap& heap;
|
||
|
|
std::string_view name;
|
||
|
|
// Added to the frame number: two scenes sharing a heap draw different
|
||
|
|
// colours, so one that drew into the other's image is seen.
|
||
|
|
std::uint64_t colourOffset;
|
||
|
|
|
||
|
|
UIRenderer ui;
|
||
|
|
// One quad buffer per frame slot: the colour changes every frame, and a
|
||
|
|
// buffer the frames shared would be overwritten under the frames still
|
||
|
|
// in flight.
|
||
|
|
std::array<GraphicsBuffer<QuadItem, true>, Window::numFrames> quads;
|
||
|
|
std::array<BufferSlot, Window::numFrames> quadSlots;
|
||
|
|
ReadbackPass readback;
|
||
|
|
bool bgra = false;
|
||
|
|
bool checkTexels = false;
|
||
|
|
|
||
|
|
// The frame each slot's texel buffer holds, -1 for none, and what the
|
||
|
|
// texel checks found since Reset.
|
||
|
|
std::array<std::int64_t, Window::numFrames> frameOfSlot;
|
||
|
|
std::uint64_t frame = 0;
|
||
|
|
std::uint64_t framesChecked = 0;
|
||
|
|
std::uint32_t wrong = 0;
|
||
|
|
std::string firstWrong;
|
||
|
|
std::set<std::uint32_t> imagesUsed;
|
||
|
|
std::set<std::pair<std::uint32_t, std::uint32_t>> pairsUsed;
|
||
|
|
// Run once at the start of the next frame's build, then dropped.
|
||
|
|
std::function<void()> duringNextFrame;
|
||
|
|
EventListener<UIBuildArgs> buildSub;
|
||
|
|
|
||
|
|
ColourScene(Window& window, GraphicsDescriptorHeap& heap, std::string_view name, std::uint64_t colourOffset)
|
||
|
|
: window(window), heap(heap), name(name), colourOffset(colourOffset) {
|
||
|
|
const fs::path dir = ExeDir();
|
||
|
|
VkCommandBuffer init = window.StartInit();
|
||
|
|
ui.Initialize(window, heap, init,
|
||
|
|
dir / "ui-quads.comp.spv", dir / "ui-circles.comp.spv",
|
||
|
|
dir / "ui-images.comp.spv", dir / "ui-text.comp.spv",
|
||
|
|
dir / "ui-fused.comp.spv");
|
||
|
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||
|
|
quads[f].Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
||
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 1, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||
|
|
quadSlots[f] = ui.RegisterBuffer(quads[f]);
|
||
|
|
}
|
||
|
|
bgra = window.colorFormat == VK_FORMAT_B8G8R8A8_UNORM;
|
||
|
|
checkTexels = bgra || window.colorFormat == VK_FORMAT_R8G8B8A8_UNORM;
|
||
|
|
window.passes.push_back(&ui);
|
||
|
|
if (checkTexels) {
|
||
|
|
for (VulkanBuffer<std::uint32_t, true>& texel : readback.texels) {
|
||
|
|
texel.Create(VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 1);
|
||
|
|
}
|
||
|
|
window.passes.push_back(&readback);
|
||
|
|
} else {
|
||
|
|
std::println("NOTE {}: the swapchain format {} isn't 8-bit RGBA or BGRA: its colours aren't checked",
|
||
|
|
name, string_VkFormat(window.colorFormat));
|
||
|
|
}
|
||
|
|
window.FinishInit();
|
||
|
|
Reset();
|
||
|
|
|
||
|
|
buildSub.SetEvent(&ui.onBuild, [this](UIBuildArgs a) {
|
||
|
|
if (duringNextFrame) {
|
||
|
|
std::function<void()> run = std::move(duringNextFrame);
|
||
|
|
duringNextFrame = nullptr;
|
||
|
|
run();
|
||
|
|
}
|
||
|
|
// The slot's fence has been waited: its buffer holds what the
|
||
|
|
// frame numFrames ago presented. This frame's copy comes after.
|
||
|
|
CheckSlot(a.frameIdx);
|
||
|
|
frameOfSlot[a.frameIdx] = static_cast<std::int64_t>(frame);
|
||
|
|
imagesUsed.insert(this->window.currentImage);
|
||
|
|
pairsUsed.insert({ a.frameIdx, this->window.currentImage });
|
||
|
|
|
||
|
|
const std::array<std::uint8_t, 3> colour = Colour(frame + this->colourOffset);
|
||
|
|
quads[a.frameIdx].value[0] = QuadItem{ 0, 0, 4096, 4096,
|
||
|
|
colour[0] / 255.0f, colour[1] / 255.0f, colour[2] / 255.0f, 1.0f,
|
||
|
|
0, 0, 0, 0, 0, 0, 0, 0 };
|
||
|
|
quads[a.frameIdx].FlushDevice(a.cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||
|
|
ui.DispatchQuads(a.cmd, quadSlots[a.frameIdx], 1);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
~ColourScene() {
|
||
|
|
std::erase(window.passes, &ui);
|
||
|
|
std::erase(window.passes, &readback);
|
||
|
|
}
|
||
|
|
|
||
|
|
void Reset() {
|
||
|
|
frameOfSlot.fill(-1);
|
||
|
|
framesChecked = 0;
|
||
|
|
wrong = 0;
|
||
|
|
firstWrong.clear();
|
||
|
|
imagesUsed.clear();
|
||
|
|
pairsUsed.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
void CheckSlot(std::uint32_t slot) {
|
||
|
|
if (!checkTexels || frameOfSlot[slot] < 0) return;
|
||
|
|
const std::uint32_t texel = readback.texels[slot].value[0];
|
||
|
|
const std::array<std::uint8_t, 3> rgb = bgra
|
||
|
|
? std::array<std::uint8_t, 3>{ static_cast<std::uint8_t>(texel >> 16), static_cast<std::uint8_t>(texel >> 8), static_cast<std::uint8_t>(texel) }
|
||
|
|
: std::array<std::uint8_t, 3>{ static_cast<std::uint8_t>(texel), static_cast<std::uint8_t>(texel >> 8), static_cast<std::uint8_t>(texel >> 16) };
|
||
|
|
const std::array<std::uint8_t, 3> expected = Colour(static_cast<std::uint64_t>(frameOfSlot[slot]) + colourOffset);
|
||
|
|
bool same = true;
|
||
|
|
for (std::size_t c = 0; c < 3; ++c) {
|
||
|
|
same = same && std::abs(static_cast<int>(rgb[c]) - static_cast<int>(expected[c])) <= 1;
|
||
|
|
}
|
||
|
|
++framesChecked;
|
||
|
|
if (!same && wrong++ == 0) {
|
||
|
|
firstWrong = std::format("frame {} presented ({}, {}, {}), drew ({}, {}, {})", frameOfSlot[slot],
|
||
|
|
rgb[0], rgb[1], rgb[2], expected[0], expected[1], expected[2]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// One frame of this scene's window.
|
||
|
|
void Render() {
|
||
|
|
window.Render();
|
||
|
|
++frame;
|
||
|
|
}
|
||
|
|
|
||
|
|
// After the frames since Reset: settles the queue, reads the slots not
|
||
|
|
// read yet, and reports.
|
||
|
|
void Report(std::string_view what) {
|
||
|
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||
|
|
for (std::uint32_t slot = 0; slot < Window::numFrames; ++slot) {
|
||
|
|
CheckSlot(slot);
|
||
|
|
frameOfSlot[slot] = -1;
|
||
|
|
}
|
||
|
|
if (checkTexels) {
|
||
|
|
Check(wrong == 0, std::format("{}: each of {} frames presented the colour it drew{}", what, framesChecked,
|
||
|
|
wrong == 0 ? std::string() : std::format(" ({} didn't; {})", wrong, firstWrong)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// The present modes on the scene's window, then a rebuild asked for during a
|
||
|
|
// frame, then a slot listed during one. Sets `moreImagesThanFrames` when some
|
||
|
|
// mode's swapchain had more images than numFrames.
|
||
|
|
void CheckModes(ColourScene& scene, bool& moreImagesThanFrames) {
|
||
|
|
Window& window = scene.window;
|
||
|
|
const std::vector<VkPresentModeKHR> offered = OfferedModes(Device::physDevice, window.vulkanSurface);
|
||
|
|
for (const ModeCase& m : kModes) {
|
||
|
|
if (!Offers(offered, m.vk)) {
|
||
|
|
std::println("NOTE the surface doesn't offer {}", m.name);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
// Asked before the swapchain is made, which asks again: the
|
||
|
|
// validation layer checks the image count against the last answer.
|
||
|
|
const VkSurfaceCapabilitiesKHR capabilities = Window::SwapchainCapabilities(
|
||
|
|
Device::physDevice, window.vulkanSurface, m.vk, Device::swapchainMaintenance1Supported);
|
||
|
|
const std::uint32_t asked = Window::SwapchainMinImageCount(capabilities);
|
||
|
|
window.SetPresentMode(m.mode);
|
||
|
|
Check(window.swapchainPresentMode == m.vk,
|
||
|
|
std::format("{}: the swapchain was made with it, not a fallback ({})", m.name, string_VkPresentModeKHR(window.swapchainPresentMode)));
|
||
|
|
const std::size_t images = window.images.size();
|
||
|
|
Check(images >= asked && images >= capabilities.minImageCount,
|
||
|
|
std::format("{}: {} images, at least the {} asked for and the mode's minimum of {}", m.name, images, asked, capabilities.minImageCount));
|
||
|
|
Check(window.imageViewInfos.size() == images && window.imageViews.size() == images
|
||
|
|
&& window.imageInitialised.size() == images && window.renderFinishedSemaphores.size() >= images,
|
||
|
|
std::format("{}: the per-image state covers all {} images", m.name, images));
|
||
|
|
moreImagesThanFrames = moreImagesThanFrames || images > Window::numFrames;
|
||
|
|
|
||
|
|
scene.Reset();
|
||
|
|
// Enough frames for every frame slot to meet several images.
|
||
|
|
const std::uint64_t frames = 4 * std::max<std::uint64_t>(images, Window::numFrames);
|
||
|
|
for (std::uint64_t i = 0; i < frames; ++i) {
|
||
|
|
scene.Render();
|
||
|
|
}
|
||
|
|
scene.Report(m.name);
|
||
|
|
Check(scene.imagesUsed.size() > 1, std::format("{}: the frames went round {} of the {} images", m.name, scene.imagesUsed.size(), images));
|
||
|
|
std::println(" {}: {} images, {} pairings of frame slot and image", m.name, images, scene.pairsUsed.size());
|
||
|
|
}
|
||
|
|
|
||
|
|
// A present mode set, and a resize asked for, from a listener of the
|
||
|
|
// frame being recorded: the frame goes on with the swapchain it
|
||
|
|
// acquired from, and the new one is made after its present.
|
||
|
|
const ModeCase* first = nullptr;
|
||
|
|
const ModeCase* second = nullptr;
|
||
|
|
for (const ModeCase& m : kModes) {
|
||
|
|
if (!Offers(offered, m.vk)) continue;
|
||
|
|
(first == nullptr ? first : second) = &m;
|
||
|
|
if (second != nullptr) break;
|
||
|
|
}
|
||
|
|
if (second != nullptr) {
|
||
|
|
window.SetPresentMode(first->mode);
|
||
|
|
const VkSwapchainKHR before = window.swapChain;
|
||
|
|
const std::uint32_t widthBefore = window.width;
|
||
|
|
const std::uint32_t heightBefore = window.height;
|
||
|
|
bool unchangedInFrame = false;
|
||
|
|
scene.duringNextFrame = [&] {
|
||
|
|
window.SetPresentMode(second->mode);
|
||
|
|
window.Resize(widthBefore + 16, heightBefore + 8);
|
||
|
|
unchangedInFrame = window.swapChain == before && window.swapchainPresentMode == first->vk
|
||
|
|
&& window.width == widthBefore && window.height == heightBefore && window.currentImage < window.images.size();
|
||
|
|
};
|
||
|
|
scene.Reset();
|
||
|
|
scene.Render();
|
||
|
|
Check(unchangedInFrame, std::format("a {} -> {} switch and a resize asked for during a frame leave that frame's swapchain and size alone",
|
||
|
|
first->name, second->name));
|
||
|
|
Check(window.swapChain != before && window.swapchainPresentMode == second->vk,
|
||
|
|
std::format("after the frame the swapchain is made again, with {} ({})", second->name, string_VkPresentModeKHR(window.swapchainPresentMode)));
|
||
|
|
Check(window.width == widthBefore + 16 && window.height == heightBefore + 8,
|
||
|
|
std::format("after the frame the window has the size asked for ({}x{})", window.width, window.height));
|
||
|
|
for (int i = 0; i < 2 * Window::numFrames; ++i) {
|
||
|
|
scene.Render();
|
||
|
|
}
|
||
|
|
scene.Report("after the rebuild a frame asked for");
|
||
|
|
} else {
|
||
|
|
std::println("NOTE the surface offers one present mode: no mid-frame switch to check");
|
||
|
|
}
|
||
|
|
|
||
|
|
// A swapchain slot listed during a frame holds that frame's image at
|
||
|
|
// once: in heap mode the descriptor bytes match the UI output slot's,
|
||
|
|
// which the frame wrote before onUpdate.
|
||
|
|
if (Device::descriptorHeapSupported) {
|
||
|
|
const ImageSlotRange spare = scene.heap.AllocateImageSlots(1);
|
||
|
|
bool same = false;
|
||
|
|
scene.duringNextFrame = [&] {
|
||
|
|
scene.heap.AddSwapchainSlot(spare.firstElement, window);
|
||
|
|
const std::uint8_t* copy = scene.heap.resourceHeap[window.currentFrame].value;
|
||
|
|
same = std::memcmp(copy + scene.heap.ImageByteOffset(spare.firstElement),
|
||
|
|
copy + scene.heap.ImageByteOffset(scene.ui.OutImageSlot()),
|
||
|
|
Device::descriptorHeapProperties.imageDescriptorSize) == 0;
|
||
|
|
};
|
||
|
|
scene.Render();
|
||
|
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||
|
|
scene.heap.FreeImageSlots(spare);
|
||
|
|
Check(same, "a swapchain slot listed during a frame holds that frame's image at once");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Renders on this process's GPU: every offered mode on one window, then two
|
||
|
|
// windows sharing the heap. Sets `moreImagesThanFrames` when some mode's
|
||
|
|
// swapchain had more images than numFrames.
|
||
|
|
int RenderFrames(bool& moreImagesThanFrames) {
|
||
|
|
// Two scenes' output slots, one spare for the mid-frame slot, and each
|
||
|
|
// scene's quad buffers.
|
||
|
|
GraphicsDescriptorHeap heap;
|
||
|
|
heap.Initialize(/*images*/ 3, /*buffers*/ 2 * Window::numFrames, /*samplers*/ 1);
|
||
|
|
{
|
||
|
|
Window window(160, 120, "SwapchainImageCount test");
|
||
|
|
window.descriptorHeap = &heap;
|
||
|
|
{
|
||
|
|
ColourScene scene(window, heap, "first window", 0);
|
||
|
|
CheckModes(scene, moreImagesThanFrames);
|
||
|
|
|
||
|
|
// A second window on the same heap. Interleaved frames: one
|
||
|
|
// window's slot writes land in copies the other's frames in
|
||
|
|
// flight are bound to, and must not touch its slots.
|
||
|
|
Window second(160, 120, "SwapchainImageCount second window");
|
||
|
|
second.descriptorHeap = &heap;
|
||
|
|
{
|
||
|
|
ColourScene other(second, heap, "second window", 100);
|
||
|
|
scene.Reset();
|
||
|
|
for (int i = 0; i < 4 * Window::numFrames; ++i) {
|
||
|
|
scene.Render();
|
||
|
|
other.Render();
|
||
|
|
}
|
||
|
|
scene.Report("two windows on one heap, the first");
|
||
|
|
other.Report("two windows on one heap, the second");
|
||
|
|
}
|
||
|
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||
|
|
}
|
||
|
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) {
|
||
|
|
Check(Device::validationErrorCount == 0,
|
||
|
|
std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount));
|
||
|
|
}
|
||
|
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||
|
|
}
|
||
|
|
|
||
|
|
// A child's run of the frame checks in binding model `model` ("heap" or
|
||
|
|
// "sets") on whatever GPU its environment picks.
|
||
|
|
int FramesOnly(std::string_view model) {
|
||
|
|
try {
|
||
|
|
Device::Initialize();
|
||
|
|
} catch (const std::runtime_error& error) {
|
||
|
|
// A missing capability is a skip; anything else that failed is a
|
||
|
|
// failure of the run that was asked for.
|
||
|
|
if (std::string_view(error.what()).find("can't run this application") != std::string_view::npos) {
|
||
|
|
std::println("SKIP this GPU can't run the library: {}", error.what());
|
||
|
|
return 77;
|
||
|
|
}
|
||
|
|
std::println("FAIL Device::Initialize: {}", error.what());
|
||
|
|
return EXIT_FAILURE;
|
||
|
|
}
|
||
|
|
std::println(" on {}, {}", std::string_view(Device::deviceProperties.deviceName),
|
||
|
|
Device::descriptorHeapSupported ? "descriptor heap" : "descriptor sets");
|
||
|
|
if (model == "heap" && !Device::descriptorHeapSupported) {
|
||
|
|
std::println("SKIP this GPU has no descriptor heap");
|
||
|
|
return 77;
|
||
|
|
}
|
||
|
|
Check(Device::descriptorHeapSupported == (model == "heap"),
|
||
|
|
std::format("the run is on {}, as asked", model == "heap" ? "the descriptor heap" : "descriptor sets"));
|
||
|
|
bool moreImagesThanFrames = false;
|
||
|
|
return RenderFrames(moreImagesThanFrames);
|
||
|
|
}
|
||
|
|
|
||
|
|
// The number CRAFTER_GRAPHICS_DEVICE gives the GPU `gpu` is one of.
|
||
|
|
std::optional<std::size_t> GpuNumber(const std::vector<std::vector<VkPhysicalDevice>>& gpus, VkPhysicalDevice gpu) {
|
||
|
|
for (std::size_t i = 0; i < gpus.size(); ++i) {
|
||
|
|
if (std::ranges::find(gpus[i], gpu) != gpus[i].end()) return i;
|
||
|
|
}
|
||
|
|
return std::nullopt;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool PresentsTo(VkPhysicalDevice gpu, VkSurfaceKHR surface) {
|
||
|
|
std::uint32_t families = 0;
|
||
|
|
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &families, nullptr);
|
||
|
|
for (std::uint32_t family = 0; family < families; ++family) {
|
||
|
|
VkBool32 supported = VK_FALSE;
|
||
|
|
if (vkGetPhysicalDeviceSurfaceSupportKHR(gpu, family, surface, &supported) == VK_SUCCESS && supported) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The number of a GPU, other than `own`, whose swapchain for `surface` would
|
||
|
|
// have more images than numFrames in some mode. Whether it has
|
||
|
|
// swapchain_maintenance1 isn't known without a device, so its counts are
|
||
|
|
// taken as without: at most an overestimate, and that GPU's MAILBOX needs the
|
||
|
|
// most either way.
|
||
|
|
std::optional<std::size_t> GpuWithMoreImages(const std::vector<std::vector<VkPhysicalDevice>>& gpus,
|
||
|
|
std::size_t own, VkSurfaceKHR surface) {
|
||
|
|
for (std::size_t i = 0; i < gpus.size(); ++i) {
|
||
|
|
if (i == own || !PresentsTo(gpus[i].front(), surface)) continue;
|
||
|
|
if (MostImagesAsked(gpus[i].front(), surface, /*modePassedAlong*/ false) > Window::numFrames) {
|
||
|
|
return i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return std::nullopt;
|
||
|
|
}
|
||
|
|
|
||
|
|
int Drive() {
|
||
|
|
CheckMinImageCount();
|
||
|
|
|
||
|
|
Device::Initialize();
|
||
|
|
const bool ownHeap = Device::descriptorHeapSupported;
|
||
|
|
std::println(" on {}, {}, swapchain_maintenance1 {}", std::string_view(Device::deviceProperties.deviceName),
|
||
|
|
ownHeap ? "descriptor heap" : "descriptor sets", Device::swapchainMaintenance1Supported ? "on" : "off");
|
||
|
|
bool moreImagesThanFrames = false;
|
||
|
|
RenderFrames(moreImagesThanFrames);
|
||
|
|
|
||
|
|
// Where frames and images pair up differently, on each binding model:
|
||
|
|
// this process covered its own GPU's model if that GPU makes more images
|
||
|
|
// than frames; the children cover the rest.
|
||
|
|
const std::vector<std::vector<VkPhysicalDevice>> gpus = Device::DistinctGpus(Device::instance);
|
||
|
|
const std::optional<std::size_t> own = GpuNumber(gpus, Device::physDevice);
|
||
|
|
std::optional<std::size_t> target = moreImagesThanFrames ? own : std::nullopt;
|
||
|
|
if (!target && own) {
|
||
|
|
Window probe(160, 120, "SwapchainImageCount probe");
|
||
|
|
target = GpuWithMoreImages(gpus, *own, probe.vulkanSurface);
|
||
|
|
}
|
||
|
|
if (!target) {
|
||
|
|
std::println("NOTE no GPU here makes a swapchain with more than {} images, "
|
||
|
|
"so frames and images were never paired differently", Window::numFrames);
|
||
|
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||
|
|
}
|
||
|
|
|
||
|
|
const fs::path exe = fs::read_symlink("/proc/self/exe");
|
||
|
|
const auto child = [&](std::string_view model) {
|
||
|
|
// `env -u` so a CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP inherited from
|
||
|
|
// the caller's environment can't turn the heap run into a sets run.
|
||
|
|
// LP_NUM_THREADS keeps llvmpipe, the usual target, off most of the
|
||
|
|
// machine's cores while the other tests run, timing ones among them.
|
||
|
|
// env reads its options before the first assignment.
|
||
|
|
const int exit = RunCommand(std::format("env {} LP_NUM_THREADS=4 CRAFTER_GRAPHICS_DEVICE={} '{}' --frames {}",
|
||
|
|
model == "heap" ? "-u CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP" : "CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP=1",
|
||
|
|
*target, exe.string(), model));
|
||
|
|
if (exit == 77) {
|
||
|
|
std::println("NOTE GPU {} skipped the run on {} (above)", *target, model);
|
||
|
|
} else {
|
||
|
|
Check(exit == 0, std::format("GPU {} draws the frames right on {} (exit {})", *target, model, exit));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
const bool coveredHere = moreImagesThanFrames;
|
||
|
|
if (!(coveredHere && ownHeap)) child("heap");
|
||
|
|
if (!(coveredHere && !ownHeap)) child("sets");
|
||
|
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
int main(int argc, char** argv) {
|
||
|
|
// It would override every present mode under test.
|
||
|
|
unsetenv("CRAFTER_PRESENT_IMMEDIATE");
|
||
|
|
const int result = argc == 3 && std::string_view(argv[1]) == "--frames" ? FramesOnly(argv[2]) : Drive();
|
||
|
|
std::println("{}", result == EXIT_SUCCESS ? "all checks passed" : result == 77 ? "skipped" : "check(s) failed");
|
||
|
|
return result;
|
||
|
|
}
|