libobs-metal: Wrap blitSwapChains in autoreleasepool to fix mach port leak

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.
This commit is contained in:
alvin1995
2026-09-17 12:43:38 -04:00
committed by Ryan Foster
parent caaa022340
commit 539064599d
+24 -19
View File
@@ -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.