From 0f333460ec5603d9edfa09f9782369a25bcc4aea Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 11 Jun 2026 13:28:02 +0000 Subject: [PATCH] =?UTF-8?q?fix(core):=20grow=20UDP=20socket=20buffers=20?= =?UTF-8?q?=E2=80=94=20fixes=204K/5K=20video=20freezing=20on=20one=20frame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data-plane UDP sockets used the OS default buffer (~208 KB on Linux, similar on macOS), which is smaller than a single high-resolution frame burst: a 5120×1440 keyframe is ~130 packets the encode|send thread hands to sendmmsg at once. The burst overflows the buffer — EAGAIN on the host send (now dropped, was fatal) or a silent drop on the client recv — and because the data plane runs infinite-GOP, one lost frame breaks every subsequent reference and the decode freezes on the last good frame until an RFI refresh that may never catch up. Symptom: connect at 5120×1440, see ONE frame, then a frozen image (audio + input keep working — those ride QUIC, not this socket). Set SO_SNDBUF/SO_RCVBUF to 8 MB (clamped by the OS to net.core.{w,r}mem_max on Linux / kern.ipc.maxsockbuf on macOS); warn if the grant lands far below target so an undersized host is diagnosable. The client side matters most — the SAME UdpTransport backs the Apple client's data plane via the C ABI, and macOS grants multi-MB buffers without any sysctl, so a rebuilt client stops losing frames. Validated live, bazzite→client at 5120×1440: was 1319/1500 frames (12% loss → freeze), now 1500/1500 @60 and 5279/5279 @240 (split-encode active), zero mismatches, p50 1.9–3.4 ms. Host send buffer was still capped at 416 KB and lost nothing — the loss was purely the client recv buffer. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 1 + crates/punktfunk-core/Cargo.toml | 1 + crates/punktfunk-core/src/transport/udp.rs | 30 ++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 783f07f..169c09a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1978,6 +1978,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha2", + "socket2", "spake2", "thiserror 2.0.18", "tokio", diff --git a/crates/punktfunk-core/Cargo.toml b/crates/punktfunk-core/Cargo.toml index 4b4609d..1c9d74e 100644 --- a/crates/punktfunk-core/Cargo.toml +++ b/crates/punktfunk-core/Cargo.toml @@ -31,6 +31,7 @@ fec-rs = { path = "vendor/fec-rs" } aes-gcm = "0.10" # AES-128-GCM session crypto, matches GameStream zerocopy = { version = "0.8", features = ["derive"] } bytes = "1" +socket2 = "0.6" # set SO_SNDBUF/SO_RCVBUF — default UDP buffers are too small for 4K/5K frame bursts thiserror = "2" tracing = { version = "0.1", default-features = false, features = ["std"] } rand = "0.9" diff --git a/crates/punktfunk-core/src/transport/udp.rs b/crates/punktfunk-core/src/transport/udp.rs index 74119f3..d31f793 100644 --- a/crates/punktfunk-core/src/transport/udp.rs +++ b/crates/punktfunk-core/src/transport/udp.rs @@ -19,14 +19,44 @@ pub struct UdpTransport { } impl UdpTransport { + /// Target kernel socket-buffer size. A high-resolution frame is a burst (a 5120×1440 + /// keyframe is ~130 packets the send thread hands to `sendmmsg` at once); the default + /// UDP buffer (~208 KB on Linux) overflows on it, which EAGAINs the host send (dropping + /// packets) or drops on the client recv — and with infinite-GOP a single lost frame + /// freezes the decode until the next RFI refresh. Requested large; the OS clamps to + /// `net.core.{wmem,rmem}_max` (Linux) / `kern.ipc.maxsockbuf` (macOS). + const TARGET_SOCKBUF: usize = 8 * 1024 * 1024; + /// Bind `local` and `connect` to `peer`, so `send`/`recv` need no address and the /// kernel filters to this peer. Non-blocking, matching the [`Transport`] contract. pub fn connect(local: &str, peer: &str) -> std::io::Result { let socket = UdpSocket::bind(local)?; socket.connect(peer)?; + Self::grow_buffers(&socket); socket.set_nonblocking(true)?; Ok(UdpTransport { socket }) } + + /// Best-effort grow of SO_SNDBUF/SO_RCVBUF (see [`TARGET_SOCKBUF`]). A failure isn't fatal + /// (the stream just runs lossier); a grant far below the request means the OS cap is too + /// low for clean 4K/5K streaming, so warn once with the knob to raise. + fn grow_buffers(socket: &UdpSocket) { + let sock = socket2::SockRef::from(socket); + let _ = sock.set_send_buffer_size(Self::TARGET_SOCKBUF); + let _ = sock.set_recv_buffer_size(Self::TARGET_SOCKBUF); + // The kernel reports back the (possibly clamped, Linux-doubled) granted size. + let granted = sock + .send_buffer_size() + .unwrap_or(0) + .min(sock.recv_buffer_size().unwrap_or(0)); + if granted < Self::TARGET_SOCKBUF / 4 { + tracing::warn!( + granted_kb = granted / 1024, + "UDP socket buffer capped well below target — high-resolution streaming may drop \ + frames; raise net.core.wmem_max / net.core.rmem_max (Linux) for clean 4K/5K" + ); + } + } } impl Transport for UdpTransport {