//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Issue #40: multi-frame-in-flight frame pacing. The renderer used to be // effectively single-buffered — Render() ended with an unconditional // vkQueueWaitIdle and shared a single (presentComplete, renderComplete) // semaphore pair with zero fences for the Window's whole lifetime. This // reworks the pacing model: // // - one VkFence per swapchain slot, keyed by the ACQUIRED IMAGE INDEX, // passed to vkQueueSubmit and waited on (then reset) before that image's // command buffer is re-recorded; // - one semaphore pair per in-flight frame, keyed by a free-running CPU // frame counter % numFrames (the image index isn't known until acquire // returns), so frame N's render-complete and frame N+1's acquire can be // pending at once; // - the steady-state vkQueueWaitIdle is gone (kept only on resize / // OUT_OF_DATE / teardown). // // The fence and semaphore changes are mutually load-bearing: dropping the // wait-idle while reusing one binary semaphore pair across in-flight frames is // a textbook Vulkan race, and per-frame semaphores without per-frame fences is // a command-buffer use-after-free. So this test drives the *real* frame loop — // a real Wayland surface, a real swapchain, real acquire/submit/present — for // many more frames than there are in-flight slots, then asserts: // // - every Render() completed and the CPU frame counter advanced by the // expected amount (no deadlock on the per-image fence wait); // - the acquired image index rotated across more than one slot (proof the // swapchain is genuinely multi-buffered, not pinned to image 0); // - the Vulkan validation layer reported ZERO errors over the whole run. // This is the load-bearing check: the old singleton-pair design only // avoided being an active race because the wait-idle masked it. Reusing a // binary semaphore across overlapping frames, or recording into a command // buffer still in flight, both light up the validation layer immediately. // // Needs a live Wayland compositor + a Vulkan device at runtime (same as the // windowed examples), so it shares the native build settings and is Linux-only. #include "vulkan/vulkan.h" #include 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; } } // namespace int main() { Device::Initialize(); // A real, configured window against the running compositor. Construction // performs the xdg configure handshake and creates the swapchain + the // per-frame fences/semaphores under test. Window window(640, 480, "FrameLoopSync test"); // Render() with no passes still exercises the full pacing path: acquire // (signals frameSemaphores[frame].presentComplete) → per-image fence // wait+reset → barriers → submit (signals renderComplete, fences the // command buffer) → present. Run well past numFrames so every semaphore // slot and every per-image fence is reused several times — exactly the // condition the old single-pair / wait-idle design could not survive // without draining the queue every frame. constexpr int kFrames = 60; static_assert(kFrames > Window::numFrames * 3, "must loop enough to reuse each in-flight slot multiple times"); std::array imageSeen{}; for (int i = 0; i < kFrames; ++i) { window.Render(); if (window.currentBuffer < Window::numFrames) { imageSeen[window.currentBuffer] = true; } } // Drain before inspecting — no steady-state wait-idle means GPU work may // still be in flight when the loop exits. Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); Check(window.frameCounter == static_cast(kFrames), std::format("CPU frame counter advanced once per Render() ({} of {})", window.frameCounter, kFrames)); int distinctImages = 0; for (bool seen : imageSeen) if (seen) ++distinctImages; Check(distinctImages > 1, std::format("swapchain rotated across multiple images ({} of {} slots used)", distinctImages, Window::numFrames)); Check(Device::validationErrorCount == 0, std::format("no Vulkan validation errors across {} frames ({} seen)", kFrames, Device::validationErrorCount)); if (failures != 0) { std::println("{} check(s) failed", failures); return EXIT_FAILURE; } std::println("all checks passed"); return EXIT_SUCCESS; }