From 4be993df873ddedaa4f6eed6b1702907347d5450 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 13 Jun 2026 01:06:58 +0200 Subject: [PATCH] fix(apple/stage2): disable layer vsync wait to kill fullscreen stutter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Sources/PunktfunkKit/MetalVideoPresenter.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/clients/apple/Sources/PunktfunkKit/MetalVideoPresenter.swift b/clients/apple/Sources/PunktfunkKit/MetalVideoPresenter.swift index 399e1ff..29de835 100644 --- a/clients/apple/Sources/PunktfunkKit/MetalVideoPresenter.swift +++ b/clients/apple/Sources/PunktfunkKit/MetalVideoPresenter.swift @@ -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 }