feat(encode): direct-SDK NVENC on Linux (CUDA input) with real RFI
ci / rust (push) Failing after 1m1s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m9s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 57s
apple / swift (push) Successful in 4m42s
ci / bench (push) Successful in 7m37s
docker / deploy-docs (push) Successful in 22s
flatpak / build-publish (push) Successful in 6m51s
windows-host / package (push) Successful in 14m26s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m59s
arch / build-publish (push) Successful in 16m48s
deb / build-publish (push) Successful in 17m14s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m49s
android / android (push) Successful in 18m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m36s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m1s
release / apple (push) Successful in 22m56s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m40s
apple / screenshots (push) Successful in 18m53s
ci / rust (push) Failing after 1m1s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m9s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 57s
apple / swift (push) Successful in 4m42s
ci / bench (push) Successful in 7m37s
docker / deploy-docs (push) Successful in 22s
flatpak / build-publish (push) Successful in 6m51s
windows-host / package (push) Successful in 14m26s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m59s
arch / build-publish (push) Successful in 16m48s
deb / build-publish (push) Successful in 17m14s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m49s
android / android (push) Successful in 18m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m36s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m1s
release / apple (push) Successful in 22m56s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m40s
apple / screenshots (push) Successful in 18m53s
Phase 5.2 of design/encoder-recovery-hardening.md (design/linux-direct-nvenc.md). The Linux NVIDIA host encodes through libavcodec `hevc_nvenc`, which structurally cannot express `nvEncInvalidateRefFrames` — so every FEC-unrecoverable loss is a full IDR and, since the client freezes-until-reanchor, a per-loss freeze for RTT+IDR-encode. This ports the Windows raw-NVENC backend to NV_ENC_DEVICE_TYPE_CUDA over the shared CUcontext so Linux NVIDIA gets the same real RFI + F2 recovery-anchor + reset() stall lever + HDR-SEI/Main10 plumbing. New `encode/linux/nvenc_cuda.rs` (`NvencCudaEncoder`): - runtime-loaded entry table via `dlopen libnvidia-encode.so.1` (never link-time, mirroring the zerocopy::cuda libcuda loader) — one binary still starts on AMD/Intel Linux boxes and falls through to VAAPI/software; - session on the shared CUcontext (zerocopy::cuda::context()); - an encoder-owned ring of registered CUDADEVICEPTR input surfaces (zerocopy::cuda::InputSurface + a contiguous-NV12 allocator), each captured DeviceBuffer device→device copied in via the existing copy_* helpers — mirrors the libav recycled-hwframe-pool copy, so zero regression vs today; - config/RFI/anchor/reset ported from the Windows backend; sync-only (NVENC async is Windows-only, so that whole subsystem is dropped); - Main10/HDR-SEI wired but inert until a Linux P010 capture path (Phase 5.1). Wired behind PUNKTFUNK_NVENC_DIRECT (default OFF) in open_nvenc_probed; the Windows path is untouched (no shared extraction in v1). Two on-hardware `#[ignore]` smokes added. Validated on .21 (RTX 5070 Ti, driver 610.43.03): builds on Linux under ci-check, clippy-clean, full host suite 272/0, NV12 smoke (8 AUs, real invalidate_ref_frames + recovery_anchor on a P-frame) and YUV444 FREXT smoke (6 AUs, chroma_444) green; Windows compile unaffected. Owed: the client-in-the-loop matrix (RFI-survives-ABR, reset() heal, A/B vs libav) and the default flip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,14 @@ openh264 = "0.9"
|
||||
ashpd = { version = "0.13", features = ["screencast", "remote_desktop"] }
|
||||
ffmpeg-next = "8"
|
||||
libc = "0.2"
|
||||
# Direct-SDK NVENC on Linux (design/linux-direct-nvenc.md): the RAW `sys::nvEncodeAPI` types only —
|
||||
# the entry points are resolved at RUNTIME from the driver's `libnvidia-encode.so.1`
|
||||
# (encode/linux/nvenc_cuda.rs), NOT link-imported, so the same binary starts fine on AMD/Intel
|
||||
# Linux boxes (no NVIDIA driver) and falls through to VAAPI/software. `ci-check` = vendored
|
||||
# bindings + cudarc `dynamic-loading` (no CUDA toolkit/headers at build); we never call the crate's
|
||||
# cudarc — CUDA is driven through the existing `zerocopy::cuda` dlopen table. Same crate + feature
|
||||
# as the Windows target dep (Cargo.toml, windows target section) so the `sys` structs never drift.
|
||||
nvidia-video-codec-sdk = { version = "0.4", features = ["ci-check"], optional = true }
|
||||
# Must match the pipewire crate ashpd 0.13 links (libspa/pipewire-sys `links` key is
|
||||
# unique per build), i.e. 0.9 — NOT the 0.10 the setup doc mentions.
|
||||
pipewire = "0.9"
|
||||
|
||||
@@ -757,6 +757,20 @@ fn open_nvenc_probed(
|
||||
bit_depth: u8,
|
||||
chroma: ChromaFormat,
|
||||
) -> Result<Box<dyn Encoder>> {
|
||||
// Direct-SDK NVENC (design/linux-direct-nvenc.md): opt-in via PUNKTFUNK_NVENC_DIRECT, and only
|
||||
// for a CUDA capture payload (it registers CUDADEVICEPTR inputs — a CPU/dmabuf frame can't feed
|
||||
// it, so those keep the libav path). It self-clamps the bitrate internally (its own level-ceiling
|
||||
// binary search at session open), so it skips the probe-loop stepping below.
|
||||
#[cfg(feature = "nvenc")]
|
||||
if cuda && nvenc_direct_enabled() {
|
||||
tracing::info!(
|
||||
codec = codec.nvenc_name(),
|
||||
"Linux direct-SDK NVENC enabled (PUNKTFUNK_NVENC_DIRECT) — real RFI + recovery anchor"
|
||||
);
|
||||
return Ok(Box::new(nvenc_cuda::NvencCudaEncoder::open(
|
||||
codec, format, width, height, fps, bitrate_bps, cuda, bit_depth, chroma,
|
||||
)?) as Box<dyn Encoder>);
|
||||
}
|
||||
const MIN_PROBE_BPS: u64 = 50_000_000;
|
||||
let mut candidates = vec![bitrate_bps];
|
||||
let cap = codec.max_bitrate_bps();
|
||||
@@ -794,6 +808,17 @@ fn open_nvenc_probed(
|
||||
Err(last.unwrap_or_else(|| anyhow::anyhow!("encoder open failed at every probed bitrate")))
|
||||
}
|
||||
|
||||
/// Whether the operator opted into the direct-SDK NVENC path (`PUNKTFUNK_NVENC_DIRECT` truthy).
|
||||
/// OFF by default until the on-glass matrix (design/linux-direct-nvenc.md §9) is green; then the
|
||||
/// default flips and this becomes the libav escape hatch (`=0`). Only meaningful with `--features
|
||||
/// nvenc`.
|
||||
#[cfg(all(target_os = "linux", feature = "nvenc"))]
|
||||
fn nvenc_direct_enabled() -> bool {
|
||||
std::env::var("PUNKTFUNK_NVENC_DIRECT")
|
||||
.map(|v| matches!(v.trim(), "1" | "true" | "yes" | "on"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Cheap, side-effect-free NVIDIA-presence probe for the `auto` backend selector: the NVIDIA
|
||||
/// kernel driver exposes these device nodes, AMD/Intel boxes have neither. Deliberately does NOT
|
||||
/// create a CUDA context (that would allocate GPU state on every host that merely *might* be
|
||||
@@ -1131,6 +1156,13 @@ mod amf;
|
||||
mod ffmpeg_win;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
// Direct-SDK NVENC on Linux (CUDA input; design/linux-direct-nvenc.md) — real RFI + recovery anchor
|
||||
// + reset() lever the libavcodec `linux::NvencEncoder` can't express. Opt-in behind
|
||||
// `PUNKTFUNK_NVENC_DIRECT` until on-glass validated; the `.so` resolves at runtime like the Windows
|
||||
// path, so `--features nvenc` stays safe on a driver-less/AMD Linux box.
|
||||
#[cfg(all(target_os = "linux", feature = "nvenc"))]
|
||||
#[path = "encode/linux/nvenc_cuda.rs"]
|
||||
mod nvenc_cuda;
|
||||
#[cfg(all(target_os = "windows", feature = "nvenc"))]
|
||||
#[path = "encode/windows/nvenc.rs"]
|
||||
mod nvenc;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -687,6 +687,88 @@ fn alloc_pitched_nv12(
|
||||
Ok(((y_ptr, y_pitch), (uv_ptr, uv_pitch)))
|
||||
}
|
||||
|
||||
/// Allocate ONE pitched buffer holding a *contiguous* NV12 surface — Y rows `[0, H)` immediately
|
||||
/// followed by interleaved-chroma rows `[H, 3H/2)`, all at the driver's single pitch. Unlike
|
||||
/// [`alloc_pitched_nv12`] (two separate allocations, the capture/IPC layout) this is the layout the
|
||||
/// direct-SDK NVENC encoder registers as a single `CUDADEVICEPTR` input: NVENC reads the UV plane
|
||||
/// at `ptr + pitch*height`. Used only by [`InputSurface`] (encode side), never the wire.
|
||||
fn alloc_pitched_nv12_contiguous(width: u32, height: u32) -> Result<(CUdeviceptr, usize)> {
|
||||
let mut ptr: CUdeviceptr = 0;
|
||||
let mut pitch: usize = 0;
|
||||
// Y is `width` bytes/row × H rows; the interleaved chroma plane is W/2 samples × 2 bytes =
|
||||
// `width` bytes/row × H/2 rows. One allocation of `H + H/2` rows keeps them contiguous under a
|
||||
// single pitch so NVENC finds UV at `ptr + pitch*H`.
|
||||
let rows = height as usize + (height as usize / 2).max(1);
|
||||
// SAFETY: `cuMemAllocPitch_v2` (wrapper → live table) writes the allocation pointer and pitch
|
||||
// into the two live, distinct stack out-params `&mut ptr`/`&mut pitch`, which outlive the
|
||||
// synchronous call; width/rows/element-size are by-value ints. No aliasing.
|
||||
unsafe {
|
||||
ck(
|
||||
cuMemAllocPitch_v2(&mut ptr, &mut pitch, width as usize, rows, 16),
|
||||
"cuMemAllocPitch_v2(NV12 contiguous)",
|
||||
)?;
|
||||
}
|
||||
Ok((ptr, pitch))
|
||||
}
|
||||
|
||||
/// An encoder-owned, contiguous pitched CUDA surface that the direct-SDK NVENC Linux backend
|
||||
/// (`encode/linux/nvenc_cuda.rs`, design/linux-direct-nvenc.md) registers **once** as a
|
||||
/// `NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR` input and copies each captured frame into (via the
|
||||
/// `copy_*_to_device` helpers) before `encode_picture`. Distinct from [`DeviceBuffer`]: these are
|
||||
/// laid out exactly as NVENC's single-pointer register expects — NV12 = Y then interleaved-UV under
|
||||
/// one pitch, YUV444 = Y|U|V stacked, RGB = packed 4-byte — and are never pooled or sent on the
|
||||
/// wire. Frees its allocation on drop (context made current first, since drop may run off-thread).
|
||||
pub struct InputSurface {
|
||||
/// Base device pointer NVENC registers. For NV12 the chroma plane lives at `ptr + pitch*height`;
|
||||
/// for YUV444 the U/V planes at `ptr + pitch*height` / `ptr + 2*pitch*height`.
|
||||
pub ptr: CUdeviceptr,
|
||||
/// Row stride in bytes (the driver's pitch), shared by every plane of the surface.
|
||||
pub pitch: usize,
|
||||
/// Luma height in rows — the plane stride multiplier NVENC / the copy helpers key off.
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl InputSurface {
|
||||
/// Contiguous NV12 (8-bit 4:2:0): one allocation, Y then interleaved UV under one pitch.
|
||||
pub fn alloc_nv12(width: u32, height: u32) -> Result<InputSurface> {
|
||||
let (ptr, pitch) = alloc_pitched_nv12_contiguous(width, height)?;
|
||||
Ok(InputSurface { ptr, pitch, height })
|
||||
}
|
||||
|
||||
/// Planar YUV444 (8-bit 4:4:4): one allocation, Y|U|V full-res planes stacked (see
|
||||
/// [`alloc_pitched_yuv444`]).
|
||||
pub fn alloc_yuv444(width: u32, height: u32) -> Result<InputSurface> {
|
||||
let (ptr, pitch) = alloc_pitched_yuv444(width, height)?;
|
||||
Ok(InputSurface { ptr, pitch, height })
|
||||
}
|
||||
|
||||
/// Packed 4-byte RGB/BGRx: one contiguous pitched allocation (NVENC does the internal CSC when
|
||||
/// registered as an `ABGR`/`ARGB` input).
|
||||
pub fn alloc_rgb(width: u32, height: u32) -> Result<InputSurface> {
|
||||
let (ptr, pitch) = alloc_pitched(width, height)?;
|
||||
Ok(InputSurface { ptr, pitch, height })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for InputSurface {
|
||||
fn drop(&mut self) {
|
||||
if self.ptr == 0 {
|
||||
return;
|
||||
}
|
||||
// SAFETY: this surface exclusively owns `self.ptr` (a single `cuMemAllocPitch_v2` allocation
|
||||
// from one of the constructors above), freed exactly once here — `drop` runs once and the
|
||||
// `ptr == 0` guard skips a moved-out/empty surface, so no double-free. The shared context is
|
||||
// made current first because drop may run on a thread where it isn't, and `cuMemFree_v2`
|
||||
// needs it. Wrapper → live table; result ignored (best-effort teardown).
|
||||
unsafe {
|
||||
if let Some(c) = CONTEXT.get() {
|
||||
let _ = cuCtxSetCurrent(c.0);
|
||||
}
|
||||
let _ = cuMemFree_v2(self.ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Free-list of recycled device allocations for one resolution. Shared (via `Arc`) between the
|
||||
/// capture thread that hands out buffers and the encode thread where a [`DeviceBuffer`] drops and
|
||||
/// returns its allocation here. Bulk-freed when the last reference drops. For NV12 each free entry
|
||||
|
||||
Reference in New Issue
Block a user