Files
punktfunk/crates/punktfunk-core/src/transport/qos.rs
T
enricobuehler da376b3122
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows-host / package (push) Has been cancelled
web-screenshots / screenshots (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
release / apple (push) Has been cancelled
linux-client-screenshots / screenshots (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
arch / build-publish (push) Has been cancelled
android / android (push) Has been cancelled
android-screenshots / screenshots (push) Has been cancelled
fix(android): gate the latency overhaul behind an experimental toggle, default off
The 5dc24a0 low-latency overhaul regressed badly on some phones. Every piece
of it — decoder ranking, per-SoC vendor keys, the async decode loop, pipeline
thread boosts, the ADPF max-performance bias, game-tagged AAudio, DSCP marking,
the Wi-Fi low-latency lock, HDMI ALLM and the forced TV mode switch — now rides
the "Low-latency mode (experimental)" toggle, default OFF. Off restores the
pre-overhaul pipeline byte-for-byte: the sync poll loop, the platform-default
decoder, and the original format keys (standard low-latency + blind Qualcomm
twin + priority=0 + operating-rate=MAX together).

- New pref key (low_latency_mode_experimental): the old key shipped default-ON,
  so any install that ever saved settings persisted true — flipping the default
  under the old key would leave exactly the regressed devices stuck on.
- DSCP is applied at socket creation, so the toggle reaches the transport via
  NativeBridge.nativeSetLowLatencyMode → transport::set_dscp_default, called in
  the connect choke point before nativeConnect; the core DSCP default reverts
  to off everywhere.
- nativeStartAudio(handle, lowLatencyMode) gates AAudio usage=Game.
- VideoDecoders.pickDecoder now skips `.secure` decoder twins and decoders that
  require FEATURE_SecurePlayback: they need a secure surface, and a secure twin
  could out-score its plain sibling (only it advertising FEATURE_LowLatency),
  which black-screens a clear stream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:18:59 +02:00

164 lines
7.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Shared UDP socket tuning for the media planes: send/recv buffer growth + best-effort link-layer
//! QoS.
//!
//! [`grow_socket_buffers`] is the `SO_SNDBUF`/`SO_RCVBUF` growth the native data plane applies; the
//! GameStream video/audio sockets reuse it so they don't go ENOBUFS-bound at high bitrate.
//!
//! [`set_media_qos`] DSCP-tags the latency-sensitive video/audio traffic (+ Linux `SO_PRIORITY`) so a
//! QoS-aware path (Wi-Fi WMM access categories, a managed switch, a shaped uplink) can prioritize it
//! over bulk flows. Mirrors what Apollo/Sunshine tag — DSCP **CS5** for video, **CS6** for audio. It
//! is **opt-in** (`PUNKTFUNK_DSCP=1`, or [`set_dscp_default`] from an embedder — the Android client
//! ties it to its experimental low-latency mode): DSCP can interact badly with some consumer ISPs/routers, and on
//! Windows a plain `IP_TOS` is silently stripped unless a qWAVE policy is active (Apollo uses the
//! qWAVE API there — that port is a follow-up; today this is a no-op on the wire on Windows).
use std::net::UdpSocket;
use std::sync::atomic::{AtomicBool, Ordering};
/// Target kernel socket-buffer size (`SO_SNDBUF`/`SO_RCVBUF`). 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).
///
/// Sized for 1 Gbps+: at ~1.2 Gbps on the wire an 8 MB buffer is only ~49 ms of steady state, and a
/// single multi-MB IDR keyframe (~4 MB ≈ 3300 packets) instantly fills most of it. 32 MB gives ~200 ms
/// of headroom and absorbs a keyframe burst without EAGAIN/ENOBUFS drops. (Paced sending —
/// `punktfunk1.rs::paced_submit` — spreads a big frame's overflow, so this buffer mostly absorbs the
/// immediate microburst rather than a whole unpaced frame.)
pub(crate) const TARGET_SOCKBUF: usize = 32 * 1024 * 1024;
/// Best-effort grow of `SO_SNDBUF`/`SO_RCVBUF` to [`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 with the knob to raise.
pub fn grow_socket_buffers(socket: &UdpSocket) {
let sock = socket2::SockRef::from(socket);
let _ = sock.set_send_buffer_size(TARGET_SOCKBUF);
let _ = sock.set_recv_buffer_size(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 < 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"
);
}
}
/// Media class of a socket — selects the DSCP code point (and Linux `SO_PRIORITY`), matching Apollo's
/// mapping: video = CS5, audio = CS6.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MediaClass {
Video,
Audio,
}
impl MediaClass {
/// DSCP code point (the high 6 bits of the IPv4 TOS / IPv6 traffic-class byte).
const fn dscp(self) -> u32 {
match self {
MediaClass::Video => 40, // CS5
MediaClass::Audio => 48, // CS6
}
}
}
/// Runtime default for DSCP marking when `PUNKTFUNK_DSCP` is unset (see [`set_dscp_default`]).
/// Off unless an embedder opts in — on Wi-Fi, access points commonly map DSCP to WMM access
/// categories (a real airtime-priority win), but wired paths rarely honour it and some bleach or
/// reject marked packets, so it never turns on by itself.
static DSCP_DEFAULT: AtomicBool = AtomicBool::new(false);
/// Opt in to (or back out of) DSCP marking for sockets created from now on. Must be called BEFORE
/// connecting — the tag is applied at socket creation. The Android client ties this to its
/// experimental low-latency mode; `PUNKTFUNK_DSCP` still overrides in either direction.
pub fn set_dscp_default(enabled: bool) {
DSCP_DEFAULT.store(enabled, Ordering::Relaxed);
}
/// Whether DSCP/QoS marking is enabled: `PUNKTFUNK_DSCP` when set (`1`/`true`/`on` forces it on,
/// `0`/`false`/`off` forces it off — e.g. to rule QoS out while debugging a flaky AP), else the
/// [`set_dscp_default`] runtime default.
pub(crate) fn dscp_enabled() -> bool {
match std::env::var("PUNKTFUNK_DSCP").as_deref() {
Ok("1") | Ok("true") | Ok("on") => true,
Ok("0") | Ok("false") | Ok("off") => false,
_ => DSCP_DEFAULT.load(Ordering::Relaxed),
}
}
/// Best-effort: tag `socket`'s outgoing packets for prioritized delivery of its media class. A no-op
/// unless `PUNKTFUNK_DSCP=1`. Every step is best-effort (failures logged at debug, never fatal) — QoS
/// is a nicety, not required for correctness.
///
/// IPv4 only (all current media sockets bind `0.0.0.0`); a v6 socket simply isn't tagged. On Windows
/// the `IP_TOS` set succeeds but the OS doesn't tag the wire without a qWAVE policy (follow-up).
pub fn set_media_qos(socket: &UdpSocket, class: MediaClass) {
if dscp_enabled() {
apply_media_qos(socket, class);
}
}
/// The unconditional QoS application, factored out of [`set_media_qos`] so it is directly testable
/// without touching the process-global `PUNKTFUNK_DSCP` env. Best-effort (every step logs-and-continues).
fn apply_media_qos(socket: &UdpSocket, class: MediaClass) {
let sock = socket2::SockRef::from(socket);
// DSCP occupies the high 6 bits of the TOS byte → shift left 2.
if let Err(e) = sock.set_tos_v4(class.dscp() << 2) {
tracing::debug!(error = %e, ?class, "set IP_TOS (DSCP) failed — QoS marking skipped");
}
// SO_PRIORITY must be set AFTER IP_TOS (setting TOS resets SO_PRIORITY to 0 on Linux). Linux-only;
// 6 is the highest priority allowed without CAP_NET_ADMIN, so video=5 / audio=6 (Apollo's scheme).
#[cfg(target_os = "linux")]
{
let prio = match class {
MediaClass::Video => 5,
MediaClass::Audio => 6,
};
if let Err(e) = sock.set_priority(prio) {
tracing::debug!(error = %e, "set SO_PRIORITY failed");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dscp_code_points_match_apollo() {
// CS5 video / CS6 audio, shifted into the TOS byte (high 6 bits).
assert_eq!(MediaClass::Video.dscp(), 40);
assert_eq!(MediaClass::Audio.dscp(), 48);
assert_eq!(MediaClass::Video.dscp() << 2, 0xA0);
assert_eq!(MediaClass::Audio.dscp() << 2, 0xC0);
}
#[test]
fn qos_and_buffer_growth_are_best_effort_and_never_panic() {
let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
// No PUNKTFUNK_DSCP in the test env → early return; must not panic regardless.
set_media_qos(&sock, MediaClass::Video);
set_media_qos(&sock, MediaClass::Audio);
grow_socket_buffers(&sock);
}
#[test]
fn apply_qos_tags_the_socket() {
// Exercise the enabled path directly (no env), and read the options back where we can.
let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
apply_media_qos(&sock, MediaClass::Video);
#[cfg(target_os = "linux")]
{
let s = socket2::SockRef::from(&sock);
assert_eq!(s.tos_v4().unwrap(), 0xA0, "video → CS5 in the TOS byte");
assert_eq!(s.priority().unwrap(), 5, "video → SO_PRIORITY 5");
}
}
}