Skip to content

Serialize per-tile ScreenCaptureKit setup (#18)#20

Merged
peterp merged 2 commits into
mainfrom
pp-fix-18-tile-start-race
May 13, 2026
Merged

Serialize per-tile ScreenCaptureKit setup (#18)#20
peterp merged 2 commits into
mainfrom
pp-fix-18-tile-start-race

Conversation

@peterp
Copy link
Copy Markdown
Owner

@peterp peterp commented May 13, 2026

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

  1. Tile.swift gains a private static serial DispatchQueue and two async helpers, makeFilter and makeStream. Tile.snapshot() and Tile.start() now build the SCContentFilter and the SCStream through 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.
  2. The helpers hop tasks onto the serial queue via withCheckedContinuation. The calling code re-checks the tile's cancelled flag after each hop, so a tile that is stopped while waiting exits cleanly.
  3. StressTest.swift is a new debug-only harness, gated behind a --stress command-line flag in main.swift. It builds many SCContentFilter and SCStream objects concurrently against the real windows on the machine and runs a parallel SCShareableContent refresher. --serialize switches it to the fixed code path; --iterations N controls 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.

peterp added 2 commits May 14, 2026 00:22
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.
@peterp peterp merged commit 9b4ef3c into main May 13, 2026
1 check passed
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