refactor(pf-encode): one Linux backend resolver, consumed by dispatch AND mirrors (WP7.6)
The Linux backend decision existed as open_video_backend's string match plus five partial hand-copies (the zero-copy-plane gate, the pyro advertisement gate, the software advertisement pin, the vulkan pref ceiling, resolved_backend_is_gpu). Windows solved this long ago — windows_resolved_backend() is consumed by its dispatch AND its mirrors, with labels still stamped at the open sites. Linux now has the twin: resolve_linux_backend (pure, lazy auto probe) + linux_resolved_backend (config wrapper, unknown→auto exactly as every mirror's old `_` arm). ⚠ This deliberately DEVIATES from the audit's shadow-assertion prescription, on both critics' findings: the shadow had no execution venue (open_video had ZERO test call sites; shipped hosts are --release) — unfalsifiable ceremony — and the file's own Windows half proves dispatch-consumes-resolver is safe: the mgmt record is protected by the label-from-the-open-site convention, not by resolver avoidance. One consumed table beats two tables plus an inert cross-check. Preservation riders from the critique, all applied: - pref_ceiling KEEPS its cfg!(vulkan-encode) branch (the resolver is feature-blind; dropping it re-creates advertise-then-die-at-open). - linux_zero_copy_is_vaapi's Vulkan|Software arm preserves the old `_` fallthrough EXACTLY — the vulkan-on-NVIDIA capture-plane mismatch and the software twin are FILED in the design doc, not fixed here. - resolved_backend_is_gpu splits as linux + not(any(windows, linux)) — never a target list, so no exotic target loses the fn. - The auto probe is lazy (impl FnOnce) — explicit prefs stay zero-probe (/serverinfo polls through these mirrors), pinned by a panicking closure in the resolver test. New coverage: the full alias table pinned, and a GPU-free dispatch test through the real open_video_backend (software arm, vendored openh264) — its first test call site anywhere. Config-latch seam guarded loudly. Cross-crate mirrors recorded out of scope in the design doc: session_plan::resolve_encoder, gamestream/serverinfo.rs base_codec_mode_support, capture.rs's "pyrowave" string. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+270
-68
@@ -41,11 +41,14 @@ impl Codec {
|
|||||||
// per-session instead (the host `session_plan::SessionPlan::output_format`) — the EGL→CUDA
|
// per-session instead (the host `session_plan::SessionPlan::output_format`) — the EGL→CUDA
|
||||||
// frames the `auto` GPU path would deliver are NVENC-only. Only a software/GPU-less pref
|
// frames the `auto` GPU path would deliver are NVENC-only. Only a software/GPU-less pref
|
||||||
// keeps the bit off (no Vulkan device to open).
|
// keeps the bit off (no Vulkan device to open).
|
||||||
|
// Resolved ONCE for every gate below (pyro bit, software pin, vulkan ceiling, the
|
||||||
|
// zero-copy plane): this fn backs the codec advertisement on polled paths, and the
|
||||||
|
// resolver's auto arm samples live GPU-preference state — four samples per call was the
|
||||||
|
// probe-amplification the WP7.6 diff critic caught.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let backend = linux_resolved_backend();
|
||||||
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
||||||
let pyro = if !matches!(
|
let pyro = if backend != LinuxBackend::Software {
|
||||||
pf_host_config::config().encoder_pref.as_str(),
|
|
||||||
"software" | "sw" | "openh264"
|
|
||||||
) {
|
|
||||||
punktfunk_core::quic::CODEC_PYROWAVE
|
punktfunk_core::quic::CODEC_PYROWAVE
|
||||||
} else {
|
} else {
|
||||||
0u8
|
0u8
|
||||||
@@ -72,10 +75,7 @@ impl Codec {
|
|||||||
| punktfunk_core::quic::CODEC_AV1;
|
| punktfunk_core::quic::CODEC_AV1;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
if matches!(
|
if backend == LinuxBackend::Software {
|
||||||
pf_host_config::config().encoder_pref.as_str(),
|
|
||||||
"software" | "sw" | "openh264"
|
|
||||||
) {
|
|
||||||
return punktfunk_core::quic::CODEC_H264;
|
return punktfunk_core::quic::CODEC_H264;
|
||||||
}
|
}
|
||||||
// A pref that FORCES the raw Vulkan Video backend can only ever serve what that
|
// A pref that FORCES the raw Vulkan Video backend can only ever serve what that
|
||||||
@@ -89,8 +89,12 @@ impl Codec {
|
|||||||
// for it: pinning a static HEVC|AV1 would ADD AV1 on the AMD/Intel hosts whose probe
|
// for it: pinning a static HEVC|AV1 would ADD AV1 on the AMD/Intel hosts whose probe
|
||||||
// currently withholds it (pre-RDNA3, pre-Arc), i.e. it would re-create this very bug
|
// currently withholds it (pre-RDNA3, pre-Arc), i.e. it would re-create this very bug
|
||||||
// for a different codec. Intersecting can only narrow.
|
// for a different codec. Intersecting can only narrow.
|
||||||
let pref_ceiling: u8 = match pf_host_config::config().encoder_pref.as_str() {
|
let pref_ceiling: u8 = match backend {
|
||||||
"vulkan" | "vulkan-video" => {
|
// Feature-blindness rider (WP7.6 critics): the resolver knows the PREF is
|
||||||
|
// vulkan; only this cfg! knows the BUILD can open it. Dropping the inner
|
||||||
|
// branch would advertise HEVC|AV1 on a featureless build — the
|
||||||
|
// advertise-then-die-at-open bug this ceiling exists to prevent.
|
||||||
|
LinuxBackend::Vulkan => {
|
||||||
if cfg!(feature = "vulkan-encode") {
|
if cfg!(feature = "vulkan-encode") {
|
||||||
punktfunk_core::quic::CODEC_HEVC | punktfunk_core::quic::CODEC_AV1
|
punktfunk_core::quic::CODEC_HEVC | punktfunk_core::quic::CODEC_AV1
|
||||||
} else {
|
} else {
|
||||||
@@ -99,7 +103,7 @@ impl Codec {
|
|||||||
}
|
}
|
||||||
_ => GPU_SUPERSET,
|
_ => GPU_SUPERSET,
|
||||||
};
|
};
|
||||||
if linux_zero_copy_is_vaapi() {
|
if linux_zero_copy_is_vaapi_for(backend) {
|
||||||
if let Some(m) = vaapi_codec_support().wire_mask() {
|
if let Some(m) = vaapi_codec_support().wire_mask() {
|
||||||
return m & pref_ceiling;
|
return m & pref_ceiling;
|
||||||
}
|
}
|
||||||
@@ -299,12 +303,14 @@ impl Encoder for TrackedEncoder {
|
|||||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||||
const SW_BITRATE_CEIL: u64 = 100_000_000;
|
const SW_BITRATE_CEIL: u64 = 100_000_000;
|
||||||
|
|
||||||
/// Open the platform encoder backend. Returns the encoder together with the display label of the
|
/// The Linux half of [`open_video_backend`], with the pref INJECTED — the seam exists so the
|
||||||
/// branch that ACTUALLY opened (`nvenc`/`vaapi`/`vulkan`/`amf`/`qsv`/`software`) — the label feeds
|
/// dispatch is testable without mutating process env (`set_var` races `getenv` in parallel
|
||||||
/// the mgmt API's live-session record, and only the open site knows which internal fallback won
|
/// tests) or fighting the once-latched `pf_host_config::config()`. Production takes the
|
||||||
/// (e.g. Vulkan Video falling back to VAAPI).
|
/// config pref via the caller; shared validation (dims/fps/chroma degrade) stays there too.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn open_video_backend(
|
fn open_video_backend_linux(
|
||||||
|
pref: &str,
|
||||||
codec: Codec,
|
codec: Codec,
|
||||||
format: PixelFormat,
|
format: PixelFormat,
|
||||||
width: u32,
|
width: u32,
|
||||||
@@ -316,31 +322,6 @@ fn open_video_backend(
|
|||||||
chroma: ChromaFormat,
|
chroma: ChromaFormat,
|
||||||
cursor_blend: bool,
|
cursor_blend: bool,
|
||||||
) -> Result<(Box<dyn Encoder>, &'static str)> {
|
) -> Result<(Box<dyn Encoder>, &'static str)> {
|
||||||
let _ = cursor_blend; // consumed only by the Linux vulkan-encode + direct-NVENC arms below
|
|
||||||
validate_dimensions(codec, width, height)?;
|
|
||||||
// Refresh/fps must be positive and sane: fps feeds the encoder time_base (`Rational(1, fps)`)
|
|
||||||
// and the pts→ns conversion (`pts * 1e9 / fps`), so 0 builds a 1/0 rational / divides by zero.
|
|
||||||
// The mid-stream Reconfigure path already guards `refresh_hz > 0`; enforcing it at this single
|
|
||||||
// open chokepoint makes EVERY path (initial Hello, GameStream ANNOUNCE, Reconfigure) safe
|
|
||||||
// regardless of which backend opens (security-review 2026-06-28 S5).
|
|
||||||
if fps == 0 || fps > 1000 {
|
|
||||||
anyhow::bail!("invalid refresh/fps {fps}: must be 1..=1000 Hz");
|
|
||||||
}
|
|
||||||
// 4:4:4 is HEVC- and PyroWave-only. The negotiator should never pass `Yuv444` for another
|
|
||||||
// codec (it gates on the codec + `can_encode_444`), but defend the contract here so a future
|
|
||||||
// caller can't silently emit a stream no decoder expects: an unsupported 4:4:4 request
|
|
||||||
// degrades to 4:2:0 with a warning.
|
|
||||||
let chroma = if chroma.is_444() && codec != Codec::H265 && codec != Codec::PyroWave {
|
|
||||||
tracing::warn!(
|
|
||||||
?codec,
|
|
||||||
"4:4:4 requested for a non-HEVC codec — encoding 4:2:0"
|
|
||||||
);
|
|
||||||
ChromaFormat::Yuv420
|
|
||||||
} else {
|
|
||||||
chroma
|
|
||||||
};
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
// A NEGOTIATED PyroWave session (client advertised + preferred it, plan §3) routes
|
// A NEGOTIATED PyroWave session (client advertised + preferred it, plan §3) routes
|
||||||
// straight to that backend — the PUNKTFUNK_ENCODER pref below stays a lab override.
|
// straight to that backend — the PUNKTFUNK_ENCODER pref below stays a lab override.
|
||||||
if codec == Codec::PyroWave {
|
if codec == Codec::PyroWave {
|
||||||
@@ -359,7 +340,6 @@ fn open_video_backend(
|
|||||||
// AMD/Intel → VAAPI (one libavcodec backend for both). Auto-detect by default so a single
|
// AMD/Intel → VAAPI (one libavcodec backend for both). Auto-detect by default so a single
|
||||||
// Linux binary serves any GPU; `PUNKTFUNK_ENCODER` forces a specific backend (and surfaces
|
// Linux binary serves any GPU; `PUNKTFUNK_ENCODER` forces a specific backend (and surfaces
|
||||||
// its errors crisply instead of silently trying the other).
|
// its errors crisply instead of silently trying the other).
|
||||||
let pref = pf_host_config::config().encoder_pref.as_str();
|
|
||||||
// AMD/Intel opener. Default = libav VAAPI. With `--features vulkan-encode` +
|
// AMD/Intel opener. Default = libav VAAPI. With `--features vulkan-encode` +
|
||||||
// PUNKTFUNK_VULKAN_ENCODE, an HEVC session instead opens the raw Vulkan Video backend (real
|
// PUNKTFUNK_VULKAN_ENCODE, an HEVC session instead opens the raw Vulkan Video backend (real
|
||||||
// RFI loss recovery the VAAPI path can't express); a failed open falls back to VAAPI so the
|
// RFI loss recovery the VAAPI path can't express); a failed open falls back to VAAPI so the
|
||||||
@@ -444,11 +424,14 @@ fn open_video_backend(
|
|||||||
)
|
)
|
||||||
.map(|e| (e, "nvenc"))
|
.map(|e| (e, "nvenc"))
|
||||||
};
|
};
|
||||||
match pref {
|
// WP7.6: the dispatch consumes the SAME resolver the capability mirrors consult (the
|
||||||
"nvenc" | "nvidia" | "cuda" => open_nvidia(),
|
// Windows shape — `windows_resolved_backend` + `match backend` below) so the alias table
|
||||||
"vaapi" | "amd" | "intel" => open_amd_intel(),
|
// exists once. Arm bodies and their open-site labels are untouched.
|
||||||
|
match resolve_linux_backend(pref, linux_auto_is_vaapi, cuda) {
|
||||||
|
Some(LinuxBackend::Nvenc) => open_nvidia(),
|
||||||
|
Some(LinuxBackend::AmdIntel) => open_amd_intel(),
|
||||||
// Force the raw Vulkan Video HEVC backend (real RFI). Needs `--features vulkan-encode`.
|
// Force the raw Vulkan Video HEVC backend (real RFI). Needs `--features vulkan-encode`.
|
||||||
"vulkan" | "vulkan-video" => {
|
Some(LinuxBackend::Vulkan) => {
|
||||||
#[cfg(feature = "vulkan-encode")]
|
#[cfg(feature = "vulkan-encode")]
|
||||||
{
|
{
|
||||||
if !matches!(codec, Codec::H265 | Codec::Av1) {
|
if !matches!(codec, Codec::H265 | Codec::Av1) {
|
||||||
@@ -480,7 +463,7 @@ fn open_video_backend(
|
|||||||
// client can decode the stream yet, so this arm exists for host-side bring-up and
|
// client can decode the stream yet, so this arm exists for host-side bring-up and
|
||||||
// latency work only. Vendor-agnostic (any Vulkan 1.3 GPU); ignores the negotiated
|
// latency work only. Vendor-agnostic (any Vulkan 1.3 GPU); ignores the negotiated
|
||||||
// codec — every AU is an independently-decodable wavelet frame.
|
// codec — every AU is an independently-decodable wavelet frame.
|
||||||
"pyrowave" => {
|
Some(LinuxBackend::Pyrowave) => {
|
||||||
#[cfg(feature = "pyrowave")]
|
#[cfg(feature = "pyrowave")]
|
||||||
{
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -513,7 +496,7 @@ fn open_video_backend(
|
|||||||
// `auto` never picks it (a box with `/dev/nvidiactl` present but a dead driver would
|
// `auto` never picks it (a box with `/dev/nvidiactl` present but a dead driver would
|
||||||
// otherwise wrongly resolve to NVENC). Needs H.264 (openh264 emits only that) and a CPU
|
// otherwise wrongly resolve to NVENC). Needs H.264 (openh264 emits only that) and a CPU
|
||||||
// RGB frame, which the capturer delivers because the software backend resolves `gpu=false`.
|
// RGB frame, which the capturer delivers because the software backend resolves `gpu=false`.
|
||||||
"software" | "sw" | "openh264" => {
|
Some(LinuxBackend::Software) => {
|
||||||
if codec != Codec::H264 {
|
if codec != Codec::H264 {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"the software encoder emits H.264 only; the session negotiated {codec:?} \
|
"the software encoder emits H.264 only; the session negotiated {codec:?} \
|
||||||
@@ -530,20 +513,69 @@ fn open_video_backend(
|
|||||||
)
|
)
|
||||||
.map(|e| (Box::new(e) as Box<dyn Encoder>, "software"))
|
.map(|e| (Box::new(e) as Box<dyn Encoder>, "software"))
|
||||||
}
|
}
|
||||||
"auto" | "" => {
|
// The auto arm lives in the resolver now (a CUDA frame can only be consumed by
|
||||||
// A CUDA frame can ONLY be consumed by NVENC. Otherwise the shared auto decision
|
// NVENC; else the shared manual-preference/NVIDIA-presence decision).
|
||||||
// (manual web-console GPU preference, else the NVIDIA-presence probe) picks the
|
None => anyhow::bail!(
|
||||||
// backend — see `linux_auto_is_vaapi`.
|
"unknown PUNKTFUNK_ENCODER={pref:?} — use auto (default), nvenc, vaapi, vulkan, pyrowave, or software"
|
||||||
if cuda || !linux_auto_is_vaapi() {
|
|
||||||
open_nvidia()
|
|
||||||
} else {
|
|
||||||
open_amd_intel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
other => anyhow::bail!(
|
|
||||||
"unknown PUNKTFUNK_ENCODER={other:?} — use auto (default), nvenc, vaapi, vulkan, pyrowave, or software"
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the platform encoder backend. Returns the encoder together with the display label of the
|
||||||
|
/// branch that ACTUALLY opened (`nvenc`/`vaapi`/`vulkan`/`amf`/`qsv`/`software`) — the label feeds
|
||||||
|
/// the mgmt API's live-session record, and only the open site knows which internal fallback won
|
||||||
|
/// (e.g. Vulkan Video falling back to VAAPI).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn open_video_backend(
|
||||||
|
codec: Codec,
|
||||||
|
format: PixelFormat,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
fps: u32,
|
||||||
|
bitrate_bps: u64,
|
||||||
|
cuda: bool,
|
||||||
|
bit_depth: u8,
|
||||||
|
chroma: ChromaFormat,
|
||||||
|
cursor_blend: bool,
|
||||||
|
) -> Result<(Box<dyn Encoder>, &'static str)> {
|
||||||
|
let _ = cursor_blend; // consumed only by the Linux vulkan-encode + direct-NVENC arms below
|
||||||
|
validate_dimensions(codec, width, height)?;
|
||||||
|
// Refresh/fps must be positive and sane: fps feeds the encoder time_base (`Rational(1, fps)`)
|
||||||
|
// and the pts→ns conversion (`pts * 1e9 / fps`), so 0 builds a 1/0 rational / divides by zero.
|
||||||
|
// The mid-stream Reconfigure path already guards `refresh_hz > 0`; enforcing it at this single
|
||||||
|
// open chokepoint makes EVERY path (initial Hello, GameStream ANNOUNCE, Reconfigure) safe
|
||||||
|
// regardless of which backend opens (security-review 2026-06-28 S5).
|
||||||
|
if fps == 0 || fps > 1000 {
|
||||||
|
anyhow::bail!("invalid refresh/fps {fps}: must be 1..=1000 Hz");
|
||||||
|
}
|
||||||
|
// 4:4:4 is HEVC- and PyroWave-only. The negotiator should never pass `Yuv444` for another
|
||||||
|
// codec (it gates on the codec + `can_encode_444`), but defend the contract here so a future
|
||||||
|
// caller can't silently emit a stream no decoder expects: an unsupported 4:4:4 request
|
||||||
|
// degrades to 4:2:0 with a warning.
|
||||||
|
let chroma = if chroma.is_444() && codec != Codec::H265 && codec != Codec::PyroWave {
|
||||||
|
tracing::warn!(
|
||||||
|
?codec,
|
||||||
|
"4:4:4 requested for a non-HEVC codec — encoding 4:2:0"
|
||||||
|
);
|
||||||
|
ChromaFormat::Yuv420
|
||||||
|
} else {
|
||||||
|
chroma
|
||||||
|
};
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
open_video_backend_linux(
|
||||||
|
pf_host_config::config().encoder_pref.as_str(),
|
||||||
|
codec,
|
||||||
|
format,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fps,
|
||||||
|
bitrate_bps,
|
||||||
|
cuda,
|
||||||
|
bit_depth,
|
||||||
|
chroma,
|
||||||
|
cursor_blend,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
@@ -1008,6 +1040,67 @@ fn linux_auto_is_vaapi() -> bool {
|
|||||||
!nvidia_present()
|
!nvidia_present()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The resolved Linux encode backend — the Linux twin of [`WindowsBackend`] (WP7.6). ONE alias
|
||||||
|
/// table, consumed by BOTH [`open_video_backend`]'s dispatch and every capability/advertisement
|
||||||
|
/// mirror below, so the two can never drift again (the audit counted five partial hand-copies of
|
||||||
|
/// this decision). Labels still come from the open sites (the mgmt-record convention) — this
|
||||||
|
/// resolves which ARM runs, never what actually opened.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum LinuxBackend {
|
||||||
|
Nvenc,
|
||||||
|
AmdIntel,
|
||||||
|
Vulkan,
|
||||||
|
Pyrowave,
|
||||||
|
Software,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pure core. `None` = the pref names no backend — [`open_video_backend`] BAILS on that,
|
||||||
|
/// while the capability mirrors historically resolved it as auto (via
|
||||||
|
/// [`linux_resolved_backend`]); that divergence is recorded, not accidental. `auto_is_vaapi` is
|
||||||
|
/// deliberately LAZY: explicit prefs must stay zero-probe — `/serverinfo` polls through the
|
||||||
|
/// mirrors, and `gamestream/serverinfo.rs` already litigated exactly that probe cost.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn resolve_linux_backend(
|
||||||
|
pref: &str,
|
||||||
|
auto_is_vaapi: impl FnOnce() -> bool,
|
||||||
|
cuda: bool,
|
||||||
|
) -> Option<LinuxBackend> {
|
||||||
|
Some(match pref {
|
||||||
|
"nvenc" | "nvidia" | "cuda" => LinuxBackend::Nvenc,
|
||||||
|
"vaapi" | "amd" | "intel" => LinuxBackend::AmdIntel,
|
||||||
|
"vulkan" | "vulkan-video" => LinuxBackend::Vulkan,
|
||||||
|
"pyrowave" => LinuxBackend::Pyrowave,
|
||||||
|
"software" | "sw" | "openh264" => LinuxBackend::Software,
|
||||||
|
// A CUDA frame can ONLY be consumed by NVENC. Otherwise the shared auto decision
|
||||||
|
// (manual web-console GPU preference, else the NVIDIA-presence probe) picks the backend —
|
||||||
|
// see `linux_auto_is_vaapi`.
|
||||||
|
"auto" | "" => {
|
||||||
|
if cuda || !auto_is_vaapi() {
|
||||||
|
LinuxBackend::Nvenc
|
||||||
|
} else {
|
||||||
|
LinuxBackend::AmdIntel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Config-reading wrapper for the capability/advertisement mirrors: an unknown pref resolves as
|
||||||
|
/// auto — exactly what every mirror's old `_` arm did. `cuda = false`: the mirrors answer
|
||||||
|
/// pre-session questions (a CUDA payload exists only inside a live NVIDIA session).
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn linux_resolved_backend() -> LinuxBackend {
|
||||||
|
let pref = pf_host_config::config().encoder_pref.as_str();
|
||||||
|
resolve_linux_backend(pref, linux_auto_is_vaapi, false).unwrap_or_else(|| {
|
||||||
|
if linux_auto_is_vaapi() {
|
||||||
|
LinuxBackend::AmdIntel
|
||||||
|
} else {
|
||||||
|
LinuxBackend::Nvenc
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// The dmabuf modifiers the PyroWave encoder's Vulkan device imports for the capture's
|
/// The dmabuf modifiers the PyroWave encoder's Vulkan device imports for the capture's
|
||||||
/// packed-RGB fourcc — advertised by the capture when the pyrowave passthrough is active
|
/// packed-RGB fourcc — advertised by the capture when the pyrowave passthrough is active
|
||||||
/// (the VAAPI LINEAR-only policy starves it on Mutter+NVIDIA, which allocates tiled only).
|
/// (the VAAPI LINEAR-only policy starves it on Mutter+NVIDIA, which allocates tiled only).
|
||||||
@@ -1021,13 +1114,25 @@ pub fn pyrowave_capture_modifiers(fourcc: u32) -> Vec<u64> {
|
|||||||
/// passthrough for VAAPI vs the EGL→CUDA import for NVENC).
|
/// passthrough for VAAPI vs the EGL→CUDA import for NVENC).
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn linux_zero_copy_is_vaapi() -> bool {
|
pub fn linux_zero_copy_is_vaapi() -> bool {
|
||||||
match pf_host_config::config().encoder_pref.as_str() {
|
linux_zero_copy_is_vaapi_for(linux_resolved_backend())
|
||||||
"nvenc" | "nvidia" | "cuda" => false,
|
}
|
||||||
"vaapi" | "amd" | "intel" => true,
|
/// The zero-copy-plane decision for an ALREADY-resolved backend — so a caller that has just
|
||||||
|
/// resolved (e.g. `host_wire_caps`, which consults several gates per call on a POLLED endpoint)
|
||||||
|
/// pays for the resolution once instead of once per gate (the `serverinfo.rs` probe-cost class,
|
||||||
|
/// caught by the WP7.6 diff critic).
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn linux_zero_copy_is_vaapi_for(backend: LinuxBackend) -> bool {
|
||||||
|
match backend {
|
||||||
|
LinuxBackend::Nvenc => false,
|
||||||
|
LinuxBackend::AmdIntel => true,
|
||||||
// PyroWave ingests the raw capture dmabuf itself (Vulkan import + compute CSC) on ANY
|
// PyroWave ingests the raw capture dmabuf itself (Vulkan import + compute CSC) on ANY
|
||||||
// vendor — it must get the passthrough payload, never the EGL→CUDA import.
|
// vendor — it must get the passthrough payload, never the EGL→CUDA import.
|
||||||
"pyrowave" => true,
|
LinuxBackend::Pyrowave => true,
|
||||||
_ => linux_auto_is_vaapi(),
|
// EXACTLY the old `_` fallthrough for these two prefs — preserved, not endorsed. The
|
||||||
|
// vulkan-on-NVIDIA half is FILED (EGL→CUDA capture negotiated for a dmabuf-importing
|
||||||
|
// backend; reachable only via the explicit lab pref); the software half is latent (a
|
||||||
|
// software host pins H.264, so the GPU-backend probes this steers never negotiate).
|
||||||
|
LinuxBackend::Vulkan | LinuxBackend::Software => linux_auto_is_vaapi(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1320,9 +1425,17 @@ pub fn windows_resolved_backend() -> WindowsBackend {
|
|||||||
pub fn resolved_backend_is_gpu() -> bool {
|
pub fn resolved_backend_is_gpu() -> bool {
|
||||||
!matches!(windows_resolved_backend(), WindowsBackend::Software)
|
!matches!(windows_resolved_backend(), WindowsBackend::Software)
|
||||||
}
|
}
|
||||||
/// Linux/other: every backend but the GPU-less software encoder (openh264) is GPU-resident. Config-backed
|
/// Linux: every backend but the GPU-less software encoder (openh264) is GPU-resident. Resolver-
|
||||||
/// (mirrors `session_plan::resolve_encoder`; the NVENC vs VAAPI split is auto-detected in [`open_video`]).
|
/// backed (WP7.6); cross-crate mirrors still exist in `session_plan::resolve_encoder` and
|
||||||
#[cfg(not(target_os = "windows"))]
|
/// `gamestream/serverinfo.rs` — out of this crate's reach, listed in the WP7.6 design doc.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn resolved_backend_is_gpu() -> bool {
|
||||||
|
linux_resolved_backend() != LinuxBackend::Software
|
||||||
|
}
|
||||||
|
/// Other non-Windows targets (the macOS dev host): keep the string gate — no resolver exists
|
||||||
|
/// there. `not(any(...))`, NEVER a target list, so no exotic target loses the fn (the
|
||||||
|
/// `can_encode_444`/`can_encode_10bit` shape).
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||||
pub fn resolved_backend_is_gpu() -> bool {
|
pub fn resolved_backend_is_gpu() -> bool {
|
||||||
!matches!(
|
!matches!(
|
||||||
pf_host_config::config().encoder_pref.as_str(),
|
pf_host_config::config().encoder_pref.as_str(),
|
||||||
@@ -1684,4 +1797,93 @@ mod tests {
|
|||||||
// here is pure belt-and-braces against a parse regression.
|
// here is pure belt-and-braces against a parse regression.
|
||||||
assert_eq!(trait_fns, impl_fns);
|
assert_eq!(trait_fns, impl_fns);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// WP7.6: the resolver's full alias table, pinned. The panicking closure is the laziness
|
||||||
|
/// contract — an explicit pref must resolve WITHOUT running the auto probe (`/serverinfo`
|
||||||
|
/// polls through the mirrors; `gamestream/serverinfo.rs` litigated exactly that cost).
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn linux_backend_resolver_table() {
|
||||||
|
use LinuxBackend::*;
|
||||||
|
let no_probe = || -> bool { panic!("explicit prefs must not run the auto probe") };
|
||||||
|
for pref in ["nvenc", "nvidia", "cuda"] {
|
||||||
|
assert_eq!(resolve_linux_backend(pref, no_probe, false), Some(Nvenc));
|
||||||
|
}
|
||||||
|
for pref in ["vaapi", "amd", "intel"] {
|
||||||
|
assert_eq!(resolve_linux_backend(pref, no_probe, false), Some(AmdIntel));
|
||||||
|
}
|
||||||
|
for pref in ["vulkan", "vulkan-video"] {
|
||||||
|
assert_eq!(resolve_linux_backend(pref, no_probe, false), Some(Vulkan));
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
resolve_linux_backend("pyrowave", no_probe, false),
|
||||||
|
Some(Pyrowave)
|
||||||
|
);
|
||||||
|
for pref in ["software", "sw", "openh264"] {
|
||||||
|
assert_eq!(resolve_linux_backend(pref, no_probe, false), Some(Software));
|
||||||
|
}
|
||||||
|
// auto (and the default empty pref): the probe decides…
|
||||||
|
assert_eq!(resolve_linux_backend("", || true, false), Some(AmdIntel));
|
||||||
|
assert_eq!(resolve_linux_backend("auto", || false, false), Some(Nvenc));
|
||||||
|
// …except a CUDA frame, which only NVENC can consume — and that short-circuits the
|
||||||
|
// probe too (`||` order).
|
||||||
|
assert_eq!(resolve_linux_backend("auto", no_probe, true), Some(Nvenc));
|
||||||
|
// Unknown prefs name no backend: the dispatch bails; the mirrors' wrapper maps this to
|
||||||
|
// auto (the historical `_`-arm behavior, recorded in the design doc).
|
||||||
|
assert_eq!(resolve_linux_backend("banana", no_probe, false), None);
|
||||||
|
// An explicit pref is never overridden by `cuda` (a CUDA payload cannot appear in a
|
||||||
|
// session whose pref forced a non-NVENC backend; the table must not mask such a bug).
|
||||||
|
assert_eq!(
|
||||||
|
resolve_linux_backend("vaapi", no_probe, true),
|
||||||
|
Some(AmdIntel)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WP7.6: the REAL dispatch through the resolver, GPU-free via the software arm (openh264 is
|
||||||
|
/// vendored + CPU-only; its own tests already run on every leg). This is the only in-CI
|
||||||
|
/// execution of the Linux dispatch — before this test it had zero test call sites. The pref
|
||||||
|
/// is INJECTED through `open_video_backend_linux` — the first draft mutated `PUNKTFUNK_ENCODER`
|
||||||
|
/// via `set_var`, the binary's first non-ignored env mutation, racing `getenv` in the sw/codec/
|
||||||
|
/// nvenc_core tests (diff-critic catch); the seam removes the race and the config-latch
|
||||||
|
/// coupling outright.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn open_video_backend_dispatches_software() {
|
||||||
|
let (enc, label) = open_video_backend_linux(
|
||||||
|
"software",
|
||||||
|
Codec::H264,
|
||||||
|
PixelFormat::Bgrx,
|
||||||
|
64,
|
||||||
|
64,
|
||||||
|
30,
|
||||||
|
1_000_000,
|
||||||
|
false,
|
||||||
|
8,
|
||||||
|
ChromaFormat::Yuv420,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("software arm must open GPU-free");
|
||||||
|
assert_eq!(label, "software");
|
||||||
|
drop(enc);
|
||||||
|
// The software arm is codec-gated — a non-H.264 session must take the bail, not fall
|
||||||
|
// through to another backend.
|
||||||
|
let err = match open_video_backend_linux(
|
||||||
|
"software",
|
||||||
|
Codec::H265,
|
||||||
|
PixelFormat::Bgrx,
|
||||||
|
64,
|
||||||
|
64,
|
||||||
|
30,
|
||||||
|
1_000_000,
|
||||||
|
false,
|
||||||
|
8,
|
||||||
|
ChromaFormat::Yuv420,
|
||||||
|
false,
|
||||||
|
) {
|
||||||
|
// `expect_err` needs `Ok: Debug` and `Box<dyn Encoder>` isn't — match instead.
|
||||||
|
Ok(_) => panic!("software emits H.264 only; an H.265 session must be refused"),
|
||||||
|
Err(e) => e,
|
||||||
|
};
|
||||||
|
assert!(err.to_string().contains("H.264"), "{err:#}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user