From b935746fe9b81e071e66861060ee7b583fd96871 Mon Sep 17 00:00:00 2001 From: John-Michael Mulesa Date: Mon, 21 Sep 2026 19:07:41 -0400 Subject: [PATCH] Submit an open Vulkan upload batch before releasing deferred destroys VK_Device_WaitUploadBatch only retires a batch that has already been submitted. A batch that is still open has recorded buffer-to-image copies into images the frame slot may be about to release, and the slot fence does not cover it because it is only submitted with the next frame. The texture-reduction reload during InitRenderSystem hits this on the first frame: the images it purges were uploaded into the open batch, the slot's fence starts signalled, and the deferred destroys run before the batch is submitted. MoltenVK encodes copies at submit time, so it dereferences the released image. On an M1 Max this crashed every Vulkan boot, with both the pinned MoltenVK 1.4.1 and 1.4.2. Submit and retire the open batch inside VK_Device_FlushDeferredDestroys when that slot has anything to release, so every caller keeps the rule. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017RU1i6R3gN4cdKRELGiCEc --- src/renderer/Vulkan/VulkanDevice.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/renderer/Vulkan/VulkanDevice.cpp b/src/renderer/Vulkan/VulkanDevice.cpp index 3aa65bb89..0761342fa 100644 --- a/src/renderer/Vulkan/VulkanDevice.cpp +++ b/src/renderer/Vulkan/VulkanDevice.cpp @@ -1614,6 +1614,14 @@ void VK_Device_DeferDestroy( VkImage image, VkImageView view, VkBuffer buffer, V } void VK_Device_FlushDeferredDestroys( int slot ) { + // an upload batch that is still open has recorded copies into images this + // slot may be about to release; the slot fence does not cover it, because + // it is only submitted with the next frame. Submit and retire it first. + if ( vkCtx.uploadBatchOpen && ( vkCtx.numDeferredDestroys[ slot ] > 0 + || vkDeferredDestroyOverflow[ slot ].Num() > 0 ) ) { + VK_Device_FlushUploadBatch(); + VK_Device_WaitUploadBatch(); + } const int fixedCount = vkCtx.numDeferredDestroys[ slot ]; for ( int i = 0; i < fixedCount + vkDeferredDestroyOverflow[ slot ].Num(); i++ ) { const vkDeferredDestroy_t &entry = i < fixedCount