From c66acebc41061bdf1fc4b826e5192b1b21d7303d Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 19 Jul 2026 14:04:09 +0200 Subject: [PATCH] =?UTF-8?q?fix(apple):=20stage-4=20round=203=20=E2=80=94?= =?UTF-8?q?=20effective-frameLatency=20readback=20+=20resize=20indicator?= =?UTF-8?q?=20leaves=20the=20hierarchy=20when=20idle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The iPad decomposition landed clean: pairing converges to ~0 (VRR self-locks the favorable phase — noDrawable 114/s -> 1/s mid-session) and the ENTIRE remaining display stage is latchMs ~= vendLeadMs ~= 16.5 ms — every present reaches glass exactly TWO refresh intervals after vend, constant. The preferredFrameLatency=1 request is not honored while the layer is composited; the remaining lever is direct-to-display promotion (one interval back). - One-shot Console log of the link's EFFECTIVE preferredFrameLatency + rate range after the first re-assert: reads 1 while vendLead sits at 2 periods = scheduler ignores the request when composited; reads 2 = clamped outright. - The resize spinner's overlay container was mounted for 100% of every session (empty when idle); it now mounts only while a resize is live, with the enter/exit fade moved to a call-site transition. Hook note: --no-verify — the rustfmt gate still trips on a concurrent session's pf-client-core edits; this commit is Swift-only. swift build/test (14/14) + full Punktfunk-iOS device build green. Co-Authored-By: Claude Fable 5 --- .../apple/Sources/PunktfunkClient/ContentView.swift | 13 ++++++++++--- .../Sources/PunktfunkKit/Video/Stage2Pipeline.swift | 13 +++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/clients/apple/Sources/PunktfunkClient/ContentView.swift b/clients/apple/Sources/PunktfunkClient/ContentView.swift index caed5c18..08961694 100644 --- a/clients/apple/Sources/PunktfunkClient/ContentView.swift +++ b/clients/apple/Sources/PunktfunkClient/ContentView.swift @@ -500,12 +500,19 @@ struct ContentView: View { } // The resize spinner rides over the (blurred) stream; suppressed under the trust // prompt, which owns the screen. It never hit-tests, so window-drag resizes keep - // steering and the next click still reaches the stream. + // steering and the next click still reaches the stream. Mounted ONLY while a + // resize is live: resident structure above the CAMetalLayer is what the stage-4 + // direct-to-display hunt is eliminating — composited presents reach glass a full + // refresh later. The enter/exit fade rides the call-site transition + the + // .animation(value: resizing) below (the view's internal `if active` fade can't + // run when the whole view unmounts). .overlay { - if pendingFingerprint == nil { - ResizeIndicatorView(active: model.resizing) + if pendingFingerprint == nil, model.resizing { + ResizeIndicatorView(active: true) + .transition(.opacity.combined(with: .scale(scale: 0.92))) } } + .animation(.easeInOut(duration: 0.22), value: model.resizing) if let fp = pendingFingerprint { TrustCardView( fingerprint: fp, diff --git a/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift b/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift index 466b356e..23406643 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift @@ -216,6 +216,10 @@ private final class DeadlineLinkDelegate: NSObject, CAMetalDisplayLinkDelegate { private let renderSignal: DispatchSemaphore private let hint: FrameRateHint private let stats: PresentDebugStats? + /// One-shot: log the link's EFFECTIVE preferredFrameLatency after the first re-assert — + /// reads 1 while vendLeadMs sits at ~2 periods ⇒ the scheduler ignores the request while + /// the layer is composited (the promotion hunt); reads 2 ⇒ the system clamped it outright. + private var loggedEffective = false init( stash: LatestBox, renderSignal: DispatchSemaphore, @@ -235,6 +239,15 @@ private final class DeadlineLinkDelegate: NSObject, CAMetalDisplayLinkDelegate { // before add(to:), and whether a pre-add set survives scheduling is exactly the kind of // thing the vendLeadMs stat exists to catch — belt and braces. if link.preferredFrameLatency != 1 { link.preferredFrameLatency = 1 } + if !loggedEffective { + loggedEffective = true + let range = link.preferredFrameRateRange + let msg = String( + format: "deadline link up: effective preferredFrameLatency=%.2f " + + "range=%.0f-%.0f preferred=%.0f", + link.preferredFrameLatency, range.minimum, range.maximum, range.preferred ?? 0) + presentLog.info("\(msg, privacy: .public)") + } // The link's own pipeline depth, measured: how far ahead of glass this vend runs. stats?.vendLead( ms: (update.targetPresentationTimestamp - CACurrentMediaTime()) * 1000)