Skip to content

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

Open
alvin1995 wants to merge 1 commit into
obsproject:masterfrom
alvin1995:fix_metal_port_leak
Open

libobs-metal: Wrap blitSwapChains in autoreleasepool to fix mach port leak#13664
alvin1995 wants to merge 1 commit into
obsproject:masterfrom
alvin1995:fix_metal_port_leak

Conversation

@alvin1995

Copy link
Copy Markdown

Description

Wraps the full frame cycle within blitSwapChains() in an autoreleasepool block, covering nextDrawable() → blit → commandBuffer.present(drawable)commandBuffer.commit().

The blitSwapChains() method is driven by a CVDisplayLink callback thread, which has no implicit autorelease pool. CAMetalLayer.nextDrawable() returns autoreleased CAMetalDrawable objects, each backed by an IOSurface with an associated IOSurfaceSharedEventReference mach port. Without a pool, these autoreleased objects are never drained, leaking one IOSurface (and port) per frame.

Additionally, the drawable presentation order is corrected: drawables are now collected and presented via commandBuffer.present(drawable) after encoder.endEncoding() and before commandBuffer.commit(), matching the standard Metal ordering where presentation calls must occur after all encoding is complete but before the command buffer is committed.

Motivation and Context

This primarily manifests when using cross-process rendering via CAContext/CALayerHost (e.g., a source preview rendered in a separate process and composited into the main process). In that scenario swapChain.renderTarget is non-nil, so nextDrawable() is called per frame and the autoreleased drawables accumulate indefinitely, resulting in unbounded mach port growth.

The in-process projector path (OBS app's own preview) is masked because the on-screen compositor drives drawable recycling even without explicit draining — but the leak exists there too and can accumulate under sustained usage.

How Has This Been Tested?

  • Environment: macOS, Apple Silicon, cross-process source preview via CAContext/CALayerHost.
  • Method: Monitored mach ports via lsmp differential snapshots. Before the fix, IOSurfaceSharedEventReference ports grew linearly per frame with the preview window open. After the fix, port count stabilizes at a small upper bound (≈ pool size × swapchain count) and remains flat.
  • Regression check: In-process preview continues to render correctly with no artifacts, no black frames, and no tearing.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read the contributing document.
  • My code has been run through clang-format / swift-format.
  • My code follows the project's style guidelines.
  • My code is not on the master branch.
  • My code has been tested.
  • All commit messages are properly formatted and commits squashed where appropriate.
  • I have included updates to all appropriate documentation (N/A)

… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant