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>
This commit is contained in:
parent
050a44d118
commit
621016f264
4 changed files with 254 additions and 30 deletions
|
|
@ -258,8 +258,45 @@ export namespace Crafter {
|
|||
std::array<bool, numFrames> imageInitialised{};
|
||||
std::thread thread;
|
||||
VkCommandBuffer drawCmdBuffers[numFrames];
|
||||
VkSubmitInfo submitInfo;
|
||||
Semaphores semaphores;
|
||||
|
||||
// ── Multi-frame-in-flight synchronisation (issue #40) ──────────────
|
||||
// The renderer keeps up to numFrames frames in flight, so a single
|
||||
// semaphore pair + a per-frame wait-idle is no longer enough. Note
|
||||
// that drawCmdBuffers, the per-frame descriptor heap slots, and the
|
||||
// swapchain images are ALL keyed by the acquired image index
|
||||
// (currentBuffer) — WriteSwapchainDescriptors bakes heap slot i to
|
||||
// write imageViews[i], so the index that selects a command buffer /
|
||||
// heap slot must equal the acquired image index. Everything below
|
||||
// follows from that.
|
||||
|
||||
// Signaled by the queue submit, waited by vkQueuePresentKHR. Keyed by
|
||||
// the acquired IMAGE index: the presentation engine keeps this
|
||||
// semaphore in use until the image is re-acquired, so a per-image
|
||||
// semaphore is the only safe key. Keying it per-CPU-frame trips
|
||||
// VUID-vkQueueSubmit-pSignalSemaphores-00067.
|
||||
// https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html
|
||||
std::array<VkSemaphore, numFrames> renderFinishedSemaphores{};
|
||||
|
||||
// Signaled by the queue submit, waited + reset before
|
||||
// drawCmdBuffers[image] and that image's descriptor-heap slot are
|
||||
// re-recorded. Keyed by image index. Created signaled so the first
|
||||
// wait on each image passes through instead of deadlocking.
|
||||
std::array<VkFence, numFrames> waitFences{};
|
||||
|
||||
// Signaled by vkAcquireNextImageKHR, waited by the queue submit. Keyed
|
||||
// by a free-running CPU frame counter, because the image index isn't
|
||||
// known until acquire returns. Sized numFrames+1: at most numFrames
|
||||
// frames are ever in flight (each image's command buffer is gated by
|
||||
// its own fence, and there are numFrames images), so numFrames+1
|
||||
// distinct acquire semaphores guarantees the one being reused has no
|
||||
// pending operation — required by
|
||||
// VUID-vkAcquireNextImageKHR-semaphore-01779.
|
||||
static constexpr std::uint8_t numAcquireSemaphores = numFrames + 1;
|
||||
std::array<VkSemaphore, numAcquireSemaphores> imageAcquiredSemaphores{};
|
||||
|
||||
// Free-running count of Render() calls; drives the acquire-semaphore
|
||||
// slot (frameCounter % numAcquireSemaphores).
|
||||
std::uint64_t frameCounter = 0;
|
||||
std::uint32_t currentBuffer = 0;
|
||||
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
std::vector<RenderPass*> passes;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue