From 780791ed81e6796b2b729d340f5baae73df14f48 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 31 Jul 2026 17:20:08 +0200 Subject: [PATCH] feat(android): the presenter's latch margin starts at zero and earns its width The 2.5 ms submit margin ahead of expected present assumed SurfaceFlinger latches well before the vsync. On-glass (NP3, 120 Hz) that lead is not needed: with the margin at zero every release still made its vsync (paced=0 across full sessions) - meaning the constant was 2.5 ms of pure display latency on every frame, and it was quantizing away exactly the readiness gains the slice-overlap pipeline just bought. e2e on the Linux-host pairing dropped from 22-24 ms to 14-18 ms. The margin is now adaptive instead of assumed: it starts at 0 and only widens (+500 us per 1 Hz window, capped at the old 2.5 ms) when the paced counter shows real latch misses - one-way per stream, so a margin that proved necessary is never re-gambled mid-session. A device that needs lead converges within seconds; a device that doesn't keeps the full win. debug.punktfunk.latch_margin_us (0..=8000) pins the margin for rebuild-free sweeps, and the presenter logs the resolved mode at stream start. Co-Authored-By: Claude Fable 5 --- .../android/native/src/decode/presenter.rs | 64 ++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/clients/android/native/src/decode/presenter.rs b/clients/android/native/src/decode/presenter.rs index 2607b5a9..025947f2 100644 --- a/clients/android/native/src/decode/presenter.rs +++ b/clients/android/native/src/decode/presenter.rs @@ -41,6 +41,31 @@ use super::vsync::VsyncShared; /// signal to widen, not stutter. const LATCH_MARGIN_NS: i64 = 2_500_000; +/// `debug.punktfunk.latch_margin_us` (0..=8000 µs): PIN the submit margin for a sweep — +/// setprop + stream restart, no rebuild. Unset/invalid = `None` = the adaptive default. +fn latch_margin_ns() -> Option { + let mut buf = [0u8; 92]; // PROP_VALUE_MAX + // SAFETY: __system_property_get with a valid name + PROP_VALUE_MAX buffer is always safe. + let n = unsafe { + libc::__system_property_get( + c"debug.punktfunk.latch_margin_us".as_ptr(), + buf.as_mut_ptr().cast(), + ) + }; + if n > 0 { + if let Ok(us) = std::str::from_utf8(&buf[..n as usize]) + .unwrap_or("") + .trim() + .parse::() + { + if (0..=8_000).contains(&us) { + return Some(us * 1_000); + } + } + } + None +} + /// The budget's liveness backstop: a release whose predicted latch never seems to arrive /// (clock glitch, mode switch) force-reopens the budget this long after the release, counted in /// `forced` — reads 0 on healthy systems (Apple's `PresentGate.staleAfter`, same value). @@ -167,10 +192,31 @@ pub(super) struct Presenter { dry: u64, pace_us: Vec, last_flush: Instant, + /// The live submit margin. Starts at 0 (P2e on-glass: SurfaceFlinger latched every + /// release with NO lead on the NP3 — the fixed 2.5 ms was pure display latency) and + /// widens by measurement: `paced` misses in a 1 Hz window push it +500 µs toward + /// [`LATCH_MARGIN_NS`], the pre-sweep known-safe ceiling. A sysprop override PINS it. + margin_ns: i64, + /// `debug.punktfunk.latch_margin_us` was set — the margin is pinned, never adapted. + margin_pinned: bool, } impl Presenter { pub(super) fn new(priority: PresentPriority) -> Presenter { + let pinned = latch_margin_ns(); + let (margin_ns, margin_pinned) = match pinned { + Some(ns) => (ns, true), + None => (0, false), + }; + log::info!( + "presenter: latch margin {}us{}", + margin_ns / 1_000, + if margin_pinned { + " (debug.punktfunk.latch_margin_us pin)" + } else { + " (adaptive — widens on latch misses)" + } + ); Presenter { fifo_capacity: match priority { PresentPriority::Latency => 0, @@ -187,6 +233,8 @@ impl Presenter { dry: 0, pace_us: Vec::with_capacity(256), last_flush: Instant::now(), + margin_ns, + margin_pinned, } } @@ -287,7 +335,7 @@ impl Presenter { return false; } // Release: timeline-timed when the clock has one, ASAP otherwise. - let target = clock.and_then(|c| c.next_target(now_mono_ns, LATCH_MARGIN_NS)); + let target = clock.and_then(|c| c.next_target(now_mono_ns, self.margin_ns)); let released = match target { Some(t) => codec .release_output_buffer_at_time_by_index(frame.index, t.expected_present_ns) @@ -308,7 +356,7 @@ impl Presenter { // two releases pile onto the same vsync) and not the present time itself (a period too // late — it would cap the sustainable rate at roughly half the panel). let reopen_at_ns = target - .map(|t| t.expected_present_ns - LATCH_MARGIN_NS) + .map(|t| t.expected_present_ns - self.margin_ns) .unwrap_or(now_mono_ns + period.unwrap_or(FALLBACK_PERIOD_NS)); self.inflight = Some(InFlight { reopen_at_ns, @@ -386,6 +434,18 @@ impl Presenter { panel_ms, ); self.released = 0; + // Margin adaptation: repeated latch misses in one window (a miss presents a vsync + // late and coalesces the next frame into `paced`) mean this device's SF does need + // lead — widen toward the pre-sweep ceiling. One-way by design: a margin that once + // proved necessary is never re-gambled mid-stream (the next stream restarts at 0). + if !self.margin_pinned && self.paced_drops > 2 && self.margin_ns < LATCH_MARGIN_NS { + self.margin_ns = (self.margin_ns + 500_000).min(LATCH_MARGIN_NS); + log::warn!( + "presenter: {} latch misses in 1s — margin widened to {}us", + self.paced_drops, + self.margin_ns / 1_000 + ); + } self.paced_drops = 0; self.no_budget = 0; self.forced = 0;