From 330be6c29acfb505fd11f22c9f573f54ea52886f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 17 Oct 2026 20:55:42 +0200 Subject: [PATCH] Metal: Don't let command buffers retain resources Resources that the GPU may still use are now released through the render manager's per-frame list, freed once the frame's work completes, instead of command buffers retaining everything that's bound. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/GPU/Metal/MetalRenderManager.h | 10 ++++++ Common/GPU/Metal/MetalRenderManager.mm | 8 +++-- Common/GPU/Metal/thin3d_metal.mm | 43 ++++++++++++++++++++++---- GPU/Metal/DrawEngineMetal.mm | 5 ++- GPU/Metal/TextureCacheMetal.mm | 6 ++-- 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/Common/GPU/Metal/MetalRenderManager.h b/Common/GPU/Metal/MetalRenderManager.h index 0b98f15f81..45f91634d0 100644 --- a/Common/GPU/Metal/MetalRenderManager.h +++ b/Common/GPU/Metal/MetalRenderManager.h @@ -162,6 +162,15 @@ public: // Waits for all submitted work to complete. void WaitIdle(); + // Command buffers don't retain what they use (that costs a retain and release per binding), so + // anything that may still be in use by the GPU has to be released through this. Keeps the object + // alive until the GPU is done with the current frame. + void DeferRelease(id object) { + if (object) { + frames_[curFrame_].deleteList.push_back(object); + } + } + // Backbuffer void Resize(int width, int height); int BackbufferWidth() const { return bbWidth_; } @@ -249,6 +258,7 @@ private: struct FrameData { MetalPushBuffer push; + std::vector deleteList; bool hasPendingWork = false; }; FrameData frames_[METAL_MAX_INFLIGHT_FRAMES]; diff --git a/Common/GPU/Metal/MetalRenderManager.mm b/Common/GPU/Metal/MetalRenderManager.mm index 5152878af7..9fb1b93593 100644 --- a/Common/GPU/Metal/MetalRenderManager.mm +++ b/Common/GPU/Metal/MetalRenderManager.mm @@ -267,6 +267,7 @@ void MetalRenderManager::CreateOffscreenBackbuffer(int width, int height) { MTLTextureDescriptor *desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:width height:height mipmapped:NO]; desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; desc.storageMode = MTLStorageModePrivate; + DeferRelease(offscreenBackbuffer_); offscreenBackbuffer_ = [device_ newTextureWithDescriptor:desc]; offscreenBackbuffer_.label = @"offscreen_backbuffer"; bbWidth_ = width; @@ -274,6 +275,7 @@ void MetalRenderManager::CreateOffscreenBackbuffer(int width, int height) { } void MetalRenderManager::CreateBackbufferDepth() { + DeferRelease(bbDepth_); if (bbWidth_ <= 0 || bbHeight_ <= 0) { bbDepth_ = nil; return; @@ -338,10 +340,10 @@ void MetalRenderManager::NewCommandBuffers() { return; } // The upload buffer is enqueued first, so it runs first even though it's committed at the same time. - uploadCmd_ = [queue_ commandBuffer]; + uploadCmd_ = [queue_ commandBufferWithUnretainedReferences]; uploadCmd_.label = @"upload"; [uploadCmd_ enqueue]; - cmd_ = [queue_ commandBuffer]; + cmd_ = [queue_ commandBufferWithUnretainedReferences]; cmd_.label = @"main"; [cmd_ enqueue]; } @@ -370,6 +372,7 @@ void MetalRenderManager::Present(bool vsync) { if (drawable_) { [cmd_ presentDrawable:drawable_]; + DeferRelease(drawable_); drawable_ = nil; } @@ -401,6 +404,7 @@ void MetalRenderManager::Present(bool vsync) { curFrame_ = (int)(frameNumber_ % METAL_MAX_INFLIGHT_FRAMES); dispatch_semaphore_wait(frameSemaphore_, DISPATCH_TIME_FOREVER); frames_[curFrame_].push.Reset(); + frames_[curFrame_].deleteList.clear(); } void MetalRenderManager::Flush(bool wait) { diff --git a/Common/GPU/Metal/thin3d_metal.mm b/Common/GPU/Metal/thin3d_metal.mm index 229ee736ee..11a9727236 100644 --- a/Common/GPU/Metal/thin3d_metal.mm +++ b/Common/GPU/Metal/thin3d_metal.mm @@ -195,7 +195,14 @@ public: class MetalSamplerState : public SamplerState { public: + MetalSamplerState(MetalRenderManager *rm) : rm_(rm) {} + ~MetalSamplerState() { + rm_->DeferRelease(sampler); + } id sampler; + +private: + MetalRenderManager *rm_; }; class MetalInputLayout : public InputLayout { @@ -217,10 +224,14 @@ public: class MetalPipeline : public Pipeline { public: + MetalPipeline(MetalRenderManager *rm) : rm_(rm) {} ~MetalPipeline() { if (vs) vs->Release(); if (fs) fs->Release(); delete[] ubo; + for (auto &iter : psos) { + rm_->DeferRelease(iter.second); + } } id GetPSO(id device, const MetalRenderTargetFormat &format); @@ -236,6 +247,9 @@ public: size_t uboSize = 0; std::string tag; std::unordered_map> psos; + +private: + MetalRenderManager *rm_; }; id MetalPipeline::GetPSO(id device, const MetalRenderTargetFormat &format) { @@ -312,7 +326,7 @@ public: class MetalTexture : public Texture { public: - MetalTexture(const TextureDesc &desc) { + MetalTexture(MetalRenderManager *rm, const TextureDesc &desc) : rm_(rm) { width_ = desc.width; height_ = desc.height; depth_ = desc.depth; @@ -320,14 +334,21 @@ public: mipLevels_ = desc.mipLevels; } + ~MetalTexture() { + rm_->DeferRelease(texture); + } + id texture; int mipLevels_ = 1; uint64_t lastUsedFrame = (uint64_t)-1; + +private: + MetalRenderManager *rm_; }; class MetalFramebuffer : public Framebuffer { public: - MetalFramebuffer(const FramebufferDesc &desc) : tag_(desc.tag ? desc.tag : "") { + MetalFramebuffer(MetalRenderManager *rm, const FramebufferDesc &desc) : rm_(rm), tag_(desc.tag ? desc.tag : "") { width_ = desc.width; height_ = desc.height; layers_ = 1; @@ -350,9 +371,18 @@ public: return stencilView; } + ~MetalFramebuffer() { + rm_->DeferRelease(color); + rm_->DeferRelease(depth); + rm_->DeferRelease(stencilView); + } + id color; id depth; id stencilView; + +private: + MetalRenderManager *rm_; std::string tag_; }; @@ -692,7 +722,7 @@ SamplerState *MetalDrawContext::CreateSamplerState(const SamplerStateDesc &desc) } sd.lodMinClamp = 0.0f; sd.lodMaxClamp = FLT_MAX; - MetalSamplerState *ss = new MetalSamplerState(); + MetalSamplerState *ss = new MetalSamplerState(rm_); ss->sampler = [device_ newSamplerStateWithDescriptor:sd]; return ss; } @@ -735,7 +765,7 @@ ShaderModule *MetalDrawContext::CreateShaderModule(ShaderStage stage, ShaderLang } Pipeline *MetalDrawContext::CreateGraphicsPipeline(const PipelineDesc &desc, const char *tag) { - MetalPipeline *pipeline = new MetalPipeline(); + MetalPipeline *pipeline = new MetalPipeline(rm_); pipeline->tag = tag; for (ShaderModule *module : desc.shaders) { MetalShaderModule *mm = (MetalShaderModule *)module; @@ -825,7 +855,7 @@ Texture *MetalDrawContext::CreateTexture(const TextureDesc &desc) { ERROR_LOG(Log::G3D, "Metal: Unsupported texture type %d", (int)desc.type); return nullptr; } - MetalTexture *tex = new MetalTexture(desc); + MetalTexture *tex = new MetalTexture(rm_, desc); bool generateMips = desc.generateMips && (GetDataFormatSupport(desc.format) & FMT_AUTOGEN_MIPS); int mipLevels = std::max(1, desc.mipLevels); if (desc.generateMips && mipLevels == 1) { @@ -904,7 +934,7 @@ void MetalDrawContext::UpdateTextureRegions(Texture *texture, int level, const T Framebuffer *MetalDrawContext::CreateFramebuffer(const FramebufferDesc &desc) { _dbg_assert_(desc.numLayers == 1); - MetalFramebuffer *fb = new MetalFramebuffer(desc); + MetalFramebuffer *fb = new MetalFramebuffer(rm_, desc); MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:desc.width height:desc.height mipmapped:NO]; td.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; td.storageMode = MTLStorageModePrivate; @@ -1023,6 +1053,7 @@ bool MetalDrawContext::CopyFramebufferToMemory(Framebuffer *srcfb, Aspect aspect int srcStride = w * srcPixelSize; size_t size = (size_t)srcStride * h; if (!readbackBuffer_ || readbackBuffer_.length < size) { + rm_->DeferRelease(readbackBuffer_); readbackBuffer_ = [device_ newBufferWithLength:std::max(size, (size_t)(1024 * 1024)) options:MTLResourceStorageModeShared]; readbackBuffer_.label = @"readback"; } diff --git a/GPU/Metal/DrawEngineMetal.mm b/GPU/Metal/DrawEngineMetal.mm index 42c2e7ea8f..769aaf5da5 100644 --- a/GPU/Metal/DrawEngineMetal.mm +++ b/GPU/Metal/DrawEngineMetal.mm @@ -188,7 +188,10 @@ void DrawEngineMetal::DeviceRestore(Draw::DrawContext *draw) { } void DrawEngineMetal::ClearPipelines() { - pipelines_.Iterate([](const MetalPipelineKey &, Pipeline *pipeline) { + pipelines_.Iterate([&](const MetalPipelineKey &, Pipeline *pipeline) { + if (rm_) { + rm_->DeferRelease(pipeline->pso); + } delete pipeline; }); pipelines_.Clear(); diff --git a/GPU/Metal/TextureCacheMetal.mm b/GPU/Metal/TextureCacheMetal.mm index f3c7579bc3..c3218c8ef6 100644 --- a/GPU/Metal/TextureCacheMetal.mm +++ b/GPU/Metal/TextureCacheMetal.mm @@ -75,9 +75,11 @@ void TextureCacheMetal::DeviceRestore(Draw::DrawContext *draw) { void TextureCacheMetal::ReleaseTexture(TexCacheEntry *entry, bool delete_them) { if (entry->texturePtr) { - // Balances the __bridge_retained in BuildTexture. Command buffers that use it keep it alive as needed. + // Balances the __bridge_retained in BuildTexture. The GPU may still be using it. id tex = (__bridge_transfer id)entry->texturePtr; - tex = nil; + if (rm_) { + rm_->DeferRelease(tex); + } entry->texturePtr = nullptr; } }