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 {