71e1865519
apple / swift (push) Successful in 2m36s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 50s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 59s
apple / screenshots (push) Successful in 5m55s
windows-host / package (push) Successful in 7m32s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m19s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m13s
release / apple (push) Successful in 12m3s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m18s
ci / rust (push) Successful in 4m52s
ci / bench (push) Successful in 6m44s
android / android (push) Successful in 5m9s
arch / build-publish (push) Successful in 5m40s
decky / build-publish (push) Successful in 15s
deb / build-publish (push) Successful in 3m20s
linux-client-screenshots / screenshots (push) Successful in 2m0s
flatpak / build-publish (push) Successful in 4m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m21s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m13s
docker / deploy-docs (push) Successful in 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
web-screenshots / screenshots (push) Successful in 2m59s
android-screenshots / screenshots (push) Failing after 3m25s
The 27c53a4 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>
164 lines
7.7 KiB
Rust
164 lines
7.7 KiB
Rust
//! 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");
|
||
}
|
||
}
|
||
}
|