From a8a6224fd8bc8a7a241bbd6f651cb782804c0198 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 12 Jun 2026 20:58:46 +0000 Subject: [PATCH] fix(encode): bound per-frame size with a tight VBV buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NVENC ran CBR (bit_rate == max_bit_rate, rc=cbr) but never set rc_buffer_size, so it used a loose default VBV. A high-motion P-frame was then allowed to spike to many times the average frame size; the extra packets overflow the depth-2 send queue (newest frame dropped) and the kernel UDP buffer (WouldBlock drops), which the client sees as framedrops/jitter — and on the infinite-GOP GameStream path as old/stale frames flashing until the next RFI. Set a tight ~1-frame VBV (rc_buffer_size = bitrate/fps) so the encoder holds frame size roughly constant and absorbs motion as a momentary QP/quality dip instead — the Sunshine/Moonlight low-latency model. Tunable via PUNKTFUNK_VBV_FRAMES (default 1.0); larger trades burst tolerance for motion quality. Fixes both the punktfunk/1 and GameStream paths (shared encoder). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/punktfunk-host/src/encode/linux.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/punktfunk-host/src/encode/linux.rs b/crates/punktfunk-host/src/encode/linux.rs index a7469ed..091ea66 100644 --- a/crates/punktfunk-host/src/encode/linux.rs +++ b/crates/punktfunk-host/src/encode/linux.rs @@ -160,6 +160,24 @@ impl NvencEncoder { video.set_frame_rate(Some(Rational(fps as i32, 1))); video.set_bit_rate(bitrate_bps as usize); video.set_max_bit_rate(bitrate_bps as usize); + // VBV/HRD buffer — bound the SIZE of any single frame. Under CBR with no buffer set, NVENC + // uses a loose default VBV, so a high-motion P-frame is allowed to balloon to many times the + // average; those extra packets overflow the bounded send queue + kernel socket buffer and + // get dropped, which the client sees as framedrops/jitter (and, on the infinite-GOP path, as + // old/stale frames flashing until the next RFI). A tight ~1-frame buffer makes the encoder + // hold frame size roughly constant and absorb motion as a momentary QP (quality) dip instead + // — the trade we want. Default = 1 frame of bits (bitrate/fps); PUNKTFUNK_VBV_FRAMES tunes it + // (larger = better motion quality but bigger per-frame bursts). + let vbv_frames = std::env::var("PUNKTFUNK_VBV_FRAMES") + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|v| v.is_finite() && *v > 0.0) + .unwrap_or(1.0); + let vbv_bits = + ((bitrate_bps as f64 / fps.max(1) as f64) * vbv_frames as f64).clamp(1.0, i32::MAX as f64); + unsafe { + (*video.as_mut_ptr()).rc_buffer_size = vbv_bits as i32; + } video.set_max_b_frames(0); // Infinite GOP — NO periodic IDR. A keyframe at 5120x1440 is ~20-40x a P-frame, so a // periodic IDR is a recurring multi-millisecond encode+packetize+send spike — the ~2s