Crafter.Graphics/tests/FrameLoopSync/main.cpp

126 lines
5.3 KiB
C++
Raw Permalink Normal View History

feat(window): multi-frame-in-flight frame pacing (#40) The renderer was effectively single-buffered despite allocating triple-buffered infrastructure: Render() ended with an unconditional vkQueueWaitIdle, and a single (presentComplete, renderComplete) semaphore pair with zero fences was shared for the Window's lifetime. Rework the pacing model so up to numFrames frames overlap: - Per-swapchain-image VkFence, signaled by the submit and waited+reset before that image's command buffer / descriptor-heap slot is re-recorded. Keyed by acquired image index (drawCmdBuffers, the heap slots, and the swapchain images are all image-indexed — see WriteSwapchainDescriptors). Created signaled so first use passes. - Per-image render-finished (present) semaphore: the presentation engine holds it until the image is re-acquired, so per-image is the only safe key (per-CPU-frame trips VUID-vkQueueSubmit-pSignalSemaphores-00067). - Per-CPU-frame acquire semaphores (image index unknown until acquire returns), sized numFrames+1: in-flight depth is bounded by the per-image fences, so numFrames+1 distinct acquire semaphores guarantee the reused one has no pending op (VUID-vkAcquireNextImageKHR-01779). - Drop the steady-state vkQueueWaitIdle; keep it on resize / OUT_OF_DATE / teardown. Add tests/FrameLoopSync: drives the real frame loop against a live Wayland compositor for 60 frames (>> in-flight slots) and asserts the CPU frame counter advanced, the swapchain rotated across multiple images, and the validation layer stayed silent — the load-bearing check, since the old single-pair design only avoided being an active race because the wait-idle masked it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:37:11 +00:00
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
// 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;
}