fix(encode): classify libav-NVENC open failures by errno, not English strerror text

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 01:05:24 +02:00
co-authored by Claude Fable 5
parent 36589c39da
commit 25765c53ec
2 changed files with 26 additions and 2 deletions
+9 -1
View File
@@ -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 \
+17 -1
View File
@@ -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::<ffmpeg::Error>(),
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`/