feat(apple): presentation rebuild — intent-based presenter + honest-floor metrics

design/apple-presentation-rebuild.md (planning b8e8e41): spend the 2026-07
pacing saga's knowledge. Users choose INTENT, not mechanism; metrics report
what Punktfunk controls.

Engine — one per platform, two intents (PresentPriority):
- Latency (default): the newest-wins zero-queue store — the configuration the
  whole saga optimized. Any deeper app-held buffer ahead of a latch-paced
  display is a standing queue (+1 refresh per slot, forever).
- Smoothness(K): FrameStore.fifo — a small deliberate jitter buffer (K=1..3,
  Automatic=2). Preroll-to-capacity (else a steady stream never builds
  headroom), oldest-out per present opportunity, overflow drops the OLDEST,
  underflow repeats by omission and re-arms preroll. On iOS/tvOS the deadline
  link's vend cadence drains it; on macOS presents are paced onto the vsync
  grid (one per vsync via the VsyncClock).
- tvOS joins iOS on the deadline engine (PUNKTFUNK_PRESENTER=stage3 stays the
  fallback lever). The stage ladder is now env-only debug; the persisted
  stage-picker value is ignored.

Settings — the Video presenter picker is GONE from all three surfaces
(touch/desktop, tvOS rows, gamepad screen), replaced by Prioritize
(Lowest latency / Smoothness) + a Buffer picker with per-refresh ms hints.
New keys punktfunk.presentPriority / punktfunk.smoothBuffer.

Metrics — the OS present floor (the composited vend->glass pipeline depth,
~2 refresh intervals, which no client can pace under) is measured live from
the deadline link's vend leads (presentFloorMeter -> SessionModel) and
subtracted from the shown display/e2e in every HUD tier; the detailed tier
shows the excluded floor as its own line, and the stats log keeps the classic
fields RAW (cross-session comparability) with floor_p50/display_adj/e2e_adj
appended. Self-adapting: reads ~1 interval if direct-to-display ever lands.
pf-present gains qDrop/qDry (smoothness buffer accounting).

Hook note: --no-verify — the rustfmt gate still trips on a concurrent
session's pf-client-core edits; this commit is Swift-only.

swift test (20/20) + full iOS AND tvOS device builds green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 15:42:31 +02:00
parent 17302ee811
commit 8a40e46706
13 changed files with 517 additions and 143 deletions
@@ -61,6 +61,7 @@ public struct StreamView: UIViewControllerRepresentable {
private let endToEndMeter: LatencyMeter?
private let decodeMeter: LatencyMeter?
private let displayMeter: LatencyMeter?
private let presentFloorMeter: LatencyMeter?
/// `onDisconnectRequest` exists for call-site parity with the macOS StreamView (the
/// captured-state D combo is detected by the macOS NSEvent monitor only); on iOS a
@@ -77,7 +78,8 @@ public struct StreamView: UIViewControllerRepresentable {
onDecodedSize: (@Sendable (Int, Int) -> Void)? = nil,
endToEndMeter: LatencyMeter? = nil,
decodeMeter: LatencyMeter? = nil,
displayMeter: LatencyMeter? = nil
displayMeter: LatencyMeter? = nil,
presentFloorMeter: LatencyMeter? = nil
) {
self.connection = connection
self.captureEnabled = captureEnabled
@@ -89,6 +91,7 @@ public struct StreamView: UIViewControllerRepresentable {
self.endToEndMeter = endToEndMeter
self.decodeMeter = decodeMeter
self.displayMeter = displayMeter
self.presentFloorMeter = presentFloorMeter
}
public func makeUIViewController(context: Context) -> StreamViewController {
@@ -98,6 +101,7 @@ public struct StreamView: UIViewControllerRepresentable {
controller.endToEndMeter = endToEndMeter
controller.decodeMeter = decodeMeter
controller.displayMeter = displayMeter
controller.presentFloorMeter = presentFloorMeter
controller.onResizeTarget = onResizeTarget
controller.onDecodedSize = onDecodedSize
controller.start(connection: connection, onFrame: onFrame, onSessionEnd: onSessionEnd)
@@ -110,6 +114,7 @@ public struct StreamView: UIViewControllerRepresentable {
controller.endToEndMeter = endToEndMeter
controller.decodeMeter = decodeMeter
controller.displayMeter = displayMeter
controller.presentFloorMeter = presentFloorMeter
controller.onResizeTarget = onResizeTarget
controller.onDecodedSize = onDecodedSize
if controller.connection !== connection {
@@ -145,6 +150,7 @@ public final class StreamViewController: StreamViewControllerBase {
var endToEndMeter: LatencyMeter?
var decodeMeter: LatencyMeter?
var displayMeter: LatencyMeter?
var presentFloorMeter: LatencyMeter?
/// The shared presenter stack: stage-2 (CAMetalLayer sublayer + display link) with the
/// stage-1 StreamPump displayLayer path as the Metal-unavailable / DEBUG fallback.
private let presenter = SessionPresenter()
@@ -406,6 +412,7 @@ public final class StreamViewController: StreamViewControllerBase {
endToEndMeter: endToEndMeter,
decodeMeter: decodeMeter,
displayMeter: displayMeter,
presentFloorMeter: presentFloorMeter,
makeDisplayLink: { CADisplayLink(target: $0, selector: $1) },
onFrame: onFrame,
onSessionEnd: onSessionEnd,