From dc20e4452ec1f1d8025cc180dd38e2c4718cb5aa Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 19 Jul 2026 01:01:59 +0200 Subject: [PATCH] fix(pyrowave): anchor frame-driven pacing to arrival, hold full framerate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frame-driven capture loop set its rate-limit floor (`earliest`) relative to `next`, which is re-based to the instant AFTER submit(). For an async encoder (NVENC) submit() returns in ~0, so that anchor is ~frame-arrival and the loop correctly waits for the next vsync — full framerate. But PyroWave's encode is SYNCHRONOUS (~2 ms inline in submit()), so the anchor lands ~2 ms late every frame: the loop misses the next arrival and samples one interval behind, making the period `interval + encode`. That capped a 240 Hz source at ~158 fps (and a 360 Hz request at ~200) with the link and the encoder both idle — no drops. Anchor the floor to this frame's arrival (`t_cap`) instead. The synchronous encode now overlaps the interval rather than stacking onto it; the ≥0.9×interval spacing from the last grab still caps the rate at ~1.11× target. No-op for async NVENC (t_cap ≈ post-submit there), which is why H.26x already held full rate. Measured on-glass (5120x1440@240, RTX 4090 host, macOS client): desktop now holds 240 fps. Also reduces latency (samples fresher frames). Co-Authored-By: Claude Fable 5 --- crates/punktfunk-host/src/native/stream.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/punktfunk-host/src/native/stream.rs b/crates/punktfunk-host/src/native/stream.rs index 8da1117b..5b12f9c2 100644 --- a/crates/punktfunk-host/src/native/stream.rs +++ b/crates/punktfunk-host/src/native/stream.rs @@ -2041,7 +2041,17 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option session fps); // the +0.5×interval keepalive keeps a static desktop re-encoding (bitrate shape, // client liveness) at 1.5×interval cadence and bounds control-servicing latency. - let earliest = next - interval.mul_f32(0.1); + // + // Anchor the floor to THIS frame's arrival (`t_cap`), not to `next` — `next` is + // re-based to the instant *after* submit(), so a synchronous encoder folds its whole + // encode into the cadence: PyroWave's ~2 ms inline encode pushes the floor out by + // that much, the loop misses the next arrival and samples one interval late, and the + // period becomes interval + encode (≈158 fps off a 240 Hz source; 360 Hz → ~200). + // An async encoder (NVENC) returns from submit in ≈0, so t_cap ≈ post-submit and this + // is a no-op for it — which is why H.26x already holds full rate. Arrival-anchoring + // lets the synchronous encode overlap the interval; the ≥0.9×interval spacing from + // the last grab still caps the rate at ~1.11× target. + let earliest = t_cap + interval.mul_f32(0.9); if let Some(d) = earliest.checked_duration_since(std::time::Instant::now()) { std::thread::sleep(d); }