From 539064599d550c9ee0e0cd7c7577527113678966 Mon Sep 17 00:00:00 2001 From: alvin1995 Date: Fri, 17 Jul 2026 16:30:23 +0800 Subject: [PATCH] libobs-metal: Wrap blitSwapChains in autoreleasepool to fix mach port leak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CVDisplayLink callback thread driving blitSwapChains() has no implicit autorelease pool. CAMetalDrawable objects returned by nextDrawable() are autoreleased and each holds an IOSurface backed by an IOSurfaceSharedEventReference mach port. Without an autoreleasepool, these drawables are never drained on the display link thread, causing a new IOSurface (and port) to leak every frame when a render target is set (e.g. cross-process preview via CAContext/CALayerHost). Fix by wrapping the full frame cycle (nextDrawable → blit → present → commit) in an autoreleasepool, so autoreleased drawables are reclaimed each frame and returned to the maximumDrawableCount pool. --- libobs-metal/MetalDevice.swift | 43 +++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/libobs-metal/MetalDevice.swift b/libobs-metal/MetalDevice.swift index d3fa017fc..d1684665b 100644 --- a/libobs-metal/MetalDevice.swift +++ b/libobs-metal/MetalDevice.swift @@ -149,38 +149,43 @@ class MetalDevice { func blitSwapChains() { guard swapChains.count > 0 else { return } - guard let commandBuffer = commandQueue.makeCommandBuffer(), - let encoder = commandBuffer.makeBlitCommandEncoder() - else { - return - } - self.swapChainQueue.sync { swapChains = swapChains.filter { $0.discard == false } } - for swapChain in swapChains { - guard let renderTarget = swapChain.renderTarget, let drawable = swapChain.layer.nextDrawable() else { - continue - } - - guard renderTarget.texture.width == drawable.texture.width, - renderTarget.texture.height == drawable.texture.height, - renderTarget.texture.pixelFormat == drawable.texture.pixelFormat + autoreleasepool { + guard let commandBuffer = commandQueue.makeCommandBuffer(), + let encoder = commandBuffer.makeBlitCommandEncoder() else { - continue + return } - autoreleasepool { + var drawablesToPresent: [CAMetalDrawable] = [] + for swapChain in swapChains { + guard let renderTarget = swapChain.renderTarget, let drawable = swapChain.layer.nextDrawable() else { + continue + } + + guard renderTarget.texture.width == drawable.texture.width, + renderTarget.texture.height == drawable.texture.height, + renderTarget.texture.pixelFormat == drawable.texture.pixelFormat + else { + continue + } + encoder.waitForFence(swapChain.fence) encoder.copy(from: renderTarget.texture, to: drawable.texture) + drawablesToPresent.append(drawable) + } + encoder.endEncoding() + + for drawable in drawablesToPresent { commandBuffer.present(drawable) } - } - encoder.endEncoding() - commandBuffer.commit() + commandBuffer.commit() + } } /// Simulates an explicit "clear" command commonly used in OpenGL or Direct3D11 implementations.