fix(host): actionable NVENC error logging — drop misleading "(no NVIDIA GPU?)"
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 1m2s
apple / swift (push) Successful in 1m10s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 40s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 56s
apple / screenshots (push) Successful in 5m24s
ci / bench (push) Successful in 7m32s
docker / deploy-docs (push) Successful in 28s
arch / build-publish (push) Successful in 11m51s
windows-host / package (push) Successful in 14m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m52s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m54s
android / android (push) Successful in 17m12s
deb / build-publish (push) Successful in 18m12s
ci / rust (push) Successful in 24m52s

Every NVENC entry-point failure was annotated "(no NVIDIA GPU?)", which
misled triage: the direct-NVENC path only loads on a machine that HAS an
NVIDIA GPU. A Linux user hit NV_ENC_ERR_INVALID_VERSION at
open_encode_session_ex (past the NvEncodeAPIGetMaxSupportedVersion pre-flight
gate) — the signature of a userspace/kernel driver version skew that a host
reboot fixes — and the log pointed at a missing GPU instead. A restart did
fix it.

Add encode/nvenc_status.rs: a shared NVENCSTATUS -> cause mapper that folds
the real cause into the anyhow::Error at construction, so every downstream
{e:#} log (the encode-recovery loop, session teardown) improves for free.
INVALID_VERSION now reads "update the NVIDIA driver, or reboot if you just
updated it (a host restart is the usual fix)"; NO_ENCODE_DEVICE /
DEVICE_NOT_EXIST / INCOMPATIBLE_CLIENT_KEY (session-count limit) / OOM /
UNSUPPORTED_PARAM get their own glosses. The required API version comes from
the SDK consts so it stays correct across crate bumps.

Wire it into all NVENC entry-point failures in both backends
(encode/linux/nvenc_cuda.rs, encode/windows/nvenc.rs) — every open, init,
preset/resource/bitstream call.

Also: when the encode-recovery loop exhausts its in-place rebuilds it now
logs a clear terminal line with the underlying cause instead of the session
silently vanishing after the last identical "rebuilt in place" line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-16 01:13:53 +02:00
parent ab4c9e44cc
commit f901bedf22
5 changed files with 122 additions and 26 deletions
+5
View File
@@ -1379,6 +1379,11 @@ mod nvenc;
#[cfg(all(target_os = "linux", feature = "nvenc"))] #[cfg(all(target_os = "linux", feature = "nvenc"))]
#[path = "encode/linux/nvenc_cuda.rs"] #[path = "encode/linux/nvenc_cuda.rs"]
mod nvenc_cuda; mod nvenc_cuda;
// Actionable `NVENCSTATUS` → cause mapping shared by both direct-NVENC backends, so a failed
// session open logs "update/reboot the driver" instead of the old misleading "(no NVIDIA GPU?)".
#[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "nvenc"))]
#[path = "encode/nvenc_status.rs"]
mod nvenc_status;
// Software (openh264) H.264 encoder — the GPU-less path on BOTH Windows and Linux (a headless / // Software (openh264) H.264 encoder — the GPU-less path on BOTH Windows and Linux (a headless /
// GPU-less test box, or a fallback when no hardware encoder is available). Platform-agnostic: it // GPU-less test box, or a fallback when no hardware encoder is available). Platform-agnostic: it
// consumes CPU RGB `CapturedFrame`s and the statically-bundled openh264 build. // consumes CPU RGB `CapturedFrame`s and the statically-bundled openh264 build.
@@ -31,6 +31,7 @@
// Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it. // Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it.
#![deny(clippy::undocumented_unsafe_blocks)] #![deny(clippy::undocumented_unsafe_blocks)]
use super::nvenc_status;
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
use crate::capture::{CapturedFrame, FramePayload}; use crate::capture::{CapturedFrame, FramePayload};
use crate::zerocopy::cuda::{self, InputSurface}; use crate::zerocopy::cuda::{self, InputSurface};
@@ -448,9 +449,7 @@ impl NvencCudaEncoder {
let mut enc: *mut c_void = ptr::null_mut(); let mut enc: *mut c_void = ptr::null_mut();
(api().open_encode_session_ex)(&mut params, &mut enc) (api().open_encode_session_ex)(&mut params, &mut enc)
.nv_ok() .nv_ok()
.map_err(|e| { .map_err(|e| nvenc_status::call_err("open_encode_session_ex (caps probe)", e))?;
anyhow!("NVENC open_encode_session_ex (caps probe): {e:?} (no NVIDIA GPU?)")
})?;
let wmax = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_WIDTH_MAX); let wmax = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_WIDTH_MAX);
let hmax = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_HEIGHT_MAX); let hmax = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_HEIGHT_MAX);
let yuv444 = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE); let yuv444 = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE);
@@ -512,7 +511,7 @@ impl NvencCudaEncoder {
&mut preset, &mut preset,
) )
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("get_encode_preset_config_ex: {e:?}"))?; .map_err(|e| nvenc_status::call_err("get_encode_preset_config_ex", e))?;
let mut cfg = preset.presetCfg; let mut cfg = preset.presetCfg;
// CBR, infinite GOP, P-only, ~1-frame VBV (mirror the Windows/Linux-libav RC config). // CBR, infinite GOP, P-only, ~1-frame VBV (mirror the Windows/Linux-libav RC config).
@@ -683,7 +682,7 @@ impl NvencCudaEncoder {
let mut enc: *mut c_void = ptr::null_mut(); let mut enc: *mut c_void = ptr::null_mut();
(api().open_encode_session_ex)(&mut params, &mut enc) (api().open_encode_session_ex)(&mut params, &mut enc)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("NVENC open_encode_session_ex: {e:?} (no NVIDIA GPU?)"))?; .map_err(|e| nvenc_status::call_err("open_encode_session_ex", e))?;
let mut cfg = match self.build_config(enc, bitrate) { let mut cfg = match self.build_config(enc, bitrate) {
Ok(cfg) => cfg, Ok(cfg) => cfg,
@@ -698,7 +697,7 @@ impl NvencCudaEncoder {
Ok(()) => Ok(enc), Ok(()) => Ok(enc),
Err(e) => { Err(e) => {
let _ = (api().destroy_encoder)(enc); let _ = (api().destroy_encoder)(enc);
Err(anyhow!("initialize_encoder: {e:?}")) Err(nvenc_status::call_err("initialize_encoder", e))
} }
} }
} }
@@ -818,7 +817,7 @@ impl NvencCudaEncoder {
}; };
(api().create_bitstream_buffer)(enc, &mut cb) (api().create_bitstream_buffer)(enc, &mut cb)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("create_bitstream_buffer: {e:?}"))?; .map_err(|e| nvenc_status::call_err("create_bitstream_buffer", e))?;
self.bitstreams.push(cb.bitstreamBuffer); self.bitstreams.push(cb.bitstreamBuffer);
} }
@@ -849,7 +848,7 @@ impl NvencCudaEncoder {
}; };
(api().register_resource)(self.encoder, &mut rr) (api().register_resource)(self.encoder, &mut rr)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("register_resource (CUDADEVICEPTR): {e:?}"))?; .map_err(|e| nvenc_status::call_err("register_resource (CUDADEVICEPTR)", e))?;
self.ring.push(RingSlot { self.ring.push(RingSlot {
surface, surface,
reg: rr.registeredResource, reg: rr.registeredResource,
@@ -1014,7 +1013,7 @@ impl Encoder for NvencCudaEncoder {
}; };
(api().map_input_resource)(self.encoder, &mut mp) (api().map_input_resource)(self.encoder, &mut mp)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("map_input_resource: {e:?}"))?; .map_err(|e| nvenc_status::call_err("map_input_resource", e))?;
let pts = self.frame_idx as u64; let pts = self.frame_idx as u64;
self.frame_idx += 1; self.frame_idx += 1;
@@ -1086,7 +1085,7 @@ impl Encoder for NvencCudaEncoder {
} }
(api().encode_picture)(self.encoder, &mut pic) (api().encode_picture)(self.encoder, &mut pic)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("encode_picture: {e:?}"))?; .map_err(|e| nvenc_status::call_err("encode_picture", e))?;
self.pending.push_back(( self.pending.push_back((
self.bitstreams[slot], self.bitstreams[slot],
mp.mappedResource, mp.mappedResource,
@@ -1186,7 +1185,7 @@ impl Encoder for NvencCudaEncoder {
}; };
(api().lock_bitstream)(self.encoder, &mut lock) (api().lock_bitstream)(self.encoder, &mut lock)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("lock_bitstream: {e:?}"))?; .map_err(|e| nvenc_status::call_err("lock_bitstream", e))?;
let data = std::slice::from_raw_parts( let data = std::slice::from_raw_parts(
lock.bitstreamBufferPtr as *const u8, lock.bitstreamBufferPtr as *const u8,
lock.bitstreamSizeInBytes as usize, lock.bitstreamSizeInBytes as usize,
@@ -1198,7 +1197,7 @@ impl Encoder for NvencCudaEncoder {
); );
(api().unlock_bitstream)(self.encoder, bs) (api().unlock_bitstream)(self.encoder, bs)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("unlock_bitstream: {e:?}"))?; .map_err(|e| nvenc_status::call_err("unlock_bitstream", e))?;
if !map.is_null() { if !map.is_null() {
let _ = (api().unmap_input_resource)(self.encoder, map); let _ = (api().unmap_input_resource)(self.encoder, map);
} }
@@ -0,0 +1,80 @@
//! Actionable explanations for `NVENCSTATUS` failures — shared by the direct-SDK NVENC backends on
//! Windows (`encode/windows/nvenc.rs`) and Linux (`encode/linux/nvenc_cuda.rs`).
//!
//! Every NVENC entry-point failure used to be annotated `(no NVIDIA GPU?)`, which actively misled
//! triage: the direct-NVENC path only loads on a machine that HAS an NVIDIA GPU, and the failure a
//! user actually hit — `NV_ENC_ERR_INVALID_VERSION` from a userspace/kernel driver version skew,
//! fixed by a reboot — has nothing to do with a missing GPU. This maps each status to what it really
//! means and what the operator should do, and folds that cause into the `anyhow::Error` at
//! construction, so every downstream `{e:#}` log (the encode-recovery loop, session teardown) says
//! the useful thing without extra plumbing.
use nvidia_video_codec_sdk::sys::nvEncodeAPI as nv;
/// A one-line, operator-actionable cause for an NVENC status. Does not repeat the raw code —
/// callers print that alongside (see [`call_err`]). Public for the few sites that build a
/// `String`/`format!` error instead of an `anyhow::Error`.
pub(super) fn explain(status: nv::NVENCSTATUS) -> String {
match status {
// The one this whole module exists for: a version word the driver rejects. Either the
// driver is genuinely older than our headers, or (the sneaky case) the userspace
// `libnvidia-encode` reports a new-enough version to the pre-flight probe but the running
// kernel module is older and rejects the session — the classic "updated the driver, didn't
// reboot" skew. Both heal the same way.
nv::NVENCSTATUS::NV_ENC_ERR_INVALID_VERSION => format!(
"the NVIDIA driver is older than this build's NVENC headers (needs NVENC API {}.{} or \
newer), or the userspace and kernel-module driver versions are mismatched — common \
right after a driver update without a reboot. Update the NVIDIA driver, or reboot if \
you just updated it (a host restart is the usual fix).",
nv::NVENCAPI_MAJOR_VERSION,
nv::NVENCAPI_MINOR_VERSION,
),
nv::NVENCSTATUS::NV_ENC_ERR_NO_ENCODE_DEVICE => {
"this GPU exposes no usable NVENC engine — it has no hardware video encoder, or NVENC is \
disabled on this card"
.to_string()
}
nv::NVENCSTATUS::NV_ENC_ERR_UNSUPPORTED_DEVICE => {
"this GPU model is not supported by NVENC".to_string()
}
nv::NVENCSTATUS::NV_ENC_ERR_INVALID_ENCODERDEVICE
| nv::NVENCSTATUS::NV_ENC_ERR_INVALID_DEVICE => {
"the device/context handed to NVENC is invalid — a GPU reset or driver reload can cause \
this"
.to_string()
}
nv::NVENCSTATUS::NV_ENC_ERR_DEVICE_NOT_EXIST => {
"the NVENC device no longer exists — the driver reset, or the GPU fell off the bus"
.to_string()
}
nv::NVENCSTATUS::NV_ENC_ERR_OUT_OF_MEMORY => "the GPU is out of memory".to_string(),
nv::NVENCSTATUS::NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY => {
"NVENC rejected the client key — the GeForce concurrent-NVENC-session limit was reached, \
or the driver is unlicensed for this many encoders"
.to_string()
}
nv::NVENCSTATUS::NV_ENC_ERR_UNIMPLEMENTED
| nv::NVENCSTATUS::NV_ENC_ERR_UNSUPPORTED_PARAM => {
"this driver/GPU does not implement the requested NVENC encode mode".to_string()
}
nv::NVENCSTATUS::NV_ENC_ERR_INVALID_PARAM => {
"NVENC rejected a parameter — an encode mode this GPU does not support".to_string()
}
nv::NVENCSTATUS::NV_ENC_ERR_ENCODER_BUSY => {
"the NVENC engine is busy — retry, or reduce the number of concurrent encode sessions"
.to_string()
}
nv::NVENCSTATUS::NV_ENC_ERR_GENERIC => {
"the NVIDIA driver returned a generic NVENC failure — check dmesg and the driver install"
.to_string()
}
other => format!("unexpected NVENC status ({other:?})"),
}
}
/// Build an actionable `anyhow::Error` for a failed NVENC entry-point call. `call` names the API
/// (e.g. `"open_encode_session_ex"`); the message carries both the raw status and its real-world
/// cause, so triage never again reads a version mismatch as "(no NVIDIA GPU?)".
pub(super) fn call_err(call: &str, status: nv::NVENCSTATUS) -> anyhow::Error {
anyhow::anyhow!("NVENC {call} failed: {status:?} — {}", explain(status))
}
@@ -36,6 +36,7 @@
// Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it. // Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it.
#![deny(clippy::undocumented_unsafe_blocks)] #![deny(clippy::undocumented_unsafe_blocks)]
use super::nvenc_status;
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
use crate::capture::{CapturedFrame, FramePayload, PixelFormat}; use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
@@ -381,7 +382,10 @@ fn retrieve_loop(
let _ = (api().unlock_bitstream)(enc as *mut c_void, job.bs as *mut c_void); let _ = (api().unlock_bitstream)(enc as *mut c_void, job.bs as *mut c_void);
Ok((data, keyframe)) Ok((data, keyframe))
} }
Err(e) => Err(format!("lock_bitstream (async): {e:?}")), Err(e) => Err(format!(
"lock_bitstream (async): {e:?}{}",
nvenc_status::explain(e)
)),
} }
} }
}; };
@@ -635,9 +639,7 @@ impl NvencD3d11Encoder {
let mut enc: *mut c_void = ptr::null_mut(); let mut enc: *mut c_void = ptr::null_mut();
(api().open_encode_session_ex)(&mut params, &mut enc) (api().open_encode_session_ex)(&mut params, &mut enc)
.nv_ok() .nv_ok()
.map_err(|e| { .map_err(|e| nvenc_status::call_err("open_encode_session_ex (caps probe)", e))?;
anyhow!("NVENC open_encode_session_ex (caps probe): {e:?} (no NVIDIA GPU?)")
})?;
let wmax = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_WIDTH_MAX); let wmax = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_WIDTH_MAX);
let hmax = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_HEIGHT_MAX); let hmax = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_HEIGHT_MAX);
let ten_bit = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_10BIT_ENCODE); let ten_bit = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_10BIT_ENCODE);
@@ -712,7 +714,7 @@ impl NvencD3d11Encoder {
&mut preset, &mut preset,
) )
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("get_encode_preset_config_ex: {e:?}"))?; .map_err(|e| nvenc_status::call_err("get_encode_preset_config_ex", e))?;
let mut cfg = preset.presetCfg; let mut cfg = preset.presetCfg;
// Mirror the Linux RC config: CBR, infinite GOP, P-only, ~1-frame VBV. // Mirror the Linux RC config: CBR, infinite GOP, P-only, ~1-frame VBV.
@@ -948,7 +950,7 @@ impl NvencD3d11Encoder {
let mut enc: *mut c_void = ptr::null_mut(); let mut enc: *mut c_void = ptr::null_mut();
(api().open_encode_session_ex)(&mut params, &mut enc) (api().open_encode_session_ex)(&mut params, &mut enc)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("NVENC open_encode_session_ex: {e:?} (no NVIDIA GPU?)"))?; .map_err(|e| nvenc_status::call_err("open_encode_session_ex", e))?;
let mut cfg = match self.build_config(enc, bitrate) { let mut cfg = match self.build_config(enc, bitrate) {
Ok(cfg) => cfg, Ok(cfg) => cfg,
@@ -963,7 +965,7 @@ impl NvencD3d11Encoder {
Ok(()) => Ok(enc), Ok(()) => Ok(enc),
Err(e) => { Err(e) => {
let _ = (api().destroy_encoder)(enc); let _ = (api().destroy_encoder)(enc);
Err(anyhow!("initialize_encoder: {e:?}")) Err(nvenc_status::call_err("initialize_encoder", e))
} }
} }
} }
@@ -1139,7 +1141,7 @@ impl NvencD3d11Encoder {
}; };
(api().create_bitstream_buffer)(enc, &mut cb) (api().create_bitstream_buffer)(enc, &mut cb)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("create_bitstream_buffer: {e:?}"))?; .map_err(|e| nvenc_status::call_err("create_bitstream_buffer", e))?;
self.bitstreams.push(cb.bitstreamBuffer); self.bitstreams.push(cb.bitstreamBuffer);
} }
// Async retrieve: one auto-reset completion event per pool bitstream, registered with // Async retrieve: one auto-reset completion event per pool bitstream, registered with
@@ -1156,7 +1158,7 @@ impl NvencD3d11Encoder {
}; };
(api().register_async_event)(enc, &mut ep) (api().register_async_event)(enc, &mut ep)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("register_async_event: {e:?}"))?; .map_err(|e| nvenc_status::call_err("register_async_event", e))?;
self.events.push(ev.0 as usize); self.events.push(ev.0 as usize);
} }
let (work_tx, work_rx) = mpsc::sync_channel::<RetrieveJob>(POOL); let (work_tx, work_rx) = mpsc::sync_channel::<RetrieveJob>(POOL);
@@ -1372,7 +1374,7 @@ impl Encoder for NvencD3d11Encoder {
}; };
(api().register_resource)(self.encoder, &mut rr) (api().register_resource)(self.encoder, &mut rr)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("register_resource: {e:?}"))?; .map_err(|e| nvenc_status::call_err("register_resource", e))?;
self.regs self.regs
.insert(key, (rr.registeredResource, frame.texture.clone())); .insert(key, (rr.registeredResource, frame.texture.clone()));
} }
@@ -1385,7 +1387,7 @@ impl Encoder for NvencD3d11Encoder {
}; };
(api().map_input_resource)(self.encoder, &mut mp) (api().map_input_resource)(self.encoder, &mut mp)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("map_input_resource: {e:?}"))?; .map_err(|e| nvenc_status::call_err("map_input_resource", e))?;
let pts = self.frame_idx as u64; let pts = self.frame_idx as u64;
self.frame_idx += 1; self.frame_idx += 1;
@@ -1469,7 +1471,7 @@ impl Encoder for NvencD3d11Encoder {
} }
(api().encode_picture)(self.encoder, &mut pic) (api().encode_picture)(self.encoder, &mut pic)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("encode_picture: {e:?}"))?; .map_err(|e| nvenc_status::call_err("encode_picture", e))?;
self.pending.push_back(( self.pending.push_back((
self.bitstreams[slot], self.bitstreams[slot],
mp.mappedResource, mp.mappedResource,
@@ -1629,7 +1631,7 @@ impl Encoder for NvencD3d11Encoder {
}; };
(api().lock_bitstream)(self.encoder, &mut lock) (api().lock_bitstream)(self.encoder, &mut lock)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("lock_bitstream: {e:?}"))?; .map_err(|e| nvenc_status::call_err("lock_bitstream", e))?;
let data = std::slice::from_raw_parts( let data = std::slice::from_raw_parts(
lock.bitstreamBufferPtr as *const u8, lock.bitstreamBufferPtr as *const u8,
lock.bitstreamSizeInBytes as usize, lock.bitstreamSizeInBytes as usize,
@@ -1641,7 +1643,7 @@ impl Encoder for NvencD3d11Encoder {
); );
(api().unlock_bitstream)(self.encoder, bs) (api().unlock_bitstream)(self.encoder, bs)
.nv_ok() .nv_ok()
.map_err(|e| anyhow!("unlock_bitstream: {e:?}"))?; .map_err(|e| nvenc_status::call_err("unlock_bitstream", e))?;
if !map.is_null() { if !map.is_null() {
let _ = (api().unmap_input_resource)(self.encoder, map); let _ = (api().unmap_input_resource)(self.encoder, map);
} }
+10
View File
@@ -4919,6 +4919,16 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
if encoder_resets > MAX_ENCODER_RESETS if encoder_resets > MAX_ENCODER_RESETS
|| !reset_stalled_encoder(&mut enc, &mut inflight) || !reset_stalled_encoder(&mut enc, &mut inflight)
{ {
// Terminal: rebuilds are exhausted (or the backend can't rebuild in place). Say so
// plainly with the underlying cause — the per-reset lines above only ever repeat
// "rebuilt in place", so without this the session just vanishes. The error carries
// its own actionable text now (e.g. an NVENC version mismatch → "update/reboot the
// driver"), so this is the one line an operator needs.
tracing::error!(
error = %format!("{e:#}"),
resets = encoder_resets,
"encoder did not recover after repeated in-place rebuilds — ending the video \
session (see the error above for the cause)");
return Err(e).context("encoder submit"); return Err(e).context("encoder submit");
} }
tracing::error!(error = %format!("{e:#}"), reset = encoder_resets, tracing::error!(error = %format!("{e:#}"), reset = encoder_resets,