diff --git a/crates/punktfunk-host/src/encode.rs b/crates/punktfunk-host/src/encode.rs index e5683188..5aa314b6 100644 --- a/crates/punktfunk-host/src/encode.rs +++ b/crates/punktfunk-host/src/encode.rs @@ -1379,6 +1379,11 @@ mod nvenc; #[cfg(all(target_os = "linux", feature = "nvenc"))] #[path = "encode/linux/nvenc_cuda.rs"] 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 / // 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. diff --git a/crates/punktfunk-host/src/encode/linux/nvenc_cuda.rs b/crates/punktfunk-host/src/encode/linux/nvenc_cuda.rs index 5bd8831a..0a677e6d 100644 --- a/crates/punktfunk-host/src/encode/linux/nvenc_cuda.rs +++ b/crates/punktfunk-host/src/encode/linux/nvenc_cuda.rs @@ -31,6 +31,7 @@ // Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it. #![deny(clippy::undocumented_unsafe_blocks)] +use super::nvenc_status; use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use crate::capture::{CapturedFrame, FramePayload}; use crate::zerocopy::cuda::{self, InputSurface}; @@ -448,9 +449,7 @@ impl NvencCudaEncoder { let mut enc: *mut c_void = ptr::null_mut(); (api().open_encode_session_ex)(&mut params, &mut enc) .nv_ok() - .map_err(|e| { - anyhow!("NVENC open_encode_session_ex (caps probe): {e:?} (no NVIDIA GPU?)") - })?; + .map_err(|e| nvenc_status::call_err("open_encode_session_ex (caps probe)", e))?; 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 yuv444 = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE); @@ -512,7 +511,7 @@ impl NvencCudaEncoder { &mut preset, ) .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; // 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(); (api().open_encode_session_ex)(&mut params, &mut enc) .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) { Ok(cfg) => cfg, @@ -698,7 +697,7 @@ impl NvencCudaEncoder { Ok(()) => Ok(enc), Err(e) => { 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) .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); } @@ -849,7 +848,7 @@ impl NvencCudaEncoder { }; (api().register_resource)(self.encoder, &mut rr) .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 { surface, reg: rr.registeredResource, @@ -1014,7 +1013,7 @@ impl Encoder for NvencCudaEncoder { }; (api().map_input_resource)(self.encoder, &mut mp) .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; self.frame_idx += 1; @@ -1086,7 +1085,7 @@ impl Encoder for NvencCudaEncoder { } (api().encode_picture)(self.encoder, &mut pic) .nv_ok() - .map_err(|e| anyhow!("encode_picture: {e:?}"))?; + .map_err(|e| nvenc_status::call_err("encode_picture", e))?; self.pending.push_back(( self.bitstreams[slot], mp.mappedResource, @@ -1186,7 +1185,7 @@ impl Encoder for NvencCudaEncoder { }; (api().lock_bitstream)(self.encoder, &mut lock) .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( lock.bitstreamBufferPtr as *const u8, lock.bitstreamSizeInBytes as usize, @@ -1198,7 +1197,7 @@ impl Encoder for NvencCudaEncoder { ); (api().unlock_bitstream)(self.encoder, bs) .nv_ok() - .map_err(|e| anyhow!("unlock_bitstream: {e:?}"))?; + .map_err(|e| nvenc_status::call_err("unlock_bitstream", e))?; if !map.is_null() { let _ = (api().unmap_input_resource)(self.encoder, map); } diff --git a/crates/punktfunk-host/src/encode/nvenc_status.rs b/crates/punktfunk-host/src/encode/nvenc_status.rs new file mode 100644 index 00000000..e7672a6f --- /dev/null +++ b/crates/punktfunk-host/src/encode/nvenc_status.rs @@ -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)) +} diff --git a/crates/punktfunk-host/src/encode/windows/nvenc.rs b/crates/punktfunk-host/src/encode/windows/nvenc.rs index 4953e750..769b44b8 100644 --- a/crates/punktfunk-host/src/encode/windows/nvenc.rs +++ b/crates/punktfunk-host/src/encode/windows/nvenc.rs @@ -36,6 +36,7 @@ // Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it. #![deny(clippy::undocumented_unsafe_blocks)] +use super::nvenc_status; use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use crate::capture::{CapturedFrame, FramePayload, PixelFormat}; 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); 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(); (api().open_encode_session_ex)(&mut params, &mut enc) .nv_ok() - .map_err(|e| { - anyhow!("NVENC open_encode_session_ex (caps probe): {e:?} (no NVIDIA GPU?)") - })?; + .map_err(|e| nvenc_status::call_err("open_encode_session_ex (caps probe)", e))?; 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 ten_bit = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_10BIT_ENCODE); @@ -712,7 +714,7 @@ impl NvencD3d11Encoder { &mut preset, ) .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; // 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(); (api().open_encode_session_ex)(&mut params, &mut enc) .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) { Ok(cfg) => cfg, @@ -963,7 +965,7 @@ impl NvencD3d11Encoder { Ok(()) => Ok(enc), Err(e) => { 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) .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); } // 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) .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); } let (work_tx, work_rx) = mpsc::sync_channel::(POOL); @@ -1372,7 +1374,7 @@ impl Encoder for NvencD3d11Encoder { }; (api().register_resource)(self.encoder, &mut rr) .nv_ok() - .map_err(|e| anyhow!("register_resource: {e:?}"))?; + .map_err(|e| nvenc_status::call_err("register_resource", e))?; self.regs .insert(key, (rr.registeredResource, frame.texture.clone())); } @@ -1385,7 +1387,7 @@ impl Encoder for NvencD3d11Encoder { }; (api().map_input_resource)(self.encoder, &mut mp) .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; self.frame_idx += 1; @@ -1469,7 +1471,7 @@ impl Encoder for NvencD3d11Encoder { } (api().encode_picture)(self.encoder, &mut pic) .nv_ok() - .map_err(|e| anyhow!("encode_picture: {e:?}"))?; + .map_err(|e| nvenc_status::call_err("encode_picture", e))?; self.pending.push_back(( self.bitstreams[slot], mp.mappedResource, @@ -1629,7 +1631,7 @@ impl Encoder for NvencD3d11Encoder { }; (api().lock_bitstream)(self.encoder, &mut lock) .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( lock.bitstreamBufferPtr as *const u8, lock.bitstreamSizeInBytes as usize, @@ -1641,7 +1643,7 @@ impl Encoder for NvencD3d11Encoder { ); (api().unlock_bitstream)(self.encoder, bs) .nv_ok() - .map_err(|e| anyhow!("unlock_bitstream: {e:?}"))?; + .map_err(|e| nvenc_status::call_err("unlock_bitstream", e))?; if !map.is_null() { let _ = (api().unmap_input_resource)(self.encoder, map); } diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index 2542a033..8fa0b8ab 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -4919,6 +4919,16 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> { if encoder_resets > MAX_ENCODER_RESETS || !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"); } tracing::error!(error = %format!("{e:#}"), reset = encoder_resets,