perf(device): pop retired deletions off the front in O(ready) (#116)

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 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-17 19:37:58 +00:00
commit 2460a66296
2 changed files with 20 additions and 15 deletions

View file

@ -1010,22 +1010,23 @@ 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) {
// 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);
} else {
deletionQueue[kept++] = entry;
deletionQueue.pop_front();
}
}
deletionQueue.resize(kept);
}
void Device::DrainDeletions() {
// Unconditional: the caller has just wait-idled, so no in-flight GPU work

View file

@ -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<PendingDeletion> 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<PendingDeletion> 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);