feat(clients/apple): user-configurable Allow VRR (macOS + iOS)
Add an "Allow VRR" toggle (Settings → Display; default on, non-tvOS) that hands the presenter's display link a wide frame-rate RANGE — preferred = the stream rate — so a ProMotion / adaptive-sync display can vary its physical refresh to match the stream. `SessionPresenter.syncFrameRate` now runs on macOS too: on it requests min 24 / max display / preferred stream Hz; off restores the prior behavior (macOS free-runs at the native rate, iOS keeps its 30 Hz floor). A no-op on fixed-refresh displays. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,12 @@ public enum DefaultsKey {
|
||||
/// (lowest latency — the default, OFF). Resolved once per session;
|
||||
/// PUNKTFUNK_PRESENT_MODE=immediate|vsync overrides it for A/B. See Stage2Pipeline's header.
|
||||
public static let vsync = "punktfunk.vsync"
|
||||
/// Allow variable refresh rate: hand the display link a wide frame-rate RANGE (low floor,
|
||||
/// preferred = stream rate) so a ProMotion / adaptive-sync display can vary its physical
|
||||
/// refresh to match the stream. On by default; a no-op on fixed-refresh displays. When off,
|
||||
/// macOS lets the link free-run at the display's native rate and iOS keeps its proven 30 Hz
|
||||
/// floor. Read per session/reconfigure by `SessionPresenter.syncFrameRate`.
|
||||
public static let allowVRR = "punktfunk.allowVRR"
|
||||
/// Request a 10-bit BT.2020 PQ (HDR10) stream. On by default; only takes effect when the host
|
||||
/// has HDR content AND this display supports HDR — otherwise the stream stays 8-bit SDR.
|
||||
public static let hdrEnabled = "punktfunk.hdrEnabled"
|
||||
|
||||
@@ -90,23 +90,37 @@ final class SessionPresenter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask the display link for the stream's own cadence. iOS/tvOS-only: without an explicit
|
||||
/// range, ProMotion devices cap CADisplayLink at 60 Hz (iPhones additionally need
|
||||
/// Hint the display link with the stream's cadence. On iOS/tvOS a range is always required:
|
||||
/// without one, ProMotion devices cap CADisplayLink at 60 Hz (iPhones additionally need
|
||||
/// `CADisableMinimumFrameDurationOnPhone` in Info.plist), so a 120 fps stream would present
|
||||
/// at half rate with the ring silently dropping every other frame. `maximum` allows up to
|
||||
/// 120 so the system MAY tick faster than a sub-120 stream (each extra tick is a near-free
|
||||
/// empty `renderTick`, and presenting on a denser grid shortens the decode→glass wait); the
|
||||
/// macOS NSView link already tracks its display and must NOT be capped to the stream rate.
|
||||
/// at half rate with the ring silently dropping every other frame. `maximum` allows up to 120
|
||||
/// so the system MAY tick faster than a sub-120 stream (each extra tick is a near-free empty
|
||||
/// `renderTick`, and presenting on a denser grid shortens the decode→glass wait).
|
||||
///
|
||||
/// The `allowVRR` setting (default on) widens that hint into a true variable-refresh request:
|
||||
/// `preferred` = the stream rate with a low floor, so a ProMotion / adaptive-sync display can
|
||||
/// drop its physical refresh to match the content. With VRR off we fall back to the proven
|
||||
/// behavior — iOS keeps a 30 Hz floor; macOS leaves the NSView link at its display's native
|
||||
/// rate (it already tracks the display and must NOT be capped to the stream rate).
|
||||
/// Re-applied from `layout` so a mid-session `Reconfigure` picks up a new refresh.
|
||||
private func syncFrameRate(hz: UInt32) {
|
||||
#if !os(macOS)
|
||||
guard hz > 0, let link = stage2Link else { return }
|
||||
let hzF = Float(hz)
|
||||
if link.preferredFrameRateRange.preferred != hzF {
|
||||
link.preferredFrameRateRange = CAFrameRateRange(
|
||||
minimum: min(30, hzF), maximum: max(hzF, 120), preferred: hzF)
|
||||
}
|
||||
let allowVRR = UserDefaults.standard.object(forKey: DefaultsKey.allowVRR) as? Bool ?? true
|
||||
#if os(macOS)
|
||||
// Off: `.default` = the link free-runs at the display's native rate (pre-VRR behavior).
|
||||
// On: request the content rate with a 24 Hz floor — capped at the display, never at the
|
||||
// stream rate, so an adaptive-sync panel can track the stream.
|
||||
let range: CAFrameRateRange = allowVRR
|
||||
? CAFrameRateRange(minimum: min(hzF, 24), maximum: max(hzF, 120), preferred: hzF)
|
||||
: .default
|
||||
#else
|
||||
// A range is mandatory here (see above); VRR only lowers the floor (24 vs 30) so the
|
||||
// panel can drop deeper to match content on a sub-rate or momentarily stalling stream.
|
||||
let floor = allowVRR ? min(hzF, 24) : min(hzF, 30)
|
||||
let range = CAFrameRateRange(minimum: floor, maximum: max(hzF, 120), preferred: hzF)
|
||||
#endif
|
||||
if link.preferredFrameRateRange != range { link.preferredFrameRateRange = range }
|
||||
}
|
||||
|
||||
/// Position the stage-2 metal sublayer aspect-fit in the hosting view (the host streams at the
|
||||
|
||||
Reference in New Issue
Block a user