From 25765c53ecc37d8df1dba383d79d2ba8ca6ee8f7 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 26 Jul 2026 01:05:24 +0200 Subject: [PATCH] fix(encode): classify libav-NVENC open failures by errno, not English strerror text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bitrate-probe ladder stepped down on format!("{e:#}").contains( "Invalid argument") — an English substring over the WHOLE context chain, which also fired on any other wrapped EINVAL (e.g. a CUDA-context errno) and gated a ~10-step ladder on strerror wording. The root ffmpeg::Error survives the anyhow chain; downcast and match Error::Other{errno:EINVAL} instead. Same fix for the intra-refresh ENOSYS probe in the open path. Co-Authored-By: Claude Fable 5 --- crates/pf-encode/src/enc/linux/mod.rs | 10 +++++++++- crates/pf-encode/src/lib.rs | 18 +++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/pf-encode/src/enc/linux/mod.rs b/crates/pf-encode/src/enc/linux/mod.rs index 69014d0f..9fc2fc0c 100644 --- a/crates/pf-encode/src/enc/linux/mod.rs +++ b/crates/pf-encode/src/enc/linux/mod.rs @@ -468,7 +468,15 @@ impl NvencEncoder { // sessions) and reopen this session without intra-refresh; any other failure — and // any failure when IR wasn't requested — propagates untouched (the bitrate probe // keys on EINVAL, which must not trip the latch). - Err(e) if intra_refresh && format!("{e:#}").contains("Function not implemented") => { + Err(e) + if intra_refresh + && matches!( + e, + ffmpeg::Error::Other { + errno: ffmpeg::util::error::ENOSYS + } + ) => + { tracing::warn!( encoder = name, "NVENC intra-refresh not supported by this GPU — falling back to IDR-only \ diff --git a/crates/pf-encode/src/lib.rs b/crates/pf-encode/src/lib.rs index 4a20be95..ad25d6e4 100644 --- a/crates/pf-encode/src/lib.rs +++ b/crates/pf-encode/src/lib.rs @@ -897,13 +897,29 @@ fn open_nvenc_probed( } // EINVAL = above this GPU's level ceiling → step down. Any other failure (no GPU, // bad mode, OOM) is real — surface it rather than masking it with bitrate retries. - Err(e) if format!("{e:#}").contains("Invalid argument") => last = Some(e), + Err(e) if nvenc_open_einval(&e) => last = Some(e), Err(e) => return Err(e), } } Err(last.unwrap_or_else(|| anyhow::anyhow!("encoder open failed at every probed bitrate"))) } +/// Whether a libav NVENC open failed with EINVAL — the "bitrate above this GPU's level ceiling" +/// signal [`open_nvenc_probed`]'s ladder steps down on. Typed: the root `ffmpeg::Error` survives +/// the `anyhow` context chain, so match it there instead of substring-matching the English +/// strerror rendering of the whole chain — which also fired on any OTHER wrapped EINVAL (e.g. a +/// CUDA-context errno) and steered the ladder on failures that have nothing to do with bitrate. +#[cfg(target_os = "linux")] +fn nvenc_open_einval(e: &anyhow::Error) -> bool { + use ffmpeg_next as ffmpeg; + matches!( + e.downcast_ref::(), + Some(ffmpeg::Error::Other { + errno: ffmpeg::util::error::EINVAL + }) + ) +} + /// Whether the direct-SDK NVENC path is active. **Default ON** — on-glass validated 2026-07-12: /// real RFI landed 73/73 as clean P-frame recovery anchors (never IDR) on an RTX host with a real /// Steam Deck client (design/linux-direct-nvenc.md §9). `PUNKTFUNK_NVENC_DIRECT=0` (also `false`/