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
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:
@@ -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"))]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user