Files
punktfunk/crates/pf-presenter
enricobuehler 3fd0a94528 feat(desktop): present on the host's cadence under smoothness, and let VRR finally be worth something
WP5 of design/presenter-cadence-rework-implementation-plan.md — the desktop
binding of the CadenceClock landed in e207d0d7.

`target != last_target_ns` is gone, and `last_target_ns` with it. That equality
was defect D3 verbatim: one present per latch slot, decided by slot identity
rather than by when the frame was actually due. In its place a frame is served
when its own due time says so — under snapping, `due < next_slot_after(now +
margin)`. That `<` is derived rather than picked: `next_slot_after` is monotone,
so "this frame's target slot is no later than the earliest slot still reachable"
reduces to exactly it.

Desktop is §5's simplest row and the code says so — `decoded_ns` and
`session::now_ns()` are both CLOCK_REALTIME, so `ready_ns` goes in raw and no
conversion appears anywhere in the path. The domain check is still asserted, with
source stamps 56 years away from the present clock.

**VRR stops being merely not-worse.** Where the presenter has MEASURED variable
refresh — the existing `CadenceProbe` verdict, not a capability bit — the snap is
skipped and the frame is presented at its due time under `free_running()` tuning.
`Unknown` reverts to snapping: the absence of a measurement is not a measurement.

Two consequences worth carrying into WP8 rather than discovering there. The
preroll gate STAYS on desktop (WP4 retires Android's; the `smooth_buffer`
reinterpretation is WP7's), so preroll now stacks on top of the cushion and
criterion 3's e2e cost is cushion + up to half a refresh + preroll. And
one-present-per-slot is now `PresentGate`'s job: where present timing is live
that is strictly better, because the gate spaces presents by real glass
completions — but where it is unavailable the gate is inert and a faster-than-
panel stream can submit two presents into one vblank, throttled only by the
blocking acquire. That is already the `latency` intent's behaviour on those
boxes, but it is new for `smooth`, and `.221` Intel Arc / AMD are where to watch.

`wake_timeout` is rewritten as the exact mirror of the decision, including the
branch that matters: before the first glass stamp the clock is unanchored and
`next_slot_after` answers "one period from the query" rather than from a grid, so
the servable instant moves with `now`. Missing that would have parked a session's
opening frames for a refresh they never owed.

The cushion ceiling is one SOURCE frame, from the negotiated stream
`Mode.refresh_hz` — never the panel period. The measured fps was rejected
deliberately: it sags exactly when the transport is struggling, which is when a
ceiling derived from it would start licensing a bigger hold.

⚠ `note_off_cadence` is unused here. Nothing on this path can identify a repeat
or a host-anchored implausible stamp without S1's wire bit, so the plan's R2
stays open for the desktop leg — recorded rather than worked around.

Verified: clippy `--all-targets -D warnings` exit 0 with `Checking pf-presenter`
confirmed present (a warm shared target dir can otherwise print Finished having
compiled nothing), 53 tests pass, and the same clean under `--no-default-features`
because the intake edit sits beside the cfg-gated PyroWave collapse.
2026-08-15 14:24:27 +02:00
..