fix(pyrowave): anchor frame-driven pacing to arrival, hold full framerate
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 <noreply@anthropic.com>
This commit is contained in:
@@ -2041,7 +2041,17 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
// rate at ~1.11× target when the source runs faster (compositor Hz > session fps);
|
// rate at ~1.11× target when the source runs faster (compositor Hz > session fps);
|
||||||
// the +0.5×interval keepalive keeps a static desktop re-encoding (bitrate shape,
|
// 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.
|
// 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()) {
|
if let Some(d) = earliest.checked_duration_since(std::time::Instant::now()) {
|
||||||
std::thread::sleep(d);
|
std::thread::sleep(d);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user