From 2460a6629668916c5cb592e6ae9410d168889aef Mon Sep 17 00:00:00 2001 From: catbot Date: Wed, 17 Jun 2026 19:37:58 +0000 Subject: [PATCH] perf(device): pop retired deletions off the front in O(ready) (#116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReclaimDeletions ran an O(n) walk + survivor compaction over the whole deletion queue every frame. frameCounter is monotonic, so retireAfter is non-decreasing down the queue and the entries ready to reclaim are always a contiguous prefix. Switch deletionQueue from std::vector to std::deque and pop the ready prefix off the front, stopping at the first entry still in flight — O(ready) instead of O(queue). Resolves #116 Co-Authored-By: Claude Opus 4.8 --- implementations/Crafter.Graphics-Device.cpp | 29 +++++++++++---------- interfaces/Crafter.Graphics-Device.cppm | 6 ++++- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index f96ddc8..8738494 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -1010,21 +1010,22 @@ void Device::EnqueueDeletion(VkBuffer buffer, VkDeviceMemory memory) { } void Device::ReclaimDeletions() { - // Free every entry whose retire frame has been reached, compacting the - // survivors down in place. An entry tagged at frame F retires at - // F + framesInFlight: by the time the CPU has begun that many later - // frames (each gated by a per-image fence wait), single-queue submission - // order guarantees all GPU work from frame F is complete. - std::size_t kept = 0; - for (PendingDeletion& entry : deletionQueue) { - if (entry.retireAfter + framesInFlight <= frameCounter) { - vkDestroyBuffer(device, entry.buffer, nullptr); - vkFreeMemory(device, entry.memory, nullptr); - } else { - deletionQueue[kept++] = entry; - } + // Free every entry whose retire frame has been reached. An entry tagged at + // frame F retires at F + framesInFlight: by the time the CPU has begun that + // many later frames (each gated by a per-image fence wait), single-queue + // submission order guarantees all GPU work from frame F is complete. + // + // frameCounter is monotonic, so retireAfter is non-decreasing down the + // queue and the ready entries are a contiguous prefix: pop them off the + // front and stop at the first one still in flight (everything behind it is + // newer, hence also in flight). O(ready), not O(queue). + while (!deletionQueue.empty() && + deletionQueue.front().retireAfter + framesInFlight <= frameCounter) { + const PendingDeletion& entry = deletionQueue.front(); + vkDestroyBuffer(device, entry.buffer, nullptr); + vkFreeMemory(device, entry.memory, nullptr); + deletionQueue.pop_front(); } - deletionQueue.resize(kept); } void Device::DrainDeletions() { diff --git a/interfaces/Crafter.Graphics-Device.cppm b/interfaces/Crafter.Graphics-Device.cppm index 95f7583..17f77c5 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -320,7 +320,11 @@ export namespace Crafter { // Zero until set, which only makes ReclaimDeletions more eager — and it // is never called before a Window (which sets this) starts rendering. inline static std::uint8_t framesInFlight = 0; - inline static std::vector deletionQueue; + // A deque, not a vector: frameCounter is monotonic, so retireAfter is + // non-decreasing down the queue and the entries ready to reclaim are + // always a prefix. ReclaimDeletions pops that prefix off the front in + // O(ready) instead of walking and compacting the whole queue. + inline static std::deque deletionQueue; // Tag {buffer, memory} for deletion after framesInFlight more frames. // A VK_NULL_HANDLE buffer is ignored (nothing to free). static void EnqueueDeletion(VkBuffer buffer, VkDeviceMemory memory);