From b092bc2e9007f0399a61379ab60e5c2cfe41fceb Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 3 Apr 2026 19:54:54 -0700 Subject: [PATCH] fix(rendering): use deferAfterAllFrameFences for bone destruction destroyInstanceBones loops over both frame slots (i=0,1), deferring both boneSet[0] and boneSet[1] to the current frame's fence. When currentFrame=0, boneSet[1] is freed after only slot 0's fence completes while slot 1's command buffer may still be using it. Switch both M2Renderer and CharacterRenderer bone destruction from deferAfterFrameFence to deferAfterAllFrameFences to ensure all in-flight frames have completed before freeing cross-slot resources. --- src/rendering/character_renderer.cpp | 4 +++- src/rendering/m2_renderer.cpp | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index fb04ca7b..12765800 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -520,8 +520,10 @@ void CharacterRenderer::destroyInstanceBones(CharacterInstance& inst, bool defer vmaDestroyBuffer(alloc, boneBuf, boneAlloc); } } else if (boneSet != VK_NULL_HANDLE || boneBuf) { + // Loop destroys bone sets for ALL frame slots — the other slot's + // command buffer may still be in flight. Wait for all fences. VkDescriptorPool pool = boneDescPool_; - vkCtx_->deferAfterFrameFence([device, alloc, pool, boneSet, boneBuf, boneAlloc]() { + vkCtx_->deferAfterAllFrameFences([device, alloc, pool, boneSet, boneBuf, boneAlloc]() { if (boneSet != VK_NULL_HANDLE && pool != VK_NULL_HANDLE) { VkDescriptorSet s = boneSet; vkFreeDescriptorSets(device, pool, 1, &s); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 456acb7e..d87a6844 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -867,10 +867,11 @@ void M2Renderer::destroyInstanceBones(M2Instance& inst, bool defer) { vmaDestroyBuffer(alloc, boneBuf, boneAlloc); } } else if (boneSet != VK_NULL_HANDLE || boneBuf) { - // Deferred destruction — previous frame's command buffer may still - // reference these descriptor sets and buffers. + // Deferred destruction — the loop destroys bone sets for ALL frame + // slots, so the other slot's command buffer may still be in flight. + // Must wait for all fences, not just the current frame's. VkDescriptorPool pool = boneDescPool_; - vkCtx_->deferAfterFrameFence([device, alloc, pool, boneSet, boneBuf, boneAlloc]() { + vkCtx_->deferAfterAllFrameFences([device, alloc, pool, boneSet, boneBuf, boneAlloc]() { if (boneSet != VK_NULL_HANDLE) { VkDescriptorSet s = boneSet; vkFreeDescriptorSets(device, pool, 1, &s);