From dd89b9e8087948b2d64c4890ae96515af83d74e0 Mon Sep 17 00:00:00 2001 From: Chris Lewis Date: Thu, 21 May 2026 23:19:01 -0700 Subject: [PATCH 1/3] Fix Metal command buffer ownership in command lists 'Clean' closures of recompiled apps (e.g. Zelda64Recomp, MarioKart64 and most importantly Snowboard Kids 2 :P) are being reported as crashes. If we dig into what is going on here: ``` NSZombieEnabled=YES NSDeallocateZombies=NO OBJC_PRINT_EXCEPTIONS=YES ./build/SnowboardKids2Recompiled.app/Contents/MacOS/SnowboardKids2Recompiled ``` We see: ``` -[AGXG14GFamilyCommandBuffer release]: message sent to deallocated instance ``` Looking through usage of mtl, we see that MetalCommandList saves a pointer to the command buffer without retaining a reference to it. My layman's reading of the Metal docs suggest that we do need to call retain if the refence escapes the method call. And sure enough, retaining this instance of mtl appears to fix the crashing issue. --- plume_metal.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plume_metal.cpp b/plume_metal.cpp index fd27894..4a505e5 100644 --- a/plume_metal.cpp +++ b/plume_metal.cpp @@ -2238,7 +2238,9 @@ namespace plume { } MetalCommandList::~MetalCommandList() { - mtl->release(); + if (mtl != nullptr) { + mtl->release(); + } for (auto& fenceSet : fences) { for (auto* fence : fenceSet) { @@ -2253,6 +2255,7 @@ namespace plume { assert(mtl == nullptr); startedEncoding = false; mtl = queue->mtl->commandBufferWithUnretainedReferences(); + mtl->retain(); mtl->setLabel(MTLSTR("RT64 Command List")); // Reset fence waits and updates for new command list. From 371250a36d3793651b8fa9f70e75fc63d2463f0f Mon Sep 17 00:00:00 2001 From: Chris Lewis Date: Thu, 21 May 2026 23:58:44 -0700 Subject: [PATCH 2/3] Retain Metal blit encoder stored on command list Zombie diagnostics reported -[AGXG14GFamilyBlitContext release]: message sent to deallocated instance during shutdown after the command buffer ownership fix landed. This is the same ownership pattern as the previous command buffer fix: blitCommandEncoder returns a non-owned/autoreleased object, but MetalCommandList stores it and later releases it. Retain the encoder when storing it so the later release is balanced. --- plume_metal.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plume_metal.cpp b/plume_metal.cpp index 4a505e5..7d311ce 100644 --- a/plume_metal.cpp +++ b/plume_metal.cpp @@ -3554,6 +3554,7 @@ namespace plume { if (activeBlitEncoder == nullptr) { activeBlitEncoder = mtl->blitCommandEncoder(device->sharedBlitDescriptor); activeBlitEncoder->setLabel(MTLSTR("Copy Blit Encoder")); + activeBlitEncoder->retain(); startedEncoding = true; From 555ed1983906a5ea54aa20a84f6cd35ded27e3bc Mon Sep 17 00:00:00 2001 From: Chris Lewis Date: Fri, 22 May 2026 00:25:13 -0700 Subject: [PATCH 3/3] TextureDescriptor was not owned so it does not need to be released --- plume_metal.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/plume_metal.cpp b/plume_metal.cpp index 7d311ce..302cce2 100644 --- a/plume_metal.cpp +++ b/plume_metal.cpp @@ -1196,8 +1196,6 @@ namespace plume { // Create texture with configured descriptor and alignment MTL::TextureDescriptor *descriptor = MTL::TextureDescriptor::textureBufferDescriptor(pixelFormat, width, options, usage); this->texture = buffer->mtl->newTexture(descriptor, 0, bytesPerRow); - - descriptor->release(); } MetalBufferFormattedView::~MetalBufferFormattedView() {