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) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-19 12:02:22 +02:00
1 parent 042e320559
commit 330be6c29a
5 files changed
+61 -11

No files matched your search

+10
View File
@@ -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<id> deleteList;
bool hasPendingWork = false;
};
FrameData frames_[METAL_MAX_INFLIGHT_FRAMES];
+6 -2
View File
@@ -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) {
+37 -6
View File
@@ -195,7 +195,14 @@ public:
class MetalSamplerState : public SamplerState {
public:
MetalSamplerState(MetalRenderManager *rm) : rm_(rm) {}
~MetalSamplerState() {
rm_->DeferRelease(sampler);
}
id<MTLSamplerState> 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<MTLRenderPipelineState> GetPSO(id<MTLDevice> device, const MetalRenderTargetFormat &format);
@@ -236,6 +247,9 @@ public:
size_t uboSize = 0;
std::string tag;
std::unordered_map<uint32_t, id<MTLRenderPipelineState>> psos;
private:
MetalRenderManager *rm_;
};
id<MTLRenderPipelineState> MetalPipeline::GetPSO(id<MTLDevice> 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<MTLTexture> 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<MTLTexture> color;
id<MTLTexture> depth;
id<MTLTexture> 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";
}
+4 -1
View File
@@ -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();
+4 -2
View File
@@ -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<MTLTexture> tex = (__bridge_transfer id<MTLTexture>)entry->texturePtr;
tex = nil;
if (rm_) {
rm_->DeferRelease(tex);
}
entry->texturePtr = nullptr;
}
}