Commit Graph

4 Commits

Author SHA1 Message Date
enricobuehler ab2bcc8e68 fix(apple): stage-4 deadline presenter (CAMetalDisplayLink) — iOS default; gate depth back to 1
Field verdict on the depth-2 gate (M4 iPad Pro, 2752x2064@120): display stage
22-28 ms vs depth-1's 14 — a REGRESSION, not the predicted 5-8. Post-mortem:
any bounded-FIFO pacing keeps a STANDING queue on the always-vsync-latch
platforms. One burst fills every admitted slot and, with arrivals and latches
then running at the same rate, occupancy never returns to zero — each gate
slot costs one full refresh, permanently (the ladder fits exactly: arrival ~3
slots -> 30+ ms, depth 2 -> 22-28, depth 1 -> 14). A bounded FIFO caps the
queue; nothing ever drains it. And depth 1 serializes presents on the on-glass
callback's delivery lag, so neither rung can approach the sub-refresh floor.

Stage-4 (PresentPacing.deadline) inverts drawable ownership instead:
- A CAMetalDisplayLink on its own runloop thread vends ONE deadline-timed
  drawable per refresh (preferredFrameLatency 1) into a newest-wins LatestBox;
  an unpresented vend is replaced by the next (back to the pool).
- The render thread pairs it with the newest decoded frame the moment either
  half arrives — the common case presents a frame INSTANTLY into an already-
  vended drawable, latching the upcoming refresh. No image queue can form and
  nothing waits on on-glass callbacks (they only feed the meters now).
- A stashed drawable can lag a mid-session HDR reconfigure by one vend:
  encodePresent skips the mismatched-format vend (frame re-rings) instead of
  tripping Metal validation.
- iOS/iPadOS defaults to stage-4; tvOS keeps stage-3 until its own A/B
  (PUNKTFUNK_PRESENTER=stage4); macOS resolves "stage4" back to its default
  (the sync-off/DCP-panic saga — deadline pacing lands there deliberately or
  not at all). Settings picker gains Stage 4 with the derived default marker.
- Gate depth defaults to 1 everywhere again; PUNKTFUNK_GATE_DEPTH stays only
  to reproduce the standing-queue ladder on-device.
- PUNKTFUNK_PRESENT_DEBUG gains latchMs p50/max (present-issue -> on-glass:
  standing queue reads ~n x period, healthy reads < 1 period) + noDrawable=.

swift test (14/14) + full Punktfunk-iOS device build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:34:26 +02:00
enricobuehler 2149673f89 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
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>
2026-07-19 12:42:34 +02:00
enricobuehler 9aebc3f251 fix(apple): default macOS PyroWave sessions to glass-gated present pacing — DCP swapID kernel-panic mitigation
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 16s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
ci / bench (push) Successful in 7m29s
docker / deploy-docs (push) Successful in 26s
release / apple (push) Successful in 10m9s
arch / build-publish (push) Successful in 13m2s
android / android (push) Successful in 14m9s
deb / build-publish (push) Successful in 16m41s
apple / screenshots (push) Successful in 6m26s
ci / rust (push) Successful in 20m34s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m12s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m8s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 54s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
apple / swift (push) Successful in 1m15s
decky / build-publish (push) Successful in 20s
Windowed PyroWave sessions were kernel-panicking Macs ("mismatched
swapID's" @UnifiedPipeline.cpp, WindowServer → AppleMobileDisp*-DCP).
The panic is an Apple DCP bug (SketchUp/Unity/240Hz-monitor reports hit
the same signature), but our stage-2 arrival pacing feeds the trigger
pattern: displaySyncEnabled=false out-of-band presents arriving faster
than the compositor latches them in a composited (windowed) session.
PyroWave makes that pattern routine — near-instant Metal wavelet decode
turns network clumps into same-millisecond present bursts, and it is
the codec that sustains stream rates above panel refresh.

Mitigation: PyroWave sessions on macOS now default to the stage-3
PresentGate (one presented-but-undisplayed swap in flight, serialized
on the on-glass callback, 100 ms stale backstop) — the racing pattern
cannot occur. Explicit stage2/stage3 picks (setting or
PUNKTFUNK_PRESENTER) still win, so the arrival-pacing A/B stays honest.
VideoToolbox codecs keep arrival pacing (decode latency spaces their
presents; no panic reports there).

PresenterChoice gains an `explicit` resolver (nil = no user selection)
so the codec-conditional default only applies when the user hasn't
picked a stage; pacing selection is a testable helper carrying the
rationale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:17:48 +02:00
enricobuehler 08694b4026 feat(apple): stage-3 presenter — glass-gated present pacing as a live A/B
ci / web (push) Successful in 52s
apple / swift (push) Successful in 1m11s
ci / docs-site (push) Successful in 1m2s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
ci / bench (push) Successful in 5m47s
docker / deploy-docs (push) Failing after 37s
release / apple (push) Successful in 8m15s
android / android (push) Successful in 12m57s
arch / build-publish (push) Successful in 13m28s
deb / build-publish (push) Successful in 12m15s
apple / screenshots (push) Successful in 5m55s
ci / rust (push) Successful in 17m52s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m22s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m24s
Stage-2's present-on-arrival saturates CAMetalLayer's FIFO image queue
whenever the stream rate runs at the panel's refresh (iOS always
vsync-latches; the macOS 26 compositor latch-paces our out-of-band
presents the same way): one early burst fills the queue to
maximumDrawableCount and — arrivals then matching latches one-for-one —
it never drains. That sticky depth is the measured 29-30 ms display
stage on the 120 Hz ProMotion devices, and the full-queue regime is
where host<->panel clock drift turns into the reported fixed-interval
repeats/drops. The 240 Hz Studio never saturates, which is why it never
showed either symptom.

Stage-3 is the same pipeline with a PresentGate: at most ONE
presented-but-undisplayed drawable in flight; the drawable's presented
handler reopens the gate and re-signals the render thread, so the next
present always takes the freshest newest-wins ring frame — the hidden
queue latency becomes explicit, correct frame drops. A 100 ms stale
fallback force-opens a gate whose handler never fires (the "presents
aren't damage" hazard class) so a pathological system degrades visibly
instead of freezing; the PUNKTFUNK_PRESENT_DEBUG `forced` counter
exposes it (0 on healthy systems).

Selection: the Settings > Display presenter picker now ships in release
builds (stage 2 default / stage 3 experimental; the freeze-prone
stage-1 diagnostic stays DEBUG-only), resolved per session with a
PUNKTFUNK_PRESENTER=stage1|stage2|stage3 env override for CLI A/B. The
pf-present debug line gains gated/forced/inflightMax — inflightMax is
the direct image-queue-depth measurement for the A/B.

Live-verified both ways against a real host at 1080p120: stage-3 holds
120/120 fps with inflightMax=1, forced=0, glass deltas p50 8.33 ms;
stage-2 is behaviorally unchanged (120/120 fps, inflightMax=2 even on
the wired 240 Hz setup — the saturation signal in miniature). Unit
tests cover the gate (one-in-flight, stale force-open, idempotent
release) and the presenter resolution (env override, release stage-1
gating); macOS tests green, iOS/tvOS xcodebuild clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 02:04:42 +02:00