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
@@ -102,6 +102,20 @@ final class SessionModel: ObservableObject {
@Published var decodeValid = false
@Published var displayP50Ms = 0.0
@Published var displayValid = false
/// The measured OS present floor (design/apple-presentation-rebuild.md): the deadline
/// engine's vendglass pipeline depth an OS property no client can pace under (~2 refresh
/// intervals composited; would read ~1 under direct-to-display). The HUD subtracts it from
/// the shown display/e2e so the numbers describe Punktfunk's own pipeline; raw values stay
/// in the detailed tier + the stats log. Invalid (0) on macOS arrival (sync-off no floor)
/// and under stage-1.
@Published var osFloorP50Ms = 0.0
@Published var osFloorValid = false
/// The floor-shaved values every HUD tier displays (raw floor, never below 0). Identical
/// to the raw values whenever no floor is measured.
var displayAdjP50Ms: Double { max(0, displayP50Ms - (osFloorValid ? osFloorP50Ms : 0)) }
var endToEndAdjP50Ms: Double { max(0, endToEndP50Ms - (osFloorValid ? osFloorP50Ms : 0)) }
var endToEndAdjP95Ms: Double { max(0, endToEndP95Ms - (osFloorValid ? osFloorP50Ms : 0)) }
/// Unrecoverable network frame drops in the last window (FEC couldn't rebuild them) and their
/// share of frames offered, `lost/(received+lost)`. The HUD hides the line while zero.
@Published var lostFrames = 0
@@ -133,6 +147,9 @@ final class SessionModel: ObservableObject {
let endToEnd = LatencyMeter()
let decodeStage = LatencyMeter()
let displayStage = LatencyMeter()
/// The OS present floor sampler (see `osFloorP50Ms`) fed one sample per display-link
/// update by the deadline engine, drained by the same 1 s tick as the stage meters.
let presentFloor = LatencyMeter()
/// Cumulative reassembler-drop counter at the last stats drain (per-window `lost` delta).
private var lastFramesDropped: UInt64 = 0
private var statsTimer: Timer?
@@ -472,6 +489,7 @@ final class SessionModel: ObservableObject {
endToEndValid = false
decodeValid = false
displayValid = false
osFloorValid = false
lostFrames = 0
lostPct = 0
mouseCaptured = false
@@ -655,15 +673,25 @@ final class SessionModel: ObservableObject {
} else {
self.displayValid = false
}
if let f = self.presentFloor.drain() {
self.osFloorP50Ms = f.p50Ms
self.osFloorValid = true
} else {
self.osFloorValid = false
}
// Mirror the window to the unified log (see statsLog) one line per second,
// stages in ms, only while frames actually flowed. `fps` counts RECEIVED AUs;
// `presents` counts frames that reached glass (the display meter's sample count)
// a presentsfps gap is the presenter dropping/serializing, an fps deficit is
// upstream (host capture/encode or the network).
if frames > 0 {
// The classic fields stay RAW (cross-session comparability with every log
// captured before the 2026-07 floor policy); the appended trio carries the
// measured OS present floor and the floor-shaved values the HUD displays.
let line = String(
format: "fps=%d presents=%d e2e_p50=%.1f e2e_p95=%.1f hostnet_p50=%.1f "
+ "decode_p50=%.1f display_p50=%.1f lost=%d",
+ "decode_p50=%.1f display_p50=%.1f lost=%d "
+ "floor_p50=%.1f display_adj=%.1f e2e_adj=%.1f",
frames,
displayWindow?.count ?? 0,
self.endToEndValid ? self.endToEndP50Ms : -1,
@@ -671,7 +699,10 @@ final class SessionModel: ObservableObject {
self.hostNetworkValid ? self.hostNetworkP50Ms : -1,
self.decodeValid ? self.decodeP50Ms : -1,
self.displayValid ? self.displayP50Ms : -1,
lost)
lost,
self.osFloorValid ? self.osFloorP50Ms : -1,
self.displayValid ? self.displayAdjP50Ms : -1,
self.endToEndValid ? self.endToEndAdjP50Ms : -1)
statsLog.info("\(line, privacy: .public)")
}
}