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
@@ -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::<RetrieveJob>(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);
}