Window - CreateSwapchain asked for exactly numFrames (3) images whatever the surface said, read them into a three-element array, and keyed the command buffers, the fences and the descriptor-heap copy holding "its" swapchain image by the acquired image index. A swapchain of another 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 (#160). Mesa's MAILBOX needs four images: Turnip on phoc, llvmpipe on any Wayland compositor. - The present mode is picked first, then the swapchain is sized by SwapchainCapabilities: the surface's counts for that mode where the instance has surface_maintenance1. With swapchain_maintenance1 the swapchain names its mode (VkSwapchainPresentModesCreateInfoKHR), which is what makes the mode's own count the one that applies, at creation and to the forward progress of a UINT64_MAX acquire; without it the stricter of the mode's and the surface's counts for all modes. It asks for numFrames or that minimum where more, never past the maximum (SwapchainMinImageCount). Turnip's FIFO keeps its three images and MAILBOX gets four. The FIFO fallback and presentModeUnfit are gone. - images, imageViewInfos (was imageViews: they are create-infos), the made imageViews that descriptor sets take, imageInitialised and the render-finished semaphores are per image, as many as the driver made. The views are made once per swapchain. The semaphores only grow, since a present of the old swapchain may still wait on one. - Frames in flight stay numFrames, keyed by currentFrame, frameCounter % numFrames: the command buffer, the fence, the acquire semaphore, the heap copy (or set), RenderingElement3D::tlases, the frameIdx a pass is recorded with. currentImage is the acquired image. currentBuffer is gone, so code that used it stops compiling instead of picking the wrong one; RenderPass::Record's frameIdx keeps its name and is now the frame slot, as RenderPass.cppm says. - The frame's fence is waited before the acquire. The acquire semaphores need no spare then, and a frame waits for exactly the one numFrames before it, which the deletion queue and the staging rings count on and fences keyed by image did not promise once images come back out of order. The swapchain slots are written before the fence is reset. - From the acquire to the present the window is recording: a Resize or SetPresentMode from a listener (a VSync key ticked in onUpdate) takes effect after the present, through RebuildSwapchain, together with a present that found the swapchain out of date. Rebuilding mid-frame used to destroy the image the frame was drawing into; with images sized per swapchain it would also index past the new one's end. DescriptorHeapVulkan - Frames and images pair up differently from frame to frame, so no heap copy can keep one swapchain image. AddSwapchainSlot(slot, window) lists an image slot as that window's swapchain image, and Window::Render writes the image a frame acquired into its window's listed slots in that frame's copy, or its set, before onUpdate, skipping a slot that holds it already. A slot is one window's, so two windows can share a heap without writing each other's slots under each other's frames in flight. A slot listed during a frame its window records is written at once. A slot past the heap's image slots, an empty ImageSlot's 0xFFFF among them, is refused. Freeing a slot unlists it, as does initialising the heap in either binding model. WriteImageSet, the one use of which this replaces, is gone, and with it the per-frame dimension of setImageViews. Device - VK_KHR_surface_maintenance1 (or EXT) and VK_KHR_get_surface_capabilities2 are enabled where the loader has them (surfaceMaintenance1Supported, vkGetPhysicalDeviceSurfaceCapabilities2KHR), and the matching swapchain_maintenance1 with its feature where the device has it (swapchainMaintenance1Supported). - DistinctGpus groups the physical devices by GPU as CRAFTER_GRAPHICS_DEVICE numbers them, for ChooseGpu and tests. UI - UIRenderer lists its output slot instead of writing every copy itself, and again on every resize; the resize listener is gone on both backends. The headers' frameIdx is currentFrame. Examples - VulkanTriangle, Sponza, SponzaBench and RTVolume list their output slot and write only the per-frame TLASes. They wrote imageViews[f] into copy f once, which a resize left pointing at destroyed images. Tests - SwapchainImageCount: SwapchainMinImageCount on the numbers drivers report; every offered mode keeps its mode and gets what the window asks for; every frame presents the colour it drew, read back a texel per frame; a switch and a resize asked for during a frame wait for its present; a slot listed during a frame holds its image at once; two windows on one heap draw their own colours. The frame checks run again in child processes, through CRAFTER_GRAPHICS_DEVICE, on a GPU whose swapchains have more images than frames (this one, else llvmpipe), in each binding model the parent didn't cover; a child skips only for a GPU that can't run the library, and checks it runs the model it was asked for. - FrameLoopSync checks the frame slot follows the frame counter and counts the swapchain's own images. HeapASRead, HeapDynamicIndexHit and RTPassPushData list their swapchain slot and drop their resize rewrites. Docs - README, AGENTS.md and AGENTS.consumer.md: frames and images, the swapchain slots, rebuilds held to the present, the new optional groups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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;
|
|
}
|