feat(host): VAAPI codec probe + AMD/Intel packaging + neutral logs (Phase 3)
apple / swift (push) Successful in 55s
ci / rust (push) Failing after 1m35s
ci / web (push) Successful in 28s
windows-host / package (push) Successful in 2m23s
ci / docs-site (push) Successful in 30s
android / android (push) Successful in 3m24s
deb / build-publish (push) Successful in 3m22s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m48s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m50s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m51s
docker / deploy-docs (push) Successful in 18s

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 10:41:37 +00:00
parent 708c62788d
commit 6922e1c467
8 changed files with 90 additions and 6 deletions
+34
View File
@@ -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<CodecSupport> = 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"))]
+17
View File
@@ -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<Option<EncodedFrame>> {
let mut pkt = Packet::empty();
@@ -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#"<?xml version="1.0" encoding="utf-8"?>
<root status_code="200">
@@ -27,7 +28,7 @@ pub fn serverinfo_xml(host: &Host, https: bool) -> String {
<MaxLumaPixelsHEVC>1869449984</MaxLumaPixelsHEVC>
<mac>{mac}</mac>
<LocalIP>{local_ip}</LocalIP>
<ServerCodecModeSupport>{SERVER_CODEC_MODE_SUPPORT}</ServerCodecModeSupport>
<ServerCodecModeSupport>{codec_mode_support}</ServerCodecModeSupport>
<PairStatus>{pair_status}</PairStatus>
<currentgame>0</currentgame>
<state>SUNSHINE_SERVER_FREE</state>
@@ -41,6 +42,30 @@ pub fn serverinfo_xml(host: &Host, https: bool) -> String {
)
}
/// The `<ServerCodecModeSupport>` 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::*;
@@ -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()
+2 -2
View File
@@ -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))
}
+1 -1
View File
@@ -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,
+4 -1
View File
@@ -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)"
+5
View File
@@ -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).