110 lines
4.7 KiB
C++
110 lines
4.7 KiB
C++
//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 <cstdlib>
|
|
|
|
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<bool, Window::numFrames> 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<std::uint64_t>(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;
|
|
}
|