fix(apple): default iOS to glass-gated present pacing with a depth-2 gate — the 23 ms display-stage fix
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m52s
release / apple (push) Successful in 8m46s
deb / build-publish (push) Successful in 11m40s
docker / deploy-docs (push) Successful in 24s
arch / build-publish (push) Successful in 12m42s
deb / build-publish-host (push) Successful in 12m6s
android / android (push) Successful in 13m55s
apple / screenshots (push) Successful in 6m36s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m59s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m39s
ci / rust (push) Successful in 24m30s
ci / docs-site (push) Successful in 50s
ci / web (push) Successful in 54s
apple / swift (push) Successful in 1m15s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m52s
release / apple (push) Successful in 8m46s
deb / build-publish (push) Successful in 11m40s
docker / deploy-docs (push) Successful in 24s
arch / build-publish (push) Successful in 12m42s
deb / build-publish-host (push) Successful in 12m6s
android / android (push) Successful in 13m55s
apple / screenshots (push) Successful in 6m36s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m59s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m39s
ci / rust (push) Successful in 24m30s
ci / docs-site (push) Successful in 50s
ci / web (push) Successful in 54s
apple / swift (push) Successful in 1m15s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
Field report (iPad Pro 13" M4, 2752x2064@120): display 23.1 ms on the default arrival pacing vs 14 ms glass-gated, dominating an otherwise ~14 ms pipeline. On iOS the layer ALWAYS vsync-latches (displaySyncEnabled is macOS-only API) and default-on VRR steers the panel to the stream rate, so arrival pacing's sticky-FIFO saturation (~2-3 refreshes of queue) is the common case, not the corner — the exact regime that made tvOS default to glass. - PresenterChoice.platformDefault -> stage3 on iOS/iPadOS (joins tvOS); explicit stage-2 stays the honest arrival A/B; macOS unchanged. - PresentGate generalized to a capacity: depth 1 is bit-identical to before; iOS runs depth 2 (one flip scanning out + one queued for the next latch), so a decoded frame presents immediately and latches the very next vsync instead of serializing on the previous flip's on-glass callback — expected ~5-8 ms at 120 Hz. tvOS keeps depth 1 (proven; A/B first), macOS is pinned to 1 (glass there is the DCP swapID-panic mitigation — serialization is its point). PUNKTFUNK_GATE_DEPTH (1-3) is the on-device A/B lever. - Settings picker derives its "(default)" marker from presenterDefault instead of hardcoding stage-2 as default / stage-3 as experimental. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,14 +50,14 @@ enum SettingsOptions {
|
||||
return options
|
||||
}
|
||||
|
||||
/// The platform's presenter default (mirrors SessionPresenter's platformDefault — tvOS runs
|
||||
/// glass pacing, everything else arrival). Views seed their @AppStorage display from this so
|
||||
/// an untouched picker shows what actually runs.
|
||||
/// The platform's presenter default (mirrors SessionPresenter's platformDefault — tvOS and
|
||||
/// iOS/iPadOS run glass pacing, macOS arrival). Views seed their @AppStorage display from
|
||||
/// this so an untouched picker shows what actually runs.
|
||||
static var presenterDefault: String {
|
||||
#if os(tvOS)
|
||||
"stage3"
|
||||
#else
|
||||
#if os(macOS)
|
||||
"stage2"
|
||||
#else
|
||||
"stage3"
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -412,27 +412,28 @@ extension SettingsView {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Stage-2 (Metal/VTDecompressionSession, present on frame arrival) is the proven default;
|
||||
// stage-3 is the same pipeline with glass-gated present pacing — a user-visible A/B while the
|
||||
// pacing work settles (see Stage2Pipeline's PresentPacing for the queue-saturation rationale).
|
||||
// Stage-1 (compressed video straight to the system layer) stays a DEBUG-only diagnostic — it
|
||||
// freezes hard on a lost HEVC reference.
|
||||
// Stage-2 (Metal/VTDecompressionSession, present on frame arrival) vs stage-3 (the same
|
||||
// pipeline with glass-gated present pacing). The default is per-platform — glass on iOS/tvOS,
|
||||
// whose layers always vsync-latch, arrival on macOS (see Stage2Pipeline's PresentPacing for
|
||||
// the queue-saturation rationale) — so the "(default)" marker rides presenterDefault instead
|
||||
// of a hardcoded row. Stage-1 (compressed video straight to the system layer) stays a
|
||||
// DEBUG-only diagnostic — it freezes hard on a lost HEVC reference.
|
||||
@ViewBuilder var presenterSection: some View {
|
||||
Section {
|
||||
Picker("Presenter", selection: $presenter) {
|
||||
Text("Stage 2 (default)").tag("stage2")
|
||||
Text("Stage 3 (experimental)").tag("stage3")
|
||||
#if DEBUG
|
||||
Text("Stage 1 (debug)").tag("stage1")
|
||||
#endif
|
||||
ForEach(SettingsOptions.presenters, id: \.tag) { option in
|
||||
Text(option.tag == SettingsOptions.presenterDefault
|
||||
? "\(option.label) (default)" : option.label)
|
||||
.tag(option.tag)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Video presenter")
|
||||
} footer: {
|
||||
Text("Stage 2: each frame is shown the moment it's decoded — proven, but on displays "
|
||||
Text("Stage 2: each frame is shown the moment it's decoded — but on displays "
|
||||
+ "running near the stream's frame rate, queued frames can add two to three "
|
||||
+ "refreshes of display latency that never drains. Stage 3: presents are paced "
|
||||
+ "to the display — at most one undisplayed frame in flight, always the freshest, "
|
||||
+ "to the display — a strict cap on undisplayed frames, always the freshest, "
|
||||
+ "dropping late frames instead of queueing them. Watch the statistics overlay's "
|
||||
+ "display time to compare. Applies from the next session.")
|
||||
.font(.geist(12, relativeTo: .caption))
|
||||
|
||||
Reference in New Issue
Block a user