Serialize per-tile ScreenCaptureKit setup (#18)#20
Merged
Conversation
Concurrent Tile.start() calls have crashed inside -[SCContentFilter initWithDesktopIndependentWindow:] with objc_retain on a stale SCWindow while another tile's SCStream init was deep in SCContentFilter copyWithZone. Funnel the synchronous setup through a single private queue so the framework never sees overlapping inits; async startCapture / captureImage still overlap. Adds a debug-only --stress harness (also gated --serialize / --iterations) for confirmation on macOS 26, where the original crash was reported.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On macOS 26, cmdcmd has been crashing when the overlay opens — sometimes on the first activation of the session (issue #18). The crash report points at
SCContentFilter, the setup object that Apple's screen-capture framework (ScreenCaptureKit) builds for each window we want to display as a tile. The overlay creates one of these per visible window at the same time, and the framework's setup code is not safe to call from multiple threads at once. Under that load, one thread tries to retain a window reference while another thread is in the middle of copying it, and the process dies.The reporter was on slightly slower hardware; on a faster machine the timing window for the race shrinks, which is likely why this hasn't been seen before.
Solution
Route the small, fast, synchronous part of the capture setup through a single private queue, so only one tile is in that code at a time. Everything that comes after — taking a single screenshot, and starting the live capture stream — still runs in parallel, so the overlay still appears just as quickly.
To confirm the fix on the affected operating system, this change also adds a debug-only stress mode. It runs the same concurrent setup pattern in a tight loop, optionally with the fix turned on, so the behaviour can be checked on a macOS 26 machine.
Technical Summary
Tile.swiftgains a private static serialDispatchQueueand two async helpers,makeFilterandmakeStream.Tile.snapshot()andTile.start()now build theSCContentFilterand theSCStreamthrough these helpers instead of constructing them inline, so every framework setup call across all tiles runs one at a time. The async parts (startCapture,captureImage) still run in parallel, so visible startup latency is unchanged.withCheckedContinuation. The calling code re-checks the tile'scancelledflag after each hop, so a tile that is stopped while waiting exits cleanly.StressTest.swiftis a new debug-only harness, gated behind a--stresscommand-line flag inmain.swift. It builds manySCContentFilterandSCStreamobjects concurrently against the real windows on the machine and runs a parallelSCShareableContentrefresher.--serializeswitches it to the fixed code path;--iterations Ncontrols run length. The harness could not trigger the crash locally on macOS 15.7.4 even at 8,400+ concurrent attempts, which is expected — the race appears to be specific to (or much wider on) macOS 26's framework code path.