From 6922e1c467fb15b60b9123533110750f9d096df1 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 20 Jun 2026 10:41:37 +0000 Subject: [PATCH] feat(host): VAAPI codec probe + AMD/Intel packaging + neutral logs (Phase 3) Polish for AMD/Intel support: - GameStream serverinfo advertises only codecs the GPU can ACTUALLY encode on the VAAPI backend (probed once by opening a tiny encoder per codec). AV1 encode is narrow (Intel Arc/Xe2+, AMD RDNA3+/RDNA4) and an old iGPU may lack HEVC, so a Moonlight client never negotiates a codec the encoder can't open. NVENC/Windows keep the Moonlight-validated static mask. Validated on a Radeon 780M: h264/h265/av1 all probe true -> mask unchanged (65793). - Packaging: Recommends mesa-va-drivers + intel-media-va-driver (deb) / mesa-va-drivers + intel-media-driver (rpm) so the auto-selected VAAPI backend works out of the box on AMD/Intel; NVIDIA boxes can --no-install-recommends. (Fedora note: stock mesa-va-drivers disables HEVC/AV1 -- needs the freeworld variant from RPM Fusion.) - De-NVIDIA-fy the user-facing encoder log/context strings ("open NVENC" -> "open video encoder") now that VAAPI is a first-class backend. Co-Authored-By: Claude Opus 4.8 --- crates/punktfunk-host/src/encode.rs | 34 +++++++++++++++++++ crates/punktfunk-host/src/encode/vaapi.rs | 17 ++++++++++ .../src/gamestream/serverinfo.rs | 27 ++++++++++++++- .../punktfunk-host/src/gamestream/stream.rs | 2 +- crates/punktfunk-host/src/punktfunk1.rs | 4 +-- crates/punktfunk-host/src/spike.rs | 2 +- packaging/debian/build-deb.sh | 5 ++- packaging/rpm/punktfunk.spec | 5 +++ 8 files changed, 90 insertions(+), 6 deletions(-) diff --git a/crates/punktfunk-host/src/encode.rs b/crates/punktfunk-host/src/encode.rs index 6bb8af5..ae5485b 100644 --- a/crates/punktfunk-host/src/encode.rs +++ b/crates/punktfunk-host/src/encode.rs @@ -320,6 +320,40 @@ pub fn linux_zero_copy_is_vaapi() -> bool { } } +/// Which codecs the active GPU can actually ENCODE. Used to build the GameStream codec +/// advertisement so a client never negotiates a codec the GPU can't do (AV1 encode is narrow — +/// Intel Arc/Xe2+, AMD RDNA3+/RDNA4 — so it must be probed, not assumed). +#[cfg(target_os = "linux")] +#[derive(Clone, Copy, Debug)] +pub struct CodecSupport { + pub h264: bool, + pub h265: bool, + pub av1: bool, +} + +/// Probe the active Linux GPU backend for its encodable codecs (cached; opens a tiny encoder per +/// codec, once). Only the VAAPI (AMD/Intel) backend is probed — NVENC keeps its Moonlight-validated +/// static advertisement (callers gate on [`linux_zero_copy_is_vaapi`]). +#[cfg(target_os = "linux")] +pub fn vaapi_codec_support() -> CodecSupport { + use std::sync::OnceLock; + static CACHE: OnceLock = OnceLock::new(); + *CACHE.get_or_init(|| { + let caps = CodecSupport { + h264: vaapi::probe_can_encode(Codec::H264), + h265: vaapi::probe_can_encode(Codec::H265), + av1: vaapi::probe_can_encode(Codec::Av1), + }; + tracing::info!( + h264 = caps.h264, + h265 = caps.h265, + av1 = caps.av1, + "VAAPI encode capabilities probed" + ); + caps + }) +} + #[cfg(target_os = "linux")] mod linux; #[cfg(all(target_os = "windows", feature = "nvenc"))] diff --git a/crates/punktfunk-host/src/encode/vaapi.rs b/crates/punktfunk-host/src/encode/vaapi.rs index 260d2fa..18a593a 100644 --- a/crates/punktfunk-host/src/encode/vaapi.rs +++ b/crates/punktfunk-host/src/encode/vaapi.rs @@ -125,6 +125,23 @@ unsafe fn open_vaapi_encoder( .with_context(|| format!("open {name} ({width}x{height}@{fps}, {bitrate_bps} bps)")) } +/// Probe whether THIS GPU can VAAPI-encode `codec`, by opening a tiny encoder: the driver rejects +/// codecs its video engine can't do (e.g. AV1 on pre-RDNA3 AMD / pre-Arc Intel). Used to build the +/// GameStream codec advertisement so a client never negotiates a codec the GPU can't encode. The +/// device + encoder are torn down immediately (RAII). +pub fn probe_can_encode(codec: Codec) -> bool { + if ffmpeg::init().is_err() { + return false; + } + unsafe { + let hw = match VaapiHw::new(ffi::AVPixelFormat::AV_PIX_FMT_NV12, 640, 480, 2) { + Ok(hw) => hw, + Err(_) => return false, + }; + open_vaapi_encoder(codec, 640, 480, 30, 2_000_000, hw.device_ref, hw.frames_ref).is_ok() + } +} + /// Drain the encoder for one packet (shared poll logic). fn poll_encoder(enc: &mut encoder::video::Encoder, fps: u32) -> Result> { let mut pkt = Packet::empty(); diff --git a/crates/punktfunk-host/src/gamestream/serverinfo.rs b/crates/punktfunk-host/src/gamestream/serverinfo.rs index cd0daee..f90d4f4 100644 --- a/crates/punktfunk-host/src/gamestream/serverinfo.rs +++ b/crates/punktfunk-host/src/gamestream/serverinfo.rs @@ -15,6 +15,7 @@ pub fn serverinfo_xml(host: &Host, https: bool) -> String { }; // Over the mutual-TLS HTTPS port the peer is an authenticated (paired) client. let pair_status = u8::from(https); + let codec_mode_support = codec_mode_support(); format!( r#" @@ -27,7 +28,7 @@ pub fn serverinfo_xml(host: &Host, https: bool) -> String { 1869449984 {mac} {local_ip} -{SERVER_CODEC_MODE_SUPPORT} +{codec_mode_support} {pair_status} 0 SUNSHINE_SERVER_FREE @@ -41,6 +42,30 @@ pub fn serverinfo_xml(host: &Host, https: bool) -> String { ) } +/// The `` mask to advertise. On the VAAPI (AMD/Intel) backend it reflects +/// what the GPU can ACTUALLY encode (probed — AV1 is narrow, and an old iGPU might lack HEVC), so a +/// Moonlight client never negotiates a codec the encoder can't open. NVENC and Windows keep the +/// Moonlight-validated static superset. +fn codec_mode_support() -> u32 { + #[cfg(target_os = "linux")] + if crate::encode::linux_zero_copy_is_vaapi() { + use super::{SCM_AV1_MAIN8, SCM_H264, SCM_HEVC}; + let caps = crate::encode::vaapi_codec_support(); + let mut m = 0; + if caps.h264 { + m |= SCM_H264; + } + if caps.h265 { + m |= SCM_HEVC; + } + if caps.av1 { + m |= SCM_AV1_MAIN8; + } + return m; + } + SERVER_CODEC_MODE_SUPPORT +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/punktfunk-host/src/gamestream/stream.rs b/crates/punktfunk-host/src/gamestream/stream.rs index a91b7c8..1dab32f 100644 --- a/crates/punktfunk-host/src/gamestream/stream.rs +++ b/crates/punktfunk-host/src/gamestream/stream.rs @@ -295,7 +295,7 @@ fn stream_body( frame.is_cuda(), 8, // GameStream/Moonlight path: 8-bit (its own codec negotiation) ) - .context("open NVENC for stream")?; + .context("open video encoder for stream")?; // FEC overhead percent (Sunshine default 20). Override with PUNKTFUNK_FEC_PCT (0 = data-only). let fec_pct: u8 = std::env::var("PUNKTFUNK_FEC_PCT") .ok() diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index a3db7ab..0086609 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -2469,7 +2469,7 @@ fn virtual_stream_relay( frame.is_cuda(), bit_depth, ) - .context("open NVENC for DDA")?; + .context("open video encoder for DDA")?; Ok(DdaPipe { cap: Box::new(cap), enc, @@ -2883,7 +2883,7 @@ fn build_pipeline( frame.is_cuda(), bit_depth, ) - .context("open NVENC")?; + .context("open video encoder")?; let interval = std::time::Duration::from_secs_f64(1.0 / effective_hz.max(1) as f64); Ok((capturer, enc, frame, interval)) } diff --git a/crates/punktfunk-host/src/spike.rs b/crates/punktfunk-host/src/spike.rs index e24543e..9e31ce3 100644 --- a/crates/punktfunk-host/src/spike.rs +++ b/crates/punktfunk-host/src/spike.rs @@ -94,7 +94,7 @@ pub fn run(opts: Options) -> Result<()> { format = ?first.format, codec = ?opts.codec, bitrate_bps = opts.bitrate_bps, - "opening NVENC encoder" + "opening video encoder" ); let mut encoder = encode::open_video( opts.codec, diff --git a/packaging/debian/build-deb.sh b/packaging/debian/build-deb.sh index 4ab6b7f..001ab94 100755 --- a/packaging/debian/build-deb.sh +++ b/packaging/debian/build-deb.sh @@ -109,10 +109,13 @@ DEPENDS="$SHDEPS, libei1, pipewire, wireplumber" # ffmpeg: Ubuntu's ffmpeg ships the NVENC-enabled libav* the binary links AND is the encoder # runtime; the libav* sonames are already hard Depends via shlibdeps, so the ffmpeg metapackage # is a Recommends. gamescope = a ready compositor backend; pipewire-pulse = desktop audio. +# mesa-va-drivers / intel-media-va-driver = the VAAPI encode drivers for AMD (radeonsi) and Intel +# (iHD) — pulled by default so the auto-selected VAAPI backend works out of the box; NVIDIA boxes +# don't need them (NVENC comes from the driver) and can --no-install-recommends. # punktfunk-web = the management web console (pairing + status) every user needs — a separate # Architecture:all .deb; Recommends so `apt install punktfunk-host` pulls it by default, while a # headless/encoding-only box can opt out with --no-install-recommends. -RECOMMENDS="ffmpeg, gamescope, pipewire-pulse, punktfunk-web" +RECOMMENDS="ffmpeg, gamescope, pipewire-pulse, mesa-va-drivers, intel-media-va-driver, punktfunk-web" SUGGESTS="kwin-wayland, mutter" INSTALLED_KB="$(du -k -s "$STAGE" | cut -f1)" diff --git a/packaging/rpm/punktfunk.spec b/packaging/rpm/punktfunk.spec index 9f7ff4e..072a5e6 100644 --- a/packaging/rpm/punktfunk.spec +++ b/packaging/rpm/punktfunk.spec @@ -100,6 +100,11 @@ Suggests: kwin Suggests: mutter # NVENC + GPU EGL come from the NVIDIA driver; on Bazzite the -nvidia image has it. Recommends: (xorg-x11-drv-nvidia-cuda if xorg-x11-drv-nvidia) +# VAAPI encode drivers for AMD (radeonsi) / Intel (iHD) — the auto-selected VAAPI backend on a +# non-NVIDIA GPU. NOTE: Fedora's stock mesa-va-drivers has HEVC/AV1 *disabled* (patents); full +# encode needs mesa-va-drivers-freeworld from RPM Fusion (same nonfree repo as ffmpeg-libs). +Recommends: mesa-va-drivers +Recommends: intel-media-driver # The management web console (pairing + status) every user needs — a separate noarch subpackage. # Weak-dep so `dnf install punktfunk` pulls it where it exists (the Gitea registry); harmless where # it doesn't (a COPR build without `--with web` simply has no punktfunk-web to satisfy).