fix(apple/stage2): disable layer vsync wait to kill fullscreen stutter
apple / swift (push) Failing after 28s
ci / web (push) Failing after 47s
ci / rust (push) Failing after 1m19s
ci / docs-site (push) Failing after 33s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 12s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / deploy-docs (push) Has been skipped
rpm / build-publish (push) Failing after 13s
deb / build-publish (push) Failing after 44s

The experimental stage-2 presenter (CAMetalLayer + display link) stuttered badly
in fullscreen but ran fine windowed. render() runs on the display-link / MAIN
thread and calls layer.nextDrawable(), which blocks that thread until a drawable
frees. With the layer's own displaySyncEnabled left on (default), present also
waits for the hardware vsync, so the block serializes the main thread to the
display — windowed, the WindowServer's looser compositing hides it; fullscreen's
tighter, more-direct path exposes it as judder. (Apple dev-forum guidance:
displaySync off measurably reduces nextDrawable() blocking.)

- displaySyncEnabled = false (macOS-only): the display link is already the per-
  vsync pacing source, so the layer's redundant vsync wait only adds the stall.
- maximumDrawableCount = 3 (explicit): more in-flight headroom before
  nextDrawable() has to block on the main thread.

Swift-only (no core/ABI change → no xcframework rebuild). Validated: swift build;
swift test (39 passed, 0 failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 01:06:58 +02:00
parent 6b5ee9f47b
commit 4be993df87
@@ -80,6 +80,19 @@ public final class MetalVideoPresenter {
layer.pixelFormat = .bgra8Unorm
layer.framebufferOnly = true
layer.isOpaque = true
// Triple-buffer: more in-flight drawables before `nextDrawable()` (called on the
// display-link / MAIN thread) has to block waiting for one to free.
layer.maximumDrawableCount = 3
#if os(macOS)
// The display link already paces exactly one present per vsync. Leaving the layer's
// own vsync wait on means `commandBuffer.present` ALSO blocks for the hardware vsync,
// so `nextDrawable()` stalls the MAIN thread until a drawable frees windowed, the
// WindowServer's looser compositing hides it; FULLSCREEN's tighter, more-direct path
// serializes the main thread to the display and the stall surfaces as bad judder.
// Disabling the layer-level sync lets present return promptly (the display link is the
// pacing source), which is what fixes the fullscreen stutter. macOS-only property.
layer.displaySyncEnabled = false
#endif
self.layer = layer
}