Skip to main content

pixelflux/encoders/
nvenc.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! NVENC hardware H.264 encoder: CUDA-bound sessions that encode ARGB — or raw NV12 / YUV444 —
8//! frames from the X11 host-ARGB and Wayland dmabuf capture paths.
9//!
10//! The module dynamically loads `libcuda`, `libnvidia-encode` and `libEGL` at runtime, negotiates
11//! the NVENC API version against the installed driver (set-once per process), and stamps every
12//! NVENCAPI struct with the exact `NV_ENC_*_VER` word the negotiated SDK defines, so one binary
13//! drives drivers from NVENC 10.0 (~R445) through 13.0. Frames reach the GPU three ways: a
14//! zero-copy dmabuf import (EGLImage → CUDA, the mapped plane registered with NVENC in place as
15//! pitch-linear memory or as a CUDA array), a pinned host→device upload of packed BGRA / RGBA that
16//! the hardware CSC converts, and a raw planar upload. Sessions reconfigure resolution and rate
17//! control in place, so a resize or bitrate change costs a few milliseconds instead of a full
18//! rebuild.
19
20// The NVENC and CUDA entry points are called through function pointers
21// resolved at runtime, so the safety contract is carried by the function
22// signatures rather than by a block around each call.
23#![allow(unsafe_op_in_unsafe_fn)]
24
25#![allow(non_camel_case_types)]
26#![allow(non_snake_case)]
27
28use std::collections::HashMap;
29use std::ffi::{c_char, c_void, CStr, CString};
30use std::os::unix::io::AsRawFd;
31use std::ptr;
32use std::sync::Arc;
33
34use libloading::{Library, Symbol};
35use smithay::backend::allocator::{dmabuf::Dmabuf, Buffer, Fourcc};
36
37use crate::RustCaptureSettings;
38use nvcodec_sys::cuda::*;
39use nvcodec_sys::*;
40
41/// EGL C-interop type aliases and the `EGL_*` attribute constants used to wrap a dmabuf as
42/// an `EGLImageKHR` for CUDA import.
43type EGLDisplay = *const c_void;
44type EGLImageKHR = *mut c_void;
45type EGLint = i32;
46type EGLenum = u32;
47type EGLBoolean = u32;
48
49const EGL_NO_IMAGE_KHR: EGLImageKHR = ptr::null_mut();
50const EGL_LINUX_DMA_BUF_EXT: u32 = 0x3270;
51const EGL_DMA_BUF_PLANE0_FD_EXT: EGLint = 0x3272;
52const EGL_DMA_BUF_PLANE0_OFFSET_EXT: EGLint = 0x3273;
53const EGL_DMA_BUF_PLANE0_PITCH_EXT: EGLint = 0x3274;
54const EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT: EGLint = 0x3443;
55const EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT: EGLint = 0x3444;
56const EGL_WIDTH: EGLint = 0x3057;
57const EGL_HEIGHT: EGLint = 0x3056;
58const EGL_LINUX_DRM_FOURCC_EXT: EGLint = 0x3271;
59const EGL_NONE: EGLint = 0x3038;
60
61/// Opaque CUDA graphics-resource handle for the EGL interop path — an `EGLImageKHR`
62/// registered with CUDA maps to one of these.
63type CUgraphicsResource = *mut c_void;
64
65/// `CUeglFrame::frame_type` values: the mapped planes are CUDA arrays, or pitch-linear device
66/// memory.
67const CU_EGL_FRAME_TYPE_ARRAY: u32 = 0;
68const CU_EGL_FRAME_TYPE_PITCH: u32 = 1;
69/// `CUeglFrame::cu_format` of an 8-bit-per-channel plane (`CU_AD_FORMAT_UNSIGNED_INT8`).
70const CU_AD_FORMAT_U8: u32 = 1;
71
72/// A CUDA frame mapped from an EGLImage: the `cuGraphicsResourceGetMappedEglFrame` result
73/// describing the imported dmabuf's plane pointers, geometry, pitch and pixel format.
74#[repr(C)]
75#[derive(Clone, Copy)]
76struct CUeglFrame {
77    frame: CUeglFrameUnion,
78    width: u32,
79    height: u32,
80    depth: u32,
81    pitch: u32,
82    plane_count: u32,
83    num_channels: u32,
84    frame_type: u32,
85    egl_color_format: u32,
86    cu_format: u32,
87}
88
89/// The mapped frame's plane pointers, as either CUDA arrays or pitch-linear device
90/// pointers — `CUeglFrame::frame_type` selects which arm of the union is valid.
91#[repr(C)]
92#[derive(Clone, Copy)]
93union CUeglFrameUnion {
94    p_array: [CUarray; 3],
95    p_pitch: [*mut c_void; 3],
96}
97
98type EglCreateImageKhrFn = unsafe extern "C" fn(
99    dpy: EGLDisplay,
100    ctx: *mut c_void,
101    target: EGLenum,
102    buffer: *mut c_void,
103    attrib_list: *const EGLint,
104) -> EGLImageKHR;
105type EglDestroyImageKhrFn = unsafe extern "C" fn(dpy: EGLDisplay, image: EGLImageKHR) -> EGLBoolean;
106
107/// Dynamically loaded EGL entry points (from `libEGL`) for creating and destroying the
108/// `EGLImageKHR` that wraps a dmabuf. `_lib` keeps the library resident for the pointers' life.
109struct EglFunctions {
110    _lib: Library,
111    eglGetProcAddress: unsafe extern "C" fn(procname: *const c_char) -> *mut c_void,
112    eglCreateImageKHR: EglCreateImageKhrFn,
113    eglDestroyImageKHR: EglDestroyImageKhrFn,
114}
115
116/// Dynamically loaded CUDA driver-API entry points (from `libcuda`) for context, device,
117/// memory, host-pin and EGL-interop calls. `_lib` keeps the library resident for the pointers' life.
118struct CudaFunctions {
119    _lib: Library,
120    cuInit: unsafe extern "C" fn(flags: u32) -> CUresult,
121    cuDeviceGet: unsafe extern "C" fn(device: *mut CUdevice, ordinal: i32) -> CUresult,
122    cuDeviceGetByPCIBusId: unsafe extern "C" fn(dev: *mut CUdevice, pciBusId: *const c_char) -> CUresult,
123    cuDevicePrimaryCtxRetain: unsafe extern "C" fn(
124        pctx: *mut CUcontext,
125        dev: CUdevice,
126    ) -> CUresult,
127    cuCtxPushCurrent_v2: unsafe extern "C" fn(ctx: CUcontext) -> CUresult,
128    cuCtxPopCurrent_v2: unsafe extern "C" fn(pctx: *mut CUcontext) -> CUresult,
129    cuDevicePrimaryCtxRelease_v2: unsafe extern "C" fn(dev: CUdevice) -> CUresult,
130    cuMemAlloc_v2: unsafe extern "C" fn(dptr: *mut CUdeviceptr, bytesize: usize) -> CUresult,
131    cuMemAllocPitch_v2: unsafe extern "C" fn(
132        dptr: *mut CUdeviceptr,
133        pPitch: *mut usize,
134        WidthInBytes: usize,
135        Height: usize,
136        ElementSizeBytes: u32,
137    ) -> CUresult,
138    cuMemFree_v2: unsafe extern "C" fn(dptr: CUdeviceptr) -> CUresult,
139    cuMemcpyHtoD_v2: unsafe extern "C" fn(
140        dstDevice: CUdeviceptr,
141        srcHost: *const c_void,
142        ByteCount: usize,
143    ) -> CUresult,
144    cuMemcpyDtoH_v2: unsafe extern "C" fn(
145        dstHost: *mut c_void,
146        srcDevice: CUdeviceptr,
147        ByteCount: usize,
148    ) -> CUresult,
149    cuMemcpy2D_v2: unsafe extern "C" fn(pCopy: *const CUDA_MEMCPY2D) -> CUresult,
150    cuMemcpy2DAsync_v2: unsafe extern "C" fn(pCopy: *const CUDA_MEMCPY2D, hStream: CUstream) -> CUresult,
151    cuStreamSynchronize: unsafe extern "C" fn(hStream: CUstream) -> CUresult,
152    cuMemHostRegister_v2: unsafe extern "C" fn(p: *mut c_void, bytesize: usize, flags: u32) -> CUresult,
153    cuMemHostUnregister: unsafe extern "C" fn(p: *mut c_void) -> CUresult,
154    cuGraphicsEGLRegisterImage: unsafe extern "C" fn(
155        pCudaResource: *mut CUgraphicsResource,
156        image: EGLImageKHR,
157        flags: u32,
158    ) -> CUresult,
159    cuGraphicsUnregisterResource: unsafe extern "C" fn(resource: CUgraphicsResource) -> CUresult,
160    cuGraphicsResourceGetMappedEglFrame: unsafe extern "C" fn(
161        pEglFrame: *mut CUeglFrame,
162        resource: CUgraphicsResource,
163        index: u32,
164        mipLevel: u32,
165    ) -> CUresult,
166    cuDeviceGetCount: unsafe extern "C" fn(count: *mut i32) -> CUresult,
167    cuDeviceGetName: unsafe extern "C" fn(name: *mut c_char, len: i32, dev: CUdevice) -> CUresult,
168    cuDeviceGetUuid: unsafe extern "C" fn(uuid: *mut CUuuid, dev: CUdevice) -> CUresult,
169    cuGetErrorName: unsafe extern "C" fn(error: CUresult, pStr: *mut *const c_char) -> CUresult,
170}
171
172/// Dynamically loaded NVENC entry points (from `libnvidia-encode`).
173///
174/// - **`create_instance`** (`NvEncodeAPICreateInstance`): fills an `NV_ENCODE_API_FUNCTION_LIST`
175///   with the driver's encode entry points for a requested API-version word.
176/// - **`get_max_version`** (`NvEncodeAPIGetMaxSupportedVersion`): the highest API version the
177///   driver supports, used to cap version probing. `Option` because very old drivers lack it, in
178///   which case probing relies on `create_instance` acceptance alone.
179///
180/// `_lib` keeps the library resident for the function pointers' life.
181struct NvencLibrary {
182    _lib: Library,
183    create_instance: unsafe extern "C" fn(
184        functionList: *mut NV_ENCODE_API_FUNCTION_LIST,
185    ) -> NVENCSTATUS,
186    get_max_version: Option<unsafe extern "C" fn(*mut u32) -> NVENCSTATUS>,
187}
188
189/// Negotiated NVENC API version `(major, minor)`, resolved once per process. `None` until
190/// `nvenc_negotiate` runs; every struct-version word and the session `apiVersion` derive from it.
191static NVENC_NEG_VER: std::sync::OnceLock<(u32, u32)> = std::sync::OnceLock::new();
192
193/// The NVENCAPI structs this encoder must stamp with a per-SDK version word — enumerated
194/// here precisely because getting that word exactly right is the whole mechanism that lets one
195/// compiled binary satisfy every driver's version check.
196///
197/// Each NVENCAPI struct carries a `version` field that the driver validates against the exact word
198/// its own SDK defined for that struct, rejecting anything else outright with
199/// `NV_ENC_ERR_INVALID_VERSION`. Only two parts of that packed word move between SDKs — the struct
200/// **revision** (bits 16-23) and the **`1<<31` flag** — so a session that has down-negotiated to an
201/// older API cannot send the compiled 13.0 words; it must stamp each struct with precisely the word
202/// that older SDK defined, while a current driver still receives its own native word. Naming the
203/// structs here is what lets `NvStruct::rev` supply the per-version `(revision, flag)` and
204/// `nvenc_struct_ver` assemble the word.
205#[derive(Clone, Copy, Debug)]
206enum NvStruct {
207    FunctionList,
208    OpenSessionExParams,
209    Config,
210    RcParams,
211    PresetConfig,
212    InitializeParams,
213    ReconfigureParams,
214    RegisterResource,
215    MapInputResource,
216    CreateBitstreamBuffer,
217    PicParams,
218    LockBitstream,
219    CapsParam,
220}
221
222impl NvStruct {
223    /// The `(struct revision, 1<<31 flag)` this struct uses under the SDK identified by the
224    /// packed API version `api` (`(major<<4)|minor`) — the only two sub-fields that move between
225    /// SDKs, and thus the entire per-version knowledge stamping a struct actually needs.
226    ///
227    /// The revision lands in bits 16-23 of the version word and the flag in bit 31; every other bit
228    /// is fixed, which is exactly why matching just these two on `api` reproduces each SDK's word.
229    /// The values are transcribed verbatim from `nvEncodeAPI.h` at the FFmpeg nv-codec-headers tags
230    /// n10.0.26.2, n11.0.10.3, n11.1.5.3, n12.0.16.1, n12.1.14.0, n12.2.72.0 and n13.0.19.0, so they
231    /// are ground truth rather than anything derived that could drift. Structs whose layout is stable
232    /// across those SDKs return a constant pair; the rest match on `api`. 10.0 is the negotiation
233    /// floor, so the oldest match arm also covers anything below it.
234    fn rev(self, api: u32) -> (u32, bool) {
235        match self {
236            NvStruct::FunctionList => (2, false),
237            NvStruct::OpenSessionExParams => (1, false),
238            NvStruct::Config => match api {
239                0xC2.. => (9, true),
240                0xC0..=0xC1 => (8, true),
241                _ => (7, true),
242            },
243            NvStruct::RcParams => (1, false),
244            NvStruct::PresetConfig => (if api >= 0xC2 { 5 } else { 4 }, true),
245            NvStruct::InitializeParams => match api {
246                0xC2.. => (7, true),
247                0xC1 => (6, true),
248                _ => (5, true),
249            },
250            NvStruct::ReconfigureParams => (if api >= 0xC2 { 2 } else { 1 }, true),
251            NvStruct::RegisterResource => match api {
252                0xC2.. => (5, false),
253                0xC0..=0xC1 => (4, false),
254                _ => (3, false),
255            },
256            NvStruct::MapInputResource => (4, false),
257            NvStruct::CreateBitstreamBuffer => (1, false),
258            NvStruct::PicParams => match api {
259                0xC2.. => (7, true),
260                0xC0..=0xC1 => (6, true),
261                _ => (4, true),
262            },
263            NvStruct::LockBitstream => match api {
264                0xC2.. => (2, true),
265                0xC1 => (1, true),
266                0xC0 => (2, false),
267                _ => (1, false),
268            },
269            NvStruct::CapsParam => (1, false),
270        }
271    }
272}
273
274/// Assemble the `NVENCAPI_STRUCT_VERSION` word for struct `s` at API version `(maj, min)`.
275///
276/// The 32-bit word packs, from `NvStruct::rev` and the API version:
277///
278/// 1. **API major** in bits 0-7, **API minor** in bits 24-27.
279/// 2. **Struct revision** (`rev`) in bits 16-23.
280/// 3. **Magic `0x7`** in bits 28-30.
281/// 4. **The `1<<31` flag** in bit 31, when this struct sets it at this version.
282///
283/// For the pinned nvcodec-sys headers this reproduces the compile-time `NV_ENC_*_VER` constants
284/// exactly, so a current driver is stamped byte-for-byte identically to its own SDK's constant; the
285/// `version_tests` module asserts that identity.
286fn nvenc_struct_ver(s: NvStruct, maj: u32, min: u32) -> u32 {
287    let (rev, high_bit) = s.rev((maj << 4) | (min & 0xF));
288    (maj & 0xFF) | ((min & 0xF) << 24) | (rev << 16) | (0x7 << 28) | ((high_bit as u32) << 31)
289}
290
291/// The process's effective NVENC API version `(major, minor)`: the negotiated value once
292/// `nvenc_negotiate` has run, otherwise the pinned `NVENCAPI_VERSION` decomposed (major in the low
293/// byte, minor at bit 24) as the pre-negotiation fallback.
294#[inline]
295fn nvenc_cur_ver() -> (u32, u32) {
296    NVENC_NEG_VER
297        .get()
298        .copied()
299        .unwrap_or((NVENCAPI_VERSION & 0xFF, (NVENCAPI_VERSION >> 24) & 0xFF))
300}
301
302/// The struct-version word for `s` tagged with the process's negotiated API version — the
303/// value every NVENCAPI struct literal assigns to its `version` field.
304#[inline]
305fn sv(s: NvStruct) -> u32 {
306    let (m, n) = nvenc_cur_ver();
307    nvenc_struct_ver(s, m, n)
308}
309
310/// The negotiated session `apiVersion` word (`major | minor<<24`) passed to
311/// `NvEncOpenEncodeSessionEx` — note the minor sits at bit 24 here, unlike the `(major<<4)|minor`
312/// packing that `NvStruct::rev` matches on.
313#[inline]
314fn neg_api() -> u32 {
315    let (m, n) = nvenc_cur_ver();
316    m | (n << 24)
317}
318
319/// Resolve the process-wide NVENC API version once, by probing the driver newest-first and
320/// remembering the highest version it accepts.
321///
322/// The bundled nv-codec-headers are NVENC 13.0 (`pinned`), so a current driver negotiates 13.0
323/// natively while older drivers down-negotiate through 12.x / 11.x to the 10.0 floor (~R445). The
324/// compiled struct *layouts* are always the 13.0 ones; only the version *words* change per
325/// negotiated version (via the `NvStruct::rev` table), so each struct is stamped with the exact word
326/// the negotiated SDK defined. Steps:
327///
328/// 1. **Cap the search** by the driver's max: query `get_max_version` when present, then optionally
329///    lower it further from `PIXELFLUX_NVENC_MAX_API` (e.g. `"11.0"`) for testing / pinning. A cap
330///    of 0 means unknown, and probing then relies on `create_instance` acceptance alone.
331/// 2. **Probe candidates** newest-first (`pinned`, 12.1, 12.0, 11.1, 11.0, 10.0), skipping any
332///    above the cap. Each probe stamps an `NV_ENCODE_API_FUNCTION_LIST` with that version's word and
333///    calls `create_instance`.
334/// 3. **Require the whole encode path**, not just a success code: the session opener plus
335///    `nvEncInitializeEncoder`, `nvEncGetEncodePresetConfigEx`, `nvEncEncodePicture` and
336///    `nvEncLockBitstream` must all be non-null, because a driver can accept the function-list word
337///    yet leave newer entry points null. The first fully-populated version wins.
338/// 4. **Fall back** to `pinned` if nothing qualifies. Stored in `NVENC_NEG_VER`, set-once.
339fn nvenc_negotiate(lib: &NvencLibrary) {
340    NVENC_NEG_VER.get_or_init(|| {
341        let pinned = (NVENCAPI_VERSION & 0xFF, (NVENCAPI_VERSION >> 24) & 0xFF);
342        let mut drv_max: u32 = 0;
343        if let Some(get_max) = lib.get_max_version {
344            let mut m: u32 = 0;
345            if unsafe { get_max(&mut m) } == NVENCSTATUS::NV_ENC_SUCCESS {
346                drv_max = m;
347            }
348        }
349        if let Ok(cap) = std::env::var("PIXELFLUX_NVENC_MAX_API") {
350            let mut it = cap.split('.');
351            if let (Some(a), Some(b)) = (it.next(), it.next())
352                && let (Ok(cm), Ok(cn)) = (a.parse::<u32>(), b.parse::<u32>()) {
353                    let capv = (cm << 4) | (cn & 0xF);
354                    if capv != 0 && (drv_max == 0 || capv < drv_max) {
355                        drv_max = capv;
356                    }
357                }
358        }
359        let candidates = [pinned, (12, 1), (12, 0), (11, 1), (11, 0), (10, 0)];
360        for (maj, min) in candidates {
361            let vv = (maj << 4) | min;
362            if drv_max != 0 && vv > drv_max {
363                continue;
364            }
365            let mut probe = NV_ENCODE_API_FUNCTION_LIST {
366                version: nvenc_struct_ver(NvStruct::FunctionList, maj, min),
367                ..Default::default()
368            };
369            let st = unsafe { (lib.create_instance)(&mut probe) };
370            if st == NVENCSTATUS::NV_ENC_SUCCESS
371                && probe.nvEncOpenEncodeSessionEx.is_some()
372                && probe.nvEncInitializeEncoder.is_some()
373                && probe.nvEncGetEncodePresetConfigEx.is_some()
374                && probe.nvEncEncodePicture.is_some()
375                && probe.nvEncLockBitstream.is_some()
376            {
377                eprintln!("[pixelflux] NVENC API version negotiated: {}.{}", maj, min);
378                return (maj, min);
379            }
380        }
381        pinned
382    });
383}
384
385/// Cached CUDA import of a dmabuf, keyed by fd so a recurring capture buffer is imported
386/// once: the `EGLImageKHR`, the CUDA graphics resource it registers as, the mapped `CUeglFrame`,
387/// and how that frame reaches NVENC. Torn down on drop / reconfigure, or evicted when the fd it is
388/// keyed by no longer names the same buffer (see `DmaBufIdentity`).
389struct CachedDmaBuf {
390    identity: DmaBufIdentity,
391    egl_image: EGLImageKHR,
392    cuda_resource: CUgraphicsResource,
393    egl_frame: CUeglFrame,
394    input: DmaBufInput,
395}
396
397/// How a cached dmabuf import feeds the encoder.
398///
399/// `Direct` is the zero-copy case: the mapped frame's first plane is itself registered and mapped
400/// as an NVENC input — as a pitch-linear device pointer or as a CUDA array, per `DirectPlane` — so
401/// encoding reads the capture buffer in place. `Copy` covers the rest: a plane `direct_plane`
402/// rules out, one the driver declined to register, or the direct path switched off; the plane is
403/// then copied into the session's packed input surface each frame.
404#[derive(Clone, Copy)]
405enum DmaBufInput {
406    Direct {
407        registered: NV_ENC_REGISTERED_PTR,
408        mapped: NV_ENC_INPUT_PTR,
409        format: NV_ENC_BUFFER_FORMAT,
410    },
411    Copy,
412}
413
414/// The NVENC packed input format whose byte order is a dmabuf's: the XR24 / AR24 family is
415/// B,G,R,A in memory (NVENC's word-ordered `ARGB`), the XB24 / AB24 family R,G,B,A (`ABGR`).
416/// `None` for any other fourcc — nothing NVENC reads as packed 8-bit RGB.
417fn fourcc_nvenc_format(code: Fourcc) -> Option<NV_ENC_BUFFER_FORMAT> {
418    match code {
419        Fourcc::Argb8888 | Fourcc::Xrgb8888 => Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB),
420        Fourcc::Abgr8888 | Fourcc::Xbgr8888 => Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR),
421        _ => None,
422    }
423}
424
425/// How the first plane of a mapped `CUeglFrame` registers with NVENC in place: the
426/// `NV_ENC_REGISTER_RESOURCE` resource type and the `pitch` word that type expects.
427#[derive(Debug, Clone, Copy, PartialEq, Eq)]
428enum DirectPlane {
429    /// Pitch-linear device memory, registered as a CUDA device pointer at this row pitch.
430    Pitch(u32),
431    /// A two-dimensional CUDA array of four 8-bit channels, registered as a CUDA array; the
432    /// value is the array's row width in bytes (`Width × NumChannels`), which is what NVENC
433    /// takes as the pitch of an array resource.
434    Array(u32),
435}
436
437/// Whether NVENC can read a mapped `CUeglFrame` in place, and how.
438///
439/// Either frame kind has to be usable as the session's `width × height` input: a first plane
440/// present and non-null, and a geometry of at least the session's. A pitch-linear plane also needs
441/// a row pitch covering `width * 4` bytes at the 4-byte alignment `NV_ENC_REGISTER_RESOURCE`
442/// requires; a CUDA-array plane has to be four 8-bit channels, the layout NVENC's packed formats
443/// describe. `None` sends the frame down the per-frame copy into the session's own input surface.
444fn direct_plane(frame: &CUeglFrame, width: u32, height: u32) -> Option<DirectPlane> {
445    if frame.plane_count < 1 || width == 0 || height == 0 {
446        return None;
447    }
448    if frame.width < width || frame.height < height {
449        return None;
450    }
451    match frame.frame_type {
452        CU_EGL_FRAME_TYPE_PITCH => {
453            let plane = unsafe { frame.frame.p_pitch[0] };
454            let pitch_ok = frame.pitch >= width.saturating_mul(4) && frame.pitch % 4 == 0;
455            (!plane.is_null() && pitch_ok).then_some(DirectPlane::Pitch(frame.pitch))
456        }
457        CU_EGL_FRAME_TYPE_ARRAY => {
458            let array = unsafe { frame.frame.p_array[0] };
459            let packed_8bit = frame.cu_format == CU_AD_FORMAT_U8 && frame.num_channels == 4;
460            (!array.is_null() && packed_8bit).then_some(DirectPlane::Array(frame.width * 4))
461        }
462        _ => None,
463    }
464}
465
466/// The stable identity of a dmabuf, so a cache keyed by the raw fd number cannot hand back a
467/// stale EGLImage after that fd was closed and recycled onto a different buffer.
468///
469/// The fd integer alone is not an identity: the host's slot renegotiation frees the backing buffer
470/// objects and the kernel reissues the same small fd numbers for the new ones. Since Linux 5.3 each
471/// dma-buf carries its own inode, so `(st_dev, st_ino)` distinguishes two buffers that reuse one fd
472/// number, and `size` reports the true allocation; the DRM format modifier and the geometry round
473/// out the identity for older kernels where the inode is shared. A cache hit requires every field to
474/// match, so a recycled fd whose buffer differs in any of them is re-imported instead of reused.
475#[derive(Debug, Clone, Copy, PartialEq, Eq)]
476struct DmaBufIdentity {
477    dev: u64,
478    ino: u64,
479    size: i64,
480    modifier: u64,
481    width: u32,
482    height: u32,
483}
484
485impl DmaBufIdentity {
486    /// Read the identity of the buffer behind `fd`: `fstat` supplies the inode and allocation
487    /// size, and the caller supplies the modifier and geometry from the dmabuf descriptor. A failed
488    /// `fstat` leaves the inode/size zero, which still combines with the modifier and geometry.
489    fn probe(fd: i32, modifier: u64, width: u32, height: u32) -> Self {
490        let mut st: libc::stat = unsafe { std::mem::zeroed() };
491        let (dev, ino, size) = if unsafe { libc::fstat(fd, &mut st) } == 0 {
492            (st.st_dev as u64, st.st_ino as u64, st.st_size as i64)
493        } else {
494            (0, 0, 0)
495        };
496        Self { dev, ino, size, modifier, width, height }
497    }
498}
499
500/// The chroma format and dimensional feasibility a session settles on given the driver's
501/// reported capabilities, so init degrades cleanly instead of failing opaquely.
502///
503/// - `fullcolor` is the chroma actually used: 4:4:4 only when it was requested and the GPU carries
504///   it, otherwise 4:2:0.
505/// - `downgraded_color` records that a 4:4:4 request was met with 4:2:0, so the caller says so once.
506/// - `too_large` carries the driver's `(max_w, max_h)` when the requested geometry exceeds it; the
507///   caller then declines NVENC and falls back to software rather than failing to initialize.
508///
509/// A capability that could not be queried is `None` and does not gate: 4:4:4 stays as requested and
510/// the dimension test is skipped, so an unavailable answer never forces a false downgrade or refusal.
511#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512struct CapsDecision {
513    fullcolor: bool,
514    downgraded_color: bool,
515    too_large: Option<(i32, i32)>,
516}
517
518/// Resolve the requested chroma and geometry against the driver caps (`None` = unknown, ungated).
519fn decide_caps(
520    req_fullcolor: bool,
521    req_w: i32,
522    req_h: i32,
523    cap_yuv444: Option<i32>,
524    cap_width_max: Option<i32>,
525    cap_height_max: Option<i32>,
526) -> CapsDecision {
527    let downgraded_color = req_fullcolor && cap_yuv444 == Some(0);
528    let fullcolor = req_fullcolor && !downgraded_color;
529    let exceeds = |req: i32, cap: Option<i32>| cap.is_some_and(|m| m > 0 && req > m);
530    let too_large = if exceeds(req_w, cap_width_max) || exceeds(req_h, cap_height_max) {
531        Some((cap_width_max.unwrap_or(0), cap_height_max.unwrap_or(0)))
532    } else {
533        None
534    };
535    CapsDecision { fullcolor, downgraded_color, too_large }
536}
537
538/// The in-place resize headroom for one axis: the requested size lifted to `floor` (the 5.2
539/// ceiling the level is pinned at) but never past the driver's reported maximum, so initializing
540/// with headroom cannot itself exceed what the GPU supports.
541fn nvenc_headroom(size: u32, floor: u32, cap: Option<i32>) -> u32 {
542    let want = size.max(floor);
543    match cap {
544        Some(m) if m > 0 => want.min(m as u32),
545        _ => want,
546    }
547}
548
549/// Query one NVENC H.264 capability on an open session, returning the driver's integer answer or
550/// `None` when the entry point is absent or the query fails — `decide_caps` reads `None` as "do not
551/// gate", so a query failure never becomes a false refusal.
552unsafe fn query_cap(
553    funcs: &NV_ENCODE_API_FUNCTION_LIST,
554    session: *mut c_void,
555    cap: NV_ENC_CAPS,
556) -> Option<i32> {
557    let get = funcs.nvEncGetEncodeCaps?;
558    let mut param = NV_ENC_CAPS_PARAM {
559        version: sv(NvStruct::CapsParam),
560        capsToQuery: cap,
561        reserved: [0u32; 62],
562    };
563    let mut val: i32 = 0;
564    if get(session, NV_ENC_CODEC_H264_GUID, &mut param, &mut val) == NVENCSTATUS::NV_ENC_SUCCESS {
565        Some(val)
566    } else {
567        None
568    }
569}
570
571/// GUID selecting the H.264 **High** profile (4:2:0) for `NV_ENC_CONFIG::profileGUID`.
572const NV_ENC_H264_PROFILE_HIGH_GUID: GUID = GUID {
573    Data1: 0x205b553d,
574    Data2: 0x5f01,
575    Data3: 0x4d9e,
576    Data4: [0x91, 0x84, 0xda, 0x32, 0x77, 0x5b, 0x55, 0x9b],
577};
578
579/// GUID selecting the H.264 **High 4:4:4 Predictive** profile for full-color encoding.
580const NV_ENC_H264_PROFILE_HIGH_444_GUID: GUID = GUID {
581    Data1: 0x7ac663cb,
582    Data2: 0xa598,
583    Data3: 0x4960,
584    Data4: [0xb8, 0x44, 0x33, 0x9b, 0x26, 0x1a, 0x7d, 0x5c],
585};
586
587/// A live NVENC H.264 encoder session with its CUDA context and interop resources.
588///
589/// One instance owns a CUDA context bound to a specific GPU plus an NVENC session and everything
590/// the three input paths need:
591///
592/// - **Packed path**: a pitched device buffer (`input_device_ptr` / `input_pitch`) registered and
593///   mapped as the NVENC input (`registered_input_resource` / `mapped_input_buffer`) in the byte
594///   order `input_format` names (re-registered in place when a source of the other order arrives),
595///   fed either by a host→device upload or by the copy arm of the dmabuf path.
596/// - **Raw planar path**: a lazily-allocated NV12 / YUV444 device buffer (the `nv12_*` fields),
597///   created on first `encode_raw`.
598/// - **Zero-copy dmabuf path**: `dmabuf_cache` memoizes each fd's EGLImage → CUDA import, keyed by
599///   fd but validated against the buffer's `DmaBufIdentity` so a recycled fd re-imports; an import
600///   whose plane NVENC can take as it is — pitch-linear memory or a packed 8-bit CUDA array — is
601///   registered in place (`DmaBufInput::Direct`) unless `direct_dmabuf` was switched off, anything
602///   else is copied into the packed input each frame.
603///
604/// `bitstream_buffers` is a small ring (`current_buffer_idx` cycles it) of output buffers.
605/// `pinned_hosts` maps each page-locked host upload source's base pointer to its registered length,
606/// with a `0` length recording a failed registration so that address is never re-pinned.
607/// `current_qp` tracks the live ConstQP so a paint-over reconfigure is skipped when unchanged.
608/// `encode_config` and `init_params` are retained so in-place reconfigure can resubmit them.
609/// `omit_stripe_headers` drops the 10-byte wire
610/// header, and `node_index` is the effective CUDA device this session is bound to — a reuse across
611/// captures that now targets a different device must rebuild rather than reconfigure.
612pub struct NvencEncoder {
613    encoder_session: *mut c_void,
614    cuda_context: CUcontext,
615    cuda_device: CUdevice,
616    egl_display: EGLDisplay,
617    width: u32,
618    height: u32,
619    current_qp: u32,
620    encode_config: NV_ENC_CONFIG,
621    init_params: NV_ENC_INITIALIZE_PARAMS,
622    input_device_ptr: CUdeviceptr,
623    input_pitch: usize,
624    input_format: NV_ENC_BUFFER_FORMAT,
625    registered_input_resource: NV_ENC_REGISTERED_PTR,
626    mapped_input_buffer: NV_ENC_INPUT_PTR,
627    nv12_device_ptr: Option<CUdeviceptr>,
628    nv12_pitch: usize,
629    nv12_registered_resource: Option<NV_ENC_REGISTERED_PTR>,
630    nv12_mapped_buffer: Option<NV_ENC_INPUT_PTR>,
631    bitstream_buffers: Vec<NV_ENC_OUTPUT_PTR>,
632    current_buffer_idx: usize,
633    dmabuf_cache: HashMap<i32, CachedDmaBuf>,
634    pinned_hosts: HashMap<usize, usize>,
635    cuda: Arc<CudaFunctions>,
636    egl: Arc<EglFunctions>,
637    _nvenc_lib: Arc<NvencLibrary>,
638    nvenc_funcs: NV_ENCODE_API_FUNCTION_LIST,
639    omit_stripe_headers: bool,
640    node_index: i32,
641    /// Resolved once at init from `PIXELFLUX_NVENC_PIN`: page-lock the host upload sources so the
642    /// copy is a direct pinned DMA rather than a pageable copy staged through a bounce buffer.
643    pin_uploads: bool,
644    /// Resolved once at init from `PIXELFLUX_NVENC_DIRECT`: register pitch-linear dmabuf imports
645    /// with NVENC in place instead of copying them into the packed input each frame.
646    direct_dmabuf: bool,
647}
648
649unsafe impl Send for NvencEncoder {}
650
651/// Release every GPU resource the session holds, in the one teardown order the drivers
652/// tolerate, so nothing leaks and no still-referenced handle is ever freed out from under the
653/// driver.
654///
655/// The whole sequence runs with the owning CUDA context pushed current, because the `cuMemFree` /
656/// `cuGraphicsUnregisterResource` / `cuMemHostUnregister` calls each act on the *current* context —
657/// pop it first and the frees silently do nothing, leaking device memory. Within that, resources
658/// go inner-handle before the outer handle that owns it, since freeing an owner first orphans or
659/// faults on what still points into it: unmap the packed and raw-plane inputs before unregistering
660/// them, free their device buffers, destroy the bitstream buffers, and release every cached dmabuf
661/// import (`release_dmabuf_import`: its NVENC mapping and registration, then the CUDA resource and
662/// the EGLImage) — all session-owned — before the encoder session itself, and destroy that
663/// session before releasing the device's primary CUDA context it was opened against (the retain is
664/// refcounted, so the context lives until the last session on that device releases it). The
665/// page-locked host sources are unpinned in the same pass, each only when its recorded length is
666/// non-zero (a `0` marks a registration that failed and so was never pinned).
667impl Drop for NvencEncoder {
668    fn drop(&mut self) {
669        unsafe {
670            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
671
672            if !self.mapped_input_buffer.is_null() {
673                (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
674                    self.encoder_session,
675                    self.mapped_input_buffer,
676                );
677            }
678            if !self.registered_input_resource.is_null() {
679                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
680                    self.encoder_session,
681                    self.registered_input_resource,
682                );
683            }
684            if self.input_device_ptr != 0 {
685                (self.cuda.cuMemFree_v2)(self.input_device_ptr);
686            }
687
688            if let Some(mapped) = self.nv12_mapped_buffer {
689                (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
690                    self.encoder_session,
691                    mapped,
692                );
693            }
694            if let Some(registered) = self.nv12_registered_resource {
695                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
696                    self.encoder_session,
697                    registered,
698                );
699            }
700            if let Some(ptr) = self.nv12_device_ptr {
701                (self.cuda.cuMemFree_v2)(ptr);
702            }
703
704            for &bs in &self.bitstream_buffers {
705                (self.nvenc_funcs.nvEncDestroyBitstreamBuffer.unwrap())(
706                    self.encoder_session,
707                    bs,
708                );
709            }
710
711            let imports: Vec<CachedDmaBuf> = self.dmabuf_cache.drain().map(|(_, c)| c).collect();
712            for cache in imports {
713                self.release_dmabuf_import(cache);
714            }
715
716            for (base, len) in &self.pinned_hosts {
717                if *len > 0 {
718                    (self.cuda.cuMemHostUnregister)(*base as *mut c_void);
719                }
720            }
721
722            if !self.encoder_session.is_null() {
723                (self.nvenc_funcs.nvEncDestroyEncoder.unwrap())(self.encoder_session);
724            }
725
726            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
727            (self.cuda.cuDevicePrimaryCtxRelease_v2)(self.cuda_device);
728        }
729    }
730}
731
732use super::min_h264_level;
733
734/// The level an NVENC session advertises for this geometry, floored at 5.2 (`NV_ENC_LEVEL`
735/// shares level_idc's numbering: 52/60/61/62 for 5.2/6.0/6.1/6.2).
736///
737/// `reconfigure_resolution` resizes a live session in place, so the level has to cover every
738/// geometry that session can still reach — a level bump mid-GOP forces some hardware decoders to
739/// re-initialize. 5.2's MaxFS of 36864 macroblocks (≈ 4096×2304) spans everything up to 4K, so
740/// pinning that floor makes the whole range resolve to High@5.2; only beyond 4K does the shared
741/// ladder step up to 6.0 / 6.1 / 6.2.
742fn nvenc_h264_level(width: u32, height: u32, fps: u32) -> u32 {
743    min_h264_level(width, height, fps).max(52)
744}
745
746impl NvencEncoder {
747    /// Resolve EGL at runtime rather than link against it, so one binary boots even on hosts
748    /// without EGL — it is needed only by the zero-copy dmabuf path — and reach the
749    /// `eglCreateImageKHR` / `eglDestroyImageKHR` entry points through `eglGetProcAddress` because
750    /// they are KHR *extensions* the base `libEGL` is not obliged to export as plain symbols.
751    /// Erroring when the library or either extension is missing lets the caller fall back to another
752    /// encoder instead of crashing at the first dmabuf import.
753    fn load_egl() -> Result<EglFunctions, String> {
754        unsafe {
755            let lib_name = "libEGL.so.1";
756            let lib = Library::new(lib_name)
757                .or_else(|_| Library::new("libEGL.so"))
758                .map_err(|e| format!("Could not load EGL library: {}", e))?;
759
760            let get_proc_addr_sym: Symbol<unsafe extern "C" fn(*const c_char) -> *mut c_void> = lib
761                .get(b"eglGetProcAddress\0")
762                .map_err(|e| format!("Missing symbol eglGetProcAddress: {}", e))?;
763
764            let eglGetProcAddress = *get_proc_addr_sym;
765
766            let load_extension = |name: &str| -> Result<*mut c_void, String> {
767                let c_name = CString::new(name).unwrap();
768                let addr = eglGetProcAddress(c_name.as_ptr());
769                if addr.is_null() {
770                    Err(format!("EGL Extension not found: {}", name))
771                } else {
772                    Ok(addr)
773                }
774            };
775
776            let create_addr = load_extension("eglCreateImageKHR")?;
777            let destroy_addr = load_extension("eglDestroyImageKHR")?;
778
779            Ok(EglFunctions {
780                _lib: lib,
781                eglGetProcAddress,
782                eglCreateImageKHR: std::mem::transmute::<*mut c_void, EglCreateImageKhrFn>(create_addr),
783                eglDestroyImageKHR: std::mem::transmute::<*mut c_void, EglDestroyImageKhrFn>(destroy_addr),
784            })
785        }
786    }
787
788    /// Resolve the CUDA driver library (`libcuda.so.1`, or `nvcuda.dll` on Windows) at
789    /// runtime so the crate links against no CUDA SDK and still runs wherever a driver is installed,
790    /// binding every `cu*` entry point up front so the per-frame hot path is plain indirect calls
791    /// with no repeated symbol lookups. A missing symbol errors with its name, turning an ABI
792    /// mismatch into a legible message instead of a later null-pointer call.
793    fn load_cuda() -> Result<CudaFunctions, String> {
794        unsafe {
795            let lib_name = if cfg!(windows) {
796                "nvcuda.dll"
797            } else {
798                "libcuda.so.1"
799            };
800            let lib = Library::new(lib_name)
801                .map_err(|e| format!("Could not load CUDA library ({}): {}", lib_name, e))?;
802
803            macro_rules! load {
804                ($lib:expr, $name:expr) => {
805                    *$lib.get($name).map_err(|e| {
806                        format!(
807                            "Missing symbol {}: {}",
808                            std::str::from_utf8($name).unwrap(),
809                            e
810                        )
811                    })?
812                };
813            }
814
815            Ok(CudaFunctions {
816                cuInit: load!(lib, b"cuInit\0"),
817                cuDeviceGet: load!(lib, b"cuDeviceGet\0"),
818                cuDeviceGetByPCIBusId: load!(lib, b"cuDeviceGetByPCIBusId\0"),
819                cuDevicePrimaryCtxRetain: load!(lib, b"cuDevicePrimaryCtxRetain\0"),
820                cuCtxPushCurrent_v2: load!(lib, b"cuCtxPushCurrent_v2\0"),
821                cuCtxPopCurrent_v2: load!(lib, b"cuCtxPopCurrent_v2\0"),
822                cuDevicePrimaryCtxRelease_v2: load!(lib, b"cuDevicePrimaryCtxRelease_v2\0"),
823                cuMemAlloc_v2: load!(lib, b"cuMemAlloc_v2\0"),
824                cuMemAllocPitch_v2: load!(lib, b"cuMemAllocPitch_v2\0"),
825                cuMemFree_v2: load!(lib, b"cuMemFree_v2\0"),
826                cuMemcpyHtoD_v2: load!(lib, b"cuMemcpyHtoD_v2\0"),
827                cuMemcpyDtoH_v2: load!(lib, b"cuMemcpyDtoH_v2\0"),
828                cuMemcpy2D_v2: load!(lib, b"cuMemcpy2D_v2\0"),
829                cuMemcpy2DAsync_v2: load!(lib, b"cuMemcpy2DAsync_v2\0"),
830                cuStreamSynchronize: load!(lib, b"cuStreamSynchronize\0"),
831                cuMemHostRegister_v2: load!(lib, b"cuMemHostRegister_v2\0"),
832                cuMemHostUnregister: load!(lib, b"cuMemHostUnregister\0"),
833                cuGraphicsEGLRegisterImage: load!(lib, b"cuGraphicsEGLRegisterImage\0"),
834                cuGraphicsUnregisterResource: load!(lib, b"cuGraphicsUnregisterResource\0"),
835                cuGraphicsResourceGetMappedEglFrame: load!(
836                    lib,
837                    b"cuGraphicsResourceGetMappedEglFrame\0"
838                ),
839                cuDeviceGetCount: load!(lib, b"cuDeviceGetCount\0"),
840                cuDeviceGetName: load!(lib, b"cuDeviceGetName\0"),
841                cuDeviceGetUuid: load!(lib, b"cuDeviceGetUuid\0"),
842                cuGetErrorName: load!(lib, b"cuGetErrorName\0"),
843                _lib: lib,
844            })
845        }
846    }
847
848    /// Resolve `libnvidia-encode` at runtime for the same reason as CUDA — no SDK to link,
849    /// runs against whatever driver ships — binding `NvEncodeAPICreateInstance` as the sole entry
850    /// point every later encode call is reached through. `NvEncodeAPIGetMaxSupportedVersion` is kept
851    /// optional and bound only when present, because very old drivers lack it; negotiation then falls
852    /// back to probing `create_instance` acceptance directly rather than failing to load.
853    fn load_nvenc() -> Result<NvencLibrary, String> {
854        unsafe {
855            let lib_name = NVENC_DLL_NAME;
856            let lib = Library::new(lib_name)
857                .map_err(|e| format!("Could not load NVENC library ({}): {}", lib_name, e))?;
858
859            let create_instance = *lib
860                .get(NV_ENCODE_API_CREATE_INSTANCE_FN_NAME)
861                .map_err(|e| e.to_string())?;
862            let get_max_version = lib
863                .get::<NvEncodeApiGetMaxSupportedVersionFn>(
864                    NV_ENCODE_API_GET_MAX_SUPPORTED_VERSION_FN_NAME,
865                )
866                .map(|s| *s)
867                .ok();
868            Ok(NvencLibrary {
869                create_instance,
870                get_max_version,
871                _lib: lib,
872            })
873        }
874    }
875
876    /// Turn a `CUresult` into the driver's own error name via `cuGetErrorName` so a failure
877    /// logs something diagnosable (e.g. `CUDA_ERROR_OUT_OF_MEMORY`) instead of a bare integer,
878    /// falling back to the numeric code only when the name is unavailable.
879    unsafe fn get_error_string(cuda: &CudaFunctions, err: CUresult) -> String {
880        let mut p_str: *const c_char = ptr::null();
881        if (cuda.cuGetErrorName)(err, &mut p_str) == CUresult::CUDA_SUCCESS && !p_str.is_null() {
882            CStr::from_ptr(p_str).to_string_lossy().into_owned()
883        } else {
884            format!("Unknown CUDA Error ({})", err.0)
885        }
886    }
887
888    /// Log the CUDA devices CUDA can enumerate — a debug aid when session init fails to find
889    /// or bind the expected GPU.
890    unsafe fn probe_devices(cuda: &CudaFunctions) {
891        let mut count = 0;
892        if (cuda.cuDeviceGetCount)(&mut count) != CUresult::CUDA_SUCCESS {
893            return;
894        }
895        println!("[NVENC] Found {} CUDA devices:", count);
896        for i in 0..count {
897            let mut dev = 0;
898            (cuda.cuDeviceGet)(&mut dev, i);
899            let mut name_buf = [0 as c_char; 256];
900            (cuda.cuDeviceGetName)(name_buf.as_mut_ptr(), 256, dev);
901            let name = CStr::from_ptr(name_buf.as_ptr()).to_string_lossy();
902            println!("[NVENC]   Device {}: {}", i, name);
903        }
904    }
905
906    /// The PCI bus ID of the GPU behind `/dev/dri/renderD<128+index>`, read from the sysfs
907    /// device symlink, so CUDA can bind to the same physical GPU the capture render node lives on.
908    fn get_pci_bus_id(render_index: i32) -> Option<String> {
909        let path = format!("/sys/class/drm/renderD{}/device", 128 + render_index);
910        if let Ok(target) = std::fs::read_link(&path)
911            && let Some(name) = target.file_name()
912            && let Some(name_str) = name.to_str() {
913                    return Some(name_str.to_string());
914                }
915        None
916    }
917
918    /// Build a live NVENC session: bind CUDA to the target GPU, open and configure the H.264
919    /// encoder, and allocate its input and output buffers.
920    ///
921    /// The sequence:
922    ///
923    /// 1. **Load and negotiate**: dlopen EGL / CUDA / NVENC, then `nvenc_negotiate` resolves the API
924    ///    version against the driver (set-once) before any struct is version-tagged. The multi-GPU
925    ///    `GET_ATTACHED_IDS` ioctl filter is installed after the NVIDIA libraries are loaded — so
926    ///    their GOTs can be patched — and before `cuInit` enumerates devices (a no-op unless a host
927    ///    GPU is hidden from this container). The three library `Arc`s are leaked once per process so
928    ///    the resolved function pointers stay valid for the program's life.
929    /// 2. **Bind the device**: `cuInit`, then bind by the render node's PCI bus ID
930    ///    (`encode_node_index`, with auto `<0` meaning device 0), falling back to CUDA device 0, and
931    ///    retain the device's primary CUDA context — shared and refcounted across every session on
932    ///    that device rather than a fresh 100-300 MiB context each — pushing it current.
933    /// 3. **Allocate input**: a pitched ARGB device buffer (`cuMemAllocPitch`, 16-byte element
934    ///    alignment) that hardware CSC turns into YUV.
935    /// 4. **Open the session and query caps**: create the function-list instance, open the session
936    ///    with the negotiated `apiVersion`, and query `nvEncGetEncodeCaps` so init degrades rather
937    ///    than fails — a 4:4:4 request on a GPU without it drops to 4:2:0, and a capture beyond the
938    ///    encoder's max dimensions returns `Err` so the caller falls back to software. Then pull a
939    ///    preset config (P4, ultra-low-latency); a failed preset lookup logs the driver's error
940    ///    string and proceeds with the zeroed default rather than aborting.
941    /// 5. **Configure the stream** (mutating the returned preset config, whose `version` word is
942    ///    re-stamped while its embedded `rcParams` keeps the version the preset fill set): High or
943    ///    High-4:4:4 profile; CBR (two-pass quarter-resolution for tighter per-frame rate adherence,
944    ///    VBV sizing, optional min/max QP clamps) or ConstQP; infinite GOP (`gopLength` / `idrPeriod`
945    ///    = `0xFFFFFFFF`); `zeroReorderDelay` plus a bitstream-restriction VUI (`max_num_reorder_frames=0`)
946    ///    so no-reorder decoders don't buffer; an explicit Annex-A level from `nvenc_h264_level`
947    ///    pinned from frame 1 so the level never bumps mid-stream; BT.709 VUI primaries and
948    ///    transfer for the sRGB source, with an SMPTE170M matrix and limited range to match the
949    ///    hardware ARGB CSC; chroma 4:2:0 or 4:4:4; repeated SPS/PPS; CABAC; no AUD; strict GOP
950    ///    target; and lookahead disabled for real-time latency.
951    /// 6. **Initialize with resize headroom**: `maxEncodeWidth` / `maxEncodeHeight` are raised to at
952    ///    least 4096×2304 (the 5.2 ceiling) so `reconfigure_resolution` can grow in place, but never
953    ///    past the driver's reported maximum; this costs ~290 MiB of device memory, so a failed init
954    ///    retries at the exact size (in-place resize then falls back to a rebuild).
955    /// 7. **Register, map, and buffer**: register and map the packed input surface (as `ARGB`;
956    ///    `set_input_format` re-registers it for an RGBA source), and create a
957    ///    4-deep ring of bitstream output buffers.
958    ///
959    /// Every failure after the CUDA allocation unwinds the resources created so far — buffers,
960    /// session, context — before returning `Err`. EGL is only needed by the zero-copy dmabuf path,
961    /// so callers on the host-ARGB path pass a null `egl_display`. The retained
962    /// `init_params.encodeConfig` raw pointer is nulled before the struct is returned (it points at
963    /// a local `config` about to move); the reconfigure paths repoint it at `self.encode_config`
964    /// when they resubmit.
965    pub fn new(
966        settings: &RustCaptureSettings,
967        egl_display: *const c_void,
968    ) -> Result<Self, String> {
969        println!("[NVENC] Initializing...");
970
971        let egl = Arc::new(Self::load_egl()?);
972        let cuda = Arc::new(Self::load_cuda()?);
973        let nvenc_lib = Arc::new(Self::load_nvenc()?);
974        nvenc_negotiate(&nvenc_lib);
975
976        crate::nvgpufilter::install();
977
978        static LEAK_ONCE: std::sync::Once = std::sync::Once::new();
979        LEAK_ONCE.call_once(|| {
980            std::mem::forget(egl.clone());
981            std::mem::forget(cuda.clone());
982            std::mem::forget(nvenc_lib.clone());
983        });
984
985        unsafe {
986            let res = (cuda.cuInit)(0);
987            if res != CUresult::CUDA_SUCCESS {
988                return Err(format!(
989                    "Init CUDA failed: {}",
990                    Self::get_error_string(&cuda, res)
991                ));
992            }
993
994            Self::probe_devices(&cuda);
995
996            let mut cu_device: CUdevice = 0;
997            let mut device_found = false;
998
999            if let Some(pci_bus_id) = Self::get_pci_bus_id(settings.encode_node_index.max(0)) {
1000                let c_pci_bus_id = CString::new(pci_bus_id.clone()).unwrap();
1001                if (cuda.cuDeviceGetByPCIBusId)(&mut cu_device, c_pci_bus_id.as_ptr()) == CUresult::CUDA_SUCCESS {
1002                    println!("[NVENC] Bound to CUDA device via PCI Bus ID: {}", pci_bus_id);
1003                    device_found = true;
1004                }
1005            }
1006
1007            if !device_found {
1008                let res = (cuda.cuDeviceGet)(&mut cu_device, 0);
1009                if res != CUresult::CUDA_SUCCESS {
1010                    return Err("Failed to get default CUDA device".into());
1011                }
1012            }
1013
1014            // One primary context per device, shared and refcounted across every session on that
1015            // device, rather than a fresh 100-300 MiB context each: a second display or a rebuild
1016            // retains the same context instead of allocating another. Retain does not make it
1017            // current, so it is pushed here to run the allocations below and left current for the
1018            // encode paths — matching the current-context state the removed cuCtxCreate produced.
1019            let mut cu_context: CUcontext = ptr::null_mut();
1020            let res = (cuda.cuDevicePrimaryCtxRetain)(&mut cu_context, cu_device);
1021            if res != CUresult::CUDA_SUCCESS {
1022                return Err("Failed to retain the device's primary CUDA context".into());
1023            }
1024            if (cuda.cuCtxPushCurrent_v2)(cu_context) != CUresult::CUDA_SUCCESS {
1025                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1026                return Err("Failed to make the primary CUDA context current".into());
1027            }
1028
1029            let width = settings.width as u32;
1030            let height = settings.height as u32;
1031            let mut input_device_ptr: CUdeviceptr = 0;
1032            let mut input_pitch: usize = 0;
1033
1034            let res = (cuda.cuMemAllocPitch_v2)(
1035                &mut input_device_ptr,
1036                &mut input_pitch,
1037                (width * 4) as usize,
1038                height as usize,
1039                16,
1040            );
1041            if res != CUresult::CUDA_SUCCESS {
1042                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1043                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1044                return Err("Failed to allocate ARGB input buffer on GPU".into());
1045            }
1046
1047            let mut function_list = NV_ENCODE_API_FUNCTION_LIST {
1048                version: sv(NvStruct::FunctionList),
1049                ..Default::default()
1050            };
1051            if (nvenc_lib.create_instance)(&mut function_list) != NVENCSTATUS::NV_ENC_SUCCESS {
1052                (cuda.cuMemFree_v2)(input_device_ptr);
1053                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1054                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1055                return Err("NvEncodeAPICreateInstance failed".into());
1056            }
1057
1058            let mut session_params = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
1059                version: sv(NvStruct::OpenSessionExParams),
1060                deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
1061                device: cu_context as *mut c_void,
1062                apiVersion: neg_api(),
1063                ..Default::default()
1064            };
1065
1066            let mut encoder_session: *mut c_void = ptr::null_mut();
1067            let open_fn = function_list.nvEncOpenEncodeSessionEx.unwrap();
1068            if open_fn(&mut session_params, &mut encoder_session) != NVENCSTATUS::NV_ENC_SUCCESS {
1069                (cuda.cuMemFree_v2)(input_device_ptr);
1070                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1071                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1072                return Err("Failed to open NVENC session".into());
1073            }
1074
1075            // Query caps so init degrades instead of failing opaquely: a 4:4:4 request on a GPU
1076            // without it drops to 4:2:0, and a capture beyond the encoder's max dimensions declines
1077            // NVENC so the caller falls back to software.
1078            let caps_444 = query_cap(
1079                &function_list,
1080                encoder_session,
1081                NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE,
1082            );
1083            let caps_wmax =
1084                query_cap(&function_list, encoder_session, NV_ENC_CAPS::NV_ENC_CAPS_WIDTH_MAX);
1085            let caps_hmax =
1086                query_cap(&function_list, encoder_session, NV_ENC_CAPS::NV_ENC_CAPS_HEIGHT_MAX);
1087            let caps = decide_caps(
1088                settings.video_fullcolor,
1089                width as i32,
1090                height as i32,
1091                caps_444,
1092                caps_wmax,
1093                caps_hmax,
1094            );
1095            if let Some((mw, mh)) = caps.too_large {
1096                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
1097                (cuda.cuMemFree_v2)(input_device_ptr);
1098                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1099                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1100                return Err(format!(
1101                    "NVENC maximum encode size {mw}x{mh} exceeded by {width}x{height}; using software"
1102                ));
1103            }
1104            if caps.downgraded_color {
1105                eprintln!("[NVENC] GPU does not support 4:4:4 (YUV444) encoding; encoding 4:2:0.");
1106            }
1107
1108            let is_444 = caps.fullcolor;
1109            let profile_guid = if is_444 {
1110                NV_ENC_H264_PROFILE_HIGH_444_GUID
1111            } else {
1112                NV_ENC_H264_PROFILE_HIGH_GUID
1113            };
1114
1115            let mut config = NV_ENC_CONFIG {
1116                version: sv(NvStruct::Config),
1117                ..Default::default()
1118            };
1119            let mut preset_config = NV_ENC_PRESET_CONFIG {
1120                version: sv(NvStruct::PresetConfig),
1121                presetCfg: config,
1122                ..Default::default()
1123            };
1124
1125            let get_preset_ex = function_list.nvEncGetEncodePresetConfigEx.unwrap();
1126            let preset_status = get_preset_ex(
1127                encoder_session,
1128                NV_ENC_CODEC_H264_GUID,
1129                NV_ENC_PRESET_P4_GUID,
1130                NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
1131                &mut preset_config,
1132            );
1133            if preset_status != NVENCSTATUS::NV_ENC_SUCCESS {
1134                let detail = function_list.nvEncGetLastErrorString.and_then(|f| {
1135                    let p = f(encoder_session);
1136                    if p.is_null() {
1137                        None
1138                    } else {
1139                        Some(CStr::from_ptr(p).to_string_lossy().into_owned())
1140                    }
1141                });
1142                eprintln!(
1143                    "[NVENC] nvEncGetEncodePresetConfigEx failed ({preset_status:?}): {}",
1144                    detail.as_deref().unwrap_or("no error string")
1145                );
1146            }
1147
1148            config = preset_config.presetCfg;
1149            config.version = sv(NvStruct::Config);
1150            config.profileGUID = profile_guid;
1151            if settings.video_cbr_mode {
1152                let bps = (settings.video_bitrate_kbps.max(0) as u32).saturating_mul(1000);
1153                config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
1154                config.rcParams.multiPass = NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION;
1155                config.rcParams.averageBitRate = bps;
1156                config.rcParams.maxBitRate = bps;
1157                config.rcParams.vbvBufferSize = crate::encoders::vbv_bits(
1158                    bps,
1159                    settings.target_fps,
1160                    settings.keyframe_interval_s,
1161                    settings.video_vbv_multiplier,
1162                );
1163                if settings.video_min_qp > 0 {
1164                    let q = settings.video_min_qp.min(51) as u32;
1165                    config.rcParams.set_enableMinQP(1);
1166                    config.rcParams.minQP.qpInterP = q;
1167                    config.rcParams.minQP.qpInterB = q;
1168                    config.rcParams.minQP.qpIntra = q;
1169                }
1170                if settings.video_max_qp > 0 {
1171                    let q = settings.video_max_qp.min(51) as u32;
1172                    config.rcParams.set_enableMaxQP(1);
1173                    config.rcParams.maxQP.qpInterP = q;
1174                    config.rcParams.maxQP.qpInterB = q;
1175                    config.rcParams.maxQP.qpIntra = q;
1176                }
1177            } else {
1178                config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
1179                config.rcParams.constQP.qpInterP = settings.video_crf as u32;
1180                config.rcParams.constQP.qpInterB = settings.video_crf as u32;
1181                config.rcParams.constQP.qpIntra = settings.video_crf as u32;
1182            }
1183            config.frameIntervalP = 1;
1184            config.gopLength = 0xFFFFFFFF;
1185            config.rcParams.set_zeroReorderDelay(1);
1186            config.encodeCodecConfig.h264Config.h264VUIParameters.bitstreamRestrictionFlag = 1;
1187            config.encodeCodecConfig.h264Config.level =
1188                nvenc_h264_level(width, height, settings.target_fps as u32);
1189            config.encodeCodecConfig.h264Config.idrPeriod = 0xFFFFFFFF;
1190            config.encodeCodecConfig.h264Config.h264VUIParameters.videoSignalTypePresentFlag = 1;
1191            config.encodeCodecConfig.h264Config.h264VUIParameters.videoFormat =
1192                NV_ENC_VUI_VIDEO_FORMAT::NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED;
1193            config.encodeCodecConfig.h264Config.h264VUIParameters.colourDescriptionPresentFlag = 1;
1194            // Primaries and transfer describe the source, which is sRGB desktop pixels —
1195            // sRGB shares BT.709's primaries and transfer function. Only the matrix follows
1196            // the encoder: NVENC's ARGB hardware CSC is fixed at BT.601, so a client that
1197            // inverts BT.709 shifts saturated colour badly.
1198            config.encodeCodecConfig.h264Config.h264VUIParameters.colourPrimaries =
1199                NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709;
1200            config.encodeCodecConfig.h264Config.h264VUIParameters.transferCharacteristics =
1201                NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709;
1202            config.encodeCodecConfig.h264Config.h264VUIParameters.colourMatrix =
1203                NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M;
1204            config.encodeCodecConfig.h264Config.chromaFormatIDC = if is_444 { 3 } else { 1 };
1205            // That same hardware CSC emits limited range in every chroma format, and both
1206            // capture paths — dmabuf and host packed — go through it. Only the raw planar
1207            // entry point could carry full range, and the VUI is per session, so every
1208            // NVENC session declares limited.
1209            config.encodeCodecConfig.h264Config.h264VUIParameters.videoFullRangeFlag = 0;
1210            config.encodeCodecConfig.h264Config.set_repeatSPSPPS(1);
1211            config.encodeCodecConfig.h264Config.entropyCodingMode =
1212                NV_ENC_H264_ENTROPY_CODING_MODE::NV_ENC_H264_ENTROPY_CODING_MODE_CABAC;
1213            config.encodeCodecConfig.h264Config.set_outputAUD(0);
1214            config.rcParams.set_strictGOPTarget(1);
1215            config.rcParams.set_enableLookahead(0);
1216            config.rcParams.lookaheadDepth = 0;
1217
1218            let mut init_params = NV_ENC_INITIALIZE_PARAMS {
1219                version: sv(NvStruct::InitializeParams),
1220                encodeGUID: NV_ENC_CODEC_H264_GUID,
1221                presetGUID: NV_ENC_PRESET_P4_GUID,
1222                tuningInfo: NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
1223                encodeWidth: width,
1224                encodeHeight: height,
1225                darWidth: width,
1226                darHeight: height,
1227                frameRateNum: settings.target_fps.max(1.0) as u32,
1228                frameRateDen: 1,
1229                enablePTD: 1,
1230                encodeConfig: &mut config,
1231                maxEncodeWidth: nvenc_headroom(width, 4096, caps_wmax),
1232                maxEncodeHeight: nvenc_headroom(height, 2304, caps_hmax),
1233                ..Default::default()
1234            };
1235
1236            let init_fn = function_list.nvEncInitializeEncoder.unwrap();
1237            if init_fn(encoder_session, &mut init_params) != NVENCSTATUS::NV_ENC_SUCCESS {
1238                init_params.maxEncodeWidth = width;
1239                init_params.maxEncodeHeight = height;
1240                if init_fn(encoder_session, &mut init_params) != NVENCSTATUS::NV_ENC_SUCCESS {
1241                    (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
1242                    (cuda.cuMemFree_v2)(input_device_ptr);
1243                    (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1244                    (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1245                    return Err("Failed to initialize encoder".into());
1246                }
1247                eprintln!("[NVENC] Init with resize headroom failed; running without it.");
1248            }
1249
1250            init_params.encodeConfig = ptr::null_mut();
1251
1252            let mut reg_res = NV_ENC_REGISTER_RESOURCE {
1253                version: sv(NvStruct::RegisterResource),
1254                resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
1255                width,
1256                height,
1257                resourceToRegister: input_device_ptr as *mut c_void,
1258                pitch: input_pitch as u32,
1259                bufferFormat: NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB,
1260                bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
1261                ..Default::default()
1262            };
1263
1264            let register_fn = function_list.nvEncRegisterResource.unwrap();
1265            if register_fn(encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
1266                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
1267                (cuda.cuMemFree_v2)(input_device_ptr);
1268                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1269                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1270                return Err("Failed to register input buffer".into());
1271            }
1272
1273            let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
1274                version: sv(NvStruct::MapInputResource),
1275                registeredResource: reg_res.registeredResource,
1276                ..Default::default()
1277            };
1278            let map_fn = function_list.nvEncMapInputResource.unwrap();
1279            if map_fn(encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
1280                (function_list.nvEncUnregisterResource.unwrap())(
1281                    encoder_session,
1282                    reg_res.registeredResource,
1283                );
1284                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
1285                (cuda.cuMemFree_v2)(input_device_ptr);
1286                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1287                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1288                return Err("Failed to map input buffer".into());
1289            }
1290
1291            let mut bitstream_buffers = Vec::new();
1292            let create_bs_fn = function_list.nvEncCreateBitstreamBuffer.unwrap();
1293            for _ in 0..4 {
1294                let mut bitstream_params = NV_ENC_CREATE_BITSTREAM_BUFFER {
1295                    version: sv(NvStruct::CreateBitstreamBuffer),
1296                    ..Default::default()
1297                };
1298                if create_bs_fn(encoder_session, &mut bitstream_params)
1299                    != NVENCSTATUS::NV_ENC_SUCCESS
1300                {
1301                    for &bs in &bitstream_buffers {
1302                        (function_list.nvEncDestroyBitstreamBuffer.unwrap())(encoder_session, bs);
1303                    }
1304                    (function_list.nvEncUnmapInputResource.unwrap())(
1305                        encoder_session,
1306                        map_params.mappedResource,
1307                    );
1308                    (function_list.nvEncUnregisterResource.unwrap())(
1309                        encoder_session,
1310                        reg_res.registeredResource,
1311                    );
1312                    (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
1313                    (cuda.cuMemFree_v2)(input_device_ptr);
1314                    (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1315                    (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1316                    return Err("Failed to create bitstream buffer".into());
1317                }
1318                bitstream_buffers.push(bitstream_params.bitstreamBuffer);
1319            }
1320
1321            println!("[NVENC] Initialized successfully (4:4:4 mode: {}).", is_444);
1322
1323            Ok(Self {
1324                encoder_session,
1325                cuda_context: cu_context,
1326                cuda_device: cu_device,
1327                egl_display: egl_display as EGLDisplay,
1328                width,
1329                height,
1330                current_qp: settings.video_crf as u32,
1331                encode_config: config,
1332                init_params,
1333                input_device_ptr,
1334                input_pitch,
1335                input_format: NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB,
1336                registered_input_resource: reg_res.registeredResource,
1337                mapped_input_buffer: map_params.mappedResource,
1338                nv12_device_ptr: None,
1339                nv12_pitch: 0,
1340                nv12_registered_resource: None,
1341                nv12_mapped_buffer: None,
1342                bitstream_buffers,
1343                current_buffer_idx: 0,
1344                dmabuf_cache: HashMap::new(),
1345                pinned_hosts: HashMap::new(),
1346                cuda,
1347                egl,
1348                _nvenc_lib: nvenc_lib,
1349                nvenc_funcs: function_list,
1350                omit_stripe_headers: settings.omit_stripe_headers,
1351                node_index: settings.encode_node_index.max(0),
1352                pin_uploads: std::env::var("PIXELFLUX_NVENC_PIN").as_deref() != Ok("0"),
1353                direct_dmabuf: std::env::var("PIXELFLUX_NVENC_DIRECT").as_deref() != Ok("0"),
1354            })
1355        }
1356    }
1357
1358    /// Resize the live session to `settings` in place, folding in the current rate / QP /
1359    /// fps, without tearing it down.
1360    ///
1361    /// The NVENC session, CUDA context and bitstream buffers survive, so a resize costs a few
1362    /// milliseconds instead of a full rebuild. Flow:
1363    ///
1364    /// 1. **Reject the unchangeable**: a different encode device, a chroma-format flip (4:4:4), an
1365    ///    RC-mode flip, or dimensions of zero or beyond the init-time `maxEncode` headroom all return
1366    ///    `Err` so the caller rebuilds. Chroma and RC mode are read back from the live
1367    ///    `encode_config` (the H.264 arm of the codec-config union is the one this encoder fills).
1368    /// 2. **Release geometry-dependent state** under the pushed CUDA context: unmap / unregister /
1369    ///    free the packed input surface, the raw-plane buffer, every cached dmabuf import (with
1370    ///    the NVENC registration a direct import holds), and every pinned
1371    ///    host. The raw-plane buffer and dmabuf imports are re-created lazily by their encode paths;
1372    ///    pinned hosts are dropped because the source shm segments are recreated on resize and may
1373    ///    reuse the same base addresses.
1374    /// 3. **Reconfigure the session**: update the level for the new size, the CBR bitrate + VBV or
1375    ///    the ConstQP, and the new dimensions / DAR / frame rate, then `NvEncReconfigureEncoder` with
1376    ///    `resetEncoder` and `forceIDR` so the stream restarts cleanly at the new size. Driver
1377    ///    rejection returns `Err`.
1378    /// 4. **Reallocate the packed input** at the new size and register + map it as init does, in
1379    ///    the byte order the session was last fed.
1380    ///
1381    /// On success the next encoded frame is a reset-RC IDR.
1382    pub fn reconfigure_resolution(&mut self, settings: &RustCaptureSettings) -> Result<(), String> {
1383        let new_w = settings.width as u32;
1384        let new_h = settings.height as u32;
1385        let is_444 =
1386            unsafe { self.encode_config.encodeCodecConfig.h264Config.chromaFormatIDC == 3 };
1387        let is_cbr = self.encode_config.rcParams.rateControlMode
1388            == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
1389        if settings.encode_node_index.max(0) != self.node_index {
1390            return Err("encode device changed".into());
1391        }
1392        if settings.video_fullcolor != is_444 {
1393            return Err("chroma format changed".into());
1394        }
1395        if settings.video_cbr_mode != is_cbr {
1396            return Err("rate-control mode changed".into());
1397        }
1398        if new_w == 0
1399            || new_h == 0
1400            || new_w > self.init_params.maxEncodeWidth
1401            || new_h > self.init_params.maxEncodeHeight
1402        {
1403            return Err(format!(
1404                "{}x{} outside reconfigure headroom {}x{}",
1405                new_w, new_h, self.init_params.maxEncodeWidth, self.init_params.maxEncodeHeight
1406            ));
1407        }
1408
1409        unsafe {
1410            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
1411            if !self.mapped_input_buffer.is_null() {
1412                (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
1413                    self.encoder_session,
1414                    self.mapped_input_buffer,
1415                );
1416                self.mapped_input_buffer = ptr::null_mut();
1417            }
1418            if !self.registered_input_resource.is_null() {
1419                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1420                    self.encoder_session,
1421                    self.registered_input_resource,
1422                );
1423                self.registered_input_resource = ptr::null_mut();
1424            }
1425            if self.input_device_ptr != 0 {
1426                (self.cuda.cuMemFree_v2)(self.input_device_ptr);
1427                self.input_device_ptr = 0;
1428            }
1429            if let Some(mapped) = self.nv12_mapped_buffer.take() {
1430                (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(self.encoder_session, mapped);
1431            }
1432            if let Some(registered) = self.nv12_registered_resource.take() {
1433                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1434                    self.encoder_session,
1435                    registered,
1436                );
1437            }
1438            if let Some(ptr) = self.nv12_device_ptr.take() {
1439                (self.cuda.cuMemFree_v2)(ptr);
1440            }
1441            self.nv12_pitch = 0;
1442            let imports: Vec<CachedDmaBuf> = self.dmabuf_cache.drain().map(|(_, c)| c).collect();
1443            for cache in imports {
1444                self.release_dmabuf_import(cache);
1445            }
1446            for (base, len) in self.pinned_hosts.drain() {
1447                if len > 0 {
1448                    (self.cuda.cuMemHostUnregister)(base as *mut c_void);
1449                }
1450            }
1451
1452            self.encode_config.encodeCodecConfig.h264Config.level =
1453                nvenc_h264_level(new_w, new_h, settings.target_fps as u32);
1454            if is_cbr {
1455                let bps = (settings.video_bitrate_kbps.max(0) as u32).saturating_mul(1000);
1456                self.encode_config.rcParams.averageBitRate = bps;
1457                self.encode_config.rcParams.maxBitRate = bps;
1458                self.encode_config.rcParams.vbvBufferSize = crate::encoders::vbv_bits(
1459                    bps,
1460                    settings.target_fps,
1461                    settings.keyframe_interval_s,
1462                    settings.video_vbv_multiplier,
1463                );
1464            } else {
1465                let qp = settings.video_crf as u32;
1466                self.encode_config.rcParams.constQP.qpInterP = qp;
1467                self.encode_config.rcParams.constQP.qpInterB = qp;
1468                self.encode_config.rcParams.constQP.qpIntra = qp;
1469                self.current_qp = qp;
1470            }
1471            self.init_params.encodeWidth = new_w;
1472            self.init_params.encodeHeight = new_h;
1473            self.init_params.darWidth = new_w;
1474            self.init_params.darHeight = new_h;
1475            self.init_params.frameRateNum = (settings.target_fps.max(1.0)) as u32;
1476            self.init_params.frameRateDen = 1;
1477            self.init_params.encodeConfig = &mut self.encode_config;
1478            let mut reconfig_params = NV_ENC_RECONFIGURE_PARAMS {
1479                version: sv(NvStruct::ReconfigureParams),
1480                reInitEncodeParams: self.init_params,
1481                ..Default::default()
1482            };
1483            reconfig_params.set_resetEncoder(1);
1484            reconfig_params.set_forceIDR(1);
1485            let reconfig_fn = self.nvenc_funcs.nvEncReconfigureEncoder.unwrap();
1486            if reconfig_fn(self.encoder_session, &mut reconfig_params)
1487                != NVENCSTATUS::NV_ENC_SUCCESS
1488            {
1489                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1490                return Err("NvEncReconfigureEncoder rejected the resolution change".into());
1491            }
1492            self.width = new_w;
1493            self.height = new_h;
1494
1495            let mut input_device_ptr: CUdeviceptr = 0;
1496            let mut input_pitch: usize = 0;
1497            let res = (self.cuda.cuMemAllocPitch_v2)(
1498                &mut input_device_ptr,
1499                &mut input_pitch,
1500                (new_w * 4) as usize,
1501                new_h as usize,
1502                16,
1503            );
1504            if res != CUresult::CUDA_SUCCESS {
1505                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1506                return Err("Failed to allocate ARGB input buffer on GPU".into());
1507            }
1508            let mut reg_res = NV_ENC_REGISTER_RESOURCE {
1509                version: sv(NvStruct::RegisterResource),
1510                resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
1511                width: new_w,
1512                height: new_h,
1513                resourceToRegister: input_device_ptr as *mut c_void,
1514                pitch: input_pitch as u32,
1515                bufferFormat: self.input_format,
1516                bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
1517                ..Default::default()
1518            };
1519            let register_fn = self.nvenc_funcs.nvEncRegisterResource.unwrap();
1520            if register_fn(self.encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
1521                (self.cuda.cuMemFree_v2)(input_device_ptr);
1522                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1523                return Err("Failed to register input buffer".into());
1524            }
1525            let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
1526                version: sv(NvStruct::MapInputResource),
1527                registeredResource: reg_res.registeredResource,
1528                ..Default::default()
1529            };
1530            let map_fn = self.nvenc_funcs.nvEncMapInputResource.unwrap();
1531            if map_fn(self.encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
1532                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1533                    self.encoder_session,
1534                    reg_res.registeredResource,
1535                );
1536                (self.cuda.cuMemFree_v2)(input_device_ptr);
1537                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1538                return Err("Failed to map input buffer".into());
1539            }
1540            self.input_device_ptr = input_device_ptr;
1541            self.input_pitch = input_pitch;
1542            self.registered_input_resource = reg_res.registeredResource;
1543            self.mapped_input_buffer = map_params.mappedResource;
1544            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1545        }
1546        self.omit_stripe_headers = settings.omit_stripe_headers;
1547        Ok(())
1548    }
1549
1550    /// Page-lock one host upload source's base address once, under the already-current CUDA
1551    /// context, so the copy is a direct pinned DMA instead of a pageable copy staged through a driver
1552    /// bounce buffer. A `0`-length entry records a failed registration so the address is never
1553    /// re-probed; the persistent, bounded shm / reused planar sources make this a one-time cost.
1554    unsafe fn pin_host_source(&mut self, base: usize, len: usize) {
1555        if let std::collections::hash_map::Entry::Vacant(e) = self.pinned_hosts.entry(base) {
1556            let st = (self.cuda.cuMemHostRegister_v2)(base as *mut c_void, len, 0);
1557            e.insert(if st == CUresult::CUDA_SUCCESS { len } else { 0 });
1558        }
1559    }
1560
1561    /// Drop every page-locked host registration, under the pushed CUDA context.
1562    ///
1563    /// Called when the capture's shm segments are recreated at unchanged dimensions: the new
1564    /// segments often reuse the old base addresses, so a stale registration would alias fresh memory.
1565    /// Subsequent uploads re-pin lazily. A `0`-length entry marks a registration that failed and so
1566    /// is not unregistered.
1567    pub fn release_pinned_hosts(&mut self) {
1568        if self.pinned_hosts.is_empty() {
1569            return;
1570        }
1571        unsafe {
1572            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
1573            for (base, len) in self.pinned_hosts.drain() {
1574                if len > 0 {
1575                    (self.cuda.cuMemHostUnregister)(base as *mut c_void);
1576                }
1577            }
1578            let _ = (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1579        }
1580    }
1581
1582    /// Tear down one cached dmabuf import under the already-current CUDA context, inner handle
1583    /// first: the NVENC mapping and registration a direct import holds, then the CUDA graphics
1584    /// resource, then the EGLImage it was built from. The encode that last read the import has
1585    /// completed (`submit_frame` waits for the bitstream), so nothing is still in flight on it.
1586    unsafe fn release_dmabuf_import(&self, cache: CachedDmaBuf) {
1587        if let DmaBufInput::Direct { registered, mapped, .. } = cache.input {
1588            (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(self.encoder_session, mapped);
1589            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(self.encoder_session, registered);
1590        }
1591        (self.cuda.cuGraphicsUnregisterResource)(cache.cuda_resource);
1592        (self.egl.eglDestroyImageKHR)(self.egl_display, cache.egl_image);
1593    }
1594
1595    /// Register the packed input surface with NVENC in the byte order `format` names, when it is
1596    /// not already: the surface memory is unchanged, only its registration (and mapping) is
1597    /// replaced, so a session fed first from one source order and then the other keeps one surface.
1598    /// Runs under the already-current CUDA context; a failed re-registration leaves the surface
1599    /// unregistered and returns `Err`, so the caller's encode fails visibly instead of encoding
1600    /// swapped channels.
1601    unsafe fn set_input_format(&mut self, format: NV_ENC_BUFFER_FORMAT) -> Result<(), String> {
1602        if self.input_format == format && !self.registered_input_resource.is_null() {
1603            return Ok(());
1604        }
1605        if !self.mapped_input_buffer.is_null() {
1606            (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
1607                self.encoder_session,
1608                self.mapped_input_buffer,
1609            );
1610            self.mapped_input_buffer = ptr::null_mut();
1611        }
1612        if !self.registered_input_resource.is_null() {
1613            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1614                self.encoder_session,
1615                self.registered_input_resource,
1616            );
1617            self.registered_input_resource = ptr::null_mut();
1618        }
1619        let mut reg_res = NV_ENC_REGISTER_RESOURCE {
1620            version: sv(NvStruct::RegisterResource),
1621            resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
1622            width: self.width,
1623            height: self.height,
1624            resourceToRegister: self.input_device_ptr as *mut c_void,
1625            pitch: self.input_pitch as u32,
1626            bufferFormat: format,
1627            bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
1628            ..Default::default()
1629        };
1630        if (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res)
1631            != NVENCSTATUS::NV_ENC_SUCCESS
1632        {
1633            return Err(format!("Failed to register input buffer as {format:?}"));
1634        }
1635        let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
1636            version: sv(NvStruct::MapInputResource),
1637            registeredResource: reg_res.registeredResource,
1638            ..Default::default()
1639        };
1640        if (self.nvenc_funcs.nvEncMapInputResource.unwrap())(self.encoder_session, &mut map_params)
1641            != NVENCSTATUS::NV_ENC_SUCCESS
1642        {
1643            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1644                self.encoder_session,
1645                reg_res.registeredResource,
1646            );
1647            return Err(format!("Failed to map input buffer as {format:?}"));
1648        }
1649        self.registered_input_resource = reg_res.registeredResource;
1650        self.mapped_input_buffer = map_params.mappedResource;
1651        self.input_format = format;
1652        Ok(())
1653    }
1654
1655    /// Reconfigure the live session's ConstQP when `target_qp` differs from the current QP,
1656    /// returning whether a reconfigure actually happened.
1657    ///
1658    /// A no-op in CBR mode (bitrate-controlled, so QP-based paint-over does not apply) and when the
1659    /// QP is unchanged. When it does apply, the three `constQP` fields are updated and the session is
1660    /// reconfigured **without** a forced IDR: a lower-QP P-frame refines the static image against the
1661    /// existing reference chain (paint-over) with no intra-frame bitrate spike, so the GOP continues
1662    /// seamlessly across the reconfigure.
1663    unsafe fn reconfigure_if_needed(&mut self, target_qp: u32) -> bool {
1664        if self.encode_config.rcParams.rateControlMode
1665            == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
1666        {
1667            return false;
1668        }
1669        if self.current_qp != target_qp {
1670            self.encode_config.rcParams.constQP.qpInterP = target_qp;
1671            self.encode_config.rcParams.constQP.qpInterB = target_qp;
1672            self.encode_config.rcParams.constQP.qpIntra = target_qp;
1673            self.init_params.encodeConfig = &mut self.encode_config;
1674
1675            let mut reconfig_params = NV_ENC_RECONFIGURE_PARAMS {
1676                version: sv(NvStruct::ReconfigureParams),
1677                reInitEncodeParams: self.init_params,
1678                ..Default::default()
1679            };
1680
1681            let reconfig_fn = self.nvenc_funcs.nvEncReconfigureEncoder.unwrap();
1682            if reconfig_fn(self.encoder_session, &mut reconfig_params)
1683                == NVENCSTATUS::NV_ENC_SUCCESS
1684            {
1685                self.current_qp = target_qp;
1686                return true;
1687            } else {
1688                eprintln!("[NVENC] Reconfigure failed.");
1689            }
1690        }
1691        false
1692    }
1693
1694    /// Apply a runtime rate-control / frame-rate change to the live session.
1695    ///
1696    /// In CBR mode the target bitrate, max bitrate and VBV buffer size are updated (the VBV is
1697    /// ignored outside CBR); the target fps is updated in either mode. The session is reconfigured
1698    /// only when one of these actually changed — no forced IDR, no RC reset — so calling it every
1699    /// frame is cheap.
1700    pub fn reconfigure_rate(&mut self, settings: &RustCaptureSettings) {
1701        unsafe {
1702            let mut changed = false;
1703            if self.encode_config.rcParams.rateControlMode
1704                == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
1705            {
1706                let bps = (settings.video_bitrate_kbps.max(0) as u32).saturating_mul(1000);
1707                let vbv = crate::encoders::vbv_bits(
1708                    bps,
1709                    settings.target_fps,
1710                    settings.keyframe_interval_s,
1711                    settings.video_vbv_multiplier,
1712                );
1713                if self.encode_config.rcParams.averageBitRate != bps
1714                    || self.encode_config.rcParams.maxBitRate != bps
1715                    || self.encode_config.rcParams.vbvBufferSize != vbv
1716                {
1717                    self.encode_config.rcParams.averageBitRate = bps;
1718                    self.encode_config.rcParams.maxBitRate = bps;
1719                    self.encode_config.rcParams.vbvBufferSize = vbv;
1720                    changed = true;
1721                }
1722            }
1723            let fps = (settings.target_fps.max(1.0)) as u32;
1724            if self.init_params.frameRateNum != fps {
1725                self.init_params.frameRateNum = fps;
1726                self.init_params.frameRateDen = 1;
1727                self.encode_config.encodeCodecConfig.h264Config.level = nvenc_h264_level(
1728                    self.init_params.encodeWidth,
1729                    self.init_params.encodeHeight,
1730                    fps,
1731                );
1732                changed = true;
1733            }
1734            if !changed {
1735                return;
1736            }
1737            self.init_params.encodeConfig = &mut self.encode_config;
1738            let mut reconfig_params = NV_ENC_RECONFIGURE_PARAMS {
1739                version: sv(NvStruct::ReconfigureParams),
1740                reInitEncodeParams: self.init_params,
1741                ..Default::default()
1742            };
1743            let reconfig_fn = self.nvenc_funcs.nvEncReconfigureEncoder.unwrap();
1744            if reconfig_fn(self.encoder_session, &mut reconfig_params)
1745                != NVENCSTATUS::NV_ENC_SUCCESS
1746            {
1747                eprintln!("[NVENC] Rate reconfigure failed.");
1748            }
1749        }
1750    }
1751
1752    /// Encode one mapped input picture and return its bitstream bytes behind the wire header.
1753    ///
1754    /// The shared tail of all three encode paths:
1755    ///
1756    /// 1. **Pick an output buffer** from the ring (`current_buffer_idx` advances modulo the ring
1757    ///    length) and submit the picture with `nvEncEncodePicture`; `force_idr` sets the force-IDR
1758    ///    pic flag.
1759    /// 2. **Lock the bitstream** (`nvEncLockBitstream`, blocking) to read the encoded bytes.
1760    /// 3. **Frame the output**: unless `omit_stripe_headers` is set, prepend the 10-byte wire header
1761    ///    — a `0x04` tag, a picture-type byte derived from the *actual* encoded `pictureType`
1762    ///    (IDR = `0x01`, I = `0x02`, P = `0x00`) rather than the `force_idr` request, the low 16 bits
1763    ///    of the frame number, a zero field, and the width and height (all big-endian).
1764    /// 4. **Emit**: append the encoded bytes, unlock the bitstream, and return the framed buffer.
1765    unsafe fn submit_frame(
1766        &mut self,
1767        mapped_buffer: NV_ENC_INPUT_PTR,
1768        buffer_format: NV_ENC_BUFFER_FORMAT,
1769        frame_number: u64,
1770        force_idr: bool,
1771    ) -> Result<Vec<u8>, String> {
1772        let output_bitstream = self.bitstream_buffers[self.current_buffer_idx];
1773        self.current_buffer_idx = (self.current_buffer_idx + 1) % self.bitstream_buffers.len();
1774
1775        let mut pic_params = NV_ENC_PIC_PARAMS {
1776            version: sv(NvStruct::PicParams),
1777            inputWidth: self.width,
1778            inputHeight: self.height,
1779            inputBuffer: mapped_buffer,
1780            outputBitstream: output_bitstream,
1781            bufferFmt: buffer_format,
1782            pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
1783            encodePicFlags: if force_idr {
1784                NV_ENC_PIC_FLAGS::NV_ENC_PIC_FLAG_FORCEIDR as u32
1785            } else {
1786                0
1787            },
1788            ..Default::default()
1789        };
1790
1791        let encode_fn = self.nvenc_funcs.nvEncEncodePicture.unwrap();
1792        let res = encode_fn(self.encoder_session, &mut pic_params);
1793        if res != NVENCSTATUS::NV_ENC_SUCCESS {
1794            return Err(format!("Encode Picture failed: {:?}", res));
1795        }
1796
1797        let mut lock_params = NV_ENC_LOCK_BITSTREAM {
1798            version: sv(NvStruct::LockBitstream),
1799            outputBitstream: output_bitstream,
1800            ..Default::default()
1801        };
1802        lock_params.set_doNotWait(0);
1803
1804        let lock_fn = self.nvenc_funcs.nvEncLockBitstream.unwrap();
1805        if lock_fn(self.encoder_session, &mut lock_params) != NVENCSTATUS::NV_ENC_SUCCESS {
1806            return Err("Lock Bitstream failed".into());
1807        }
1808
1809        let data_ptr = lock_params.bitstreamBufferPtr as *const u8;
1810        let data_size = lock_params.bitstreamSizeInBytes as usize;
1811        let header_sz = if self.omit_stripe_headers { 0 } else { 10 };
1812        let mut output = Vec::with_capacity(header_sz + data_size);
1813
1814        if !self.omit_stripe_headers {
1815            let type_hdr = match lock_params.pictureType {
1816                NV_ENC_PIC_TYPE::NV_ENC_PIC_TYPE_IDR => 0x01u8,
1817                NV_ENC_PIC_TYPE::NV_ENC_PIC_TYPE_I => 0x02u8,
1818                _ => 0x00u8,
1819            };
1820            output.push(0x04);
1821            output.push(type_hdr);
1822            output.extend_from_slice(&(frame_number as u16).to_be_bytes());
1823            output.extend_from_slice(&0u16.to_be_bytes());
1824            output.extend_from_slice(&(self.width as u16).to_be_bytes());
1825            output.extend_from_slice(&(self.height as u16).to_be_bytes());
1826        }
1827
1828        if data_size > 0 && !data_ptr.is_null() {
1829            let slice = std::slice::from_raw_parts(data_ptr, data_size);
1830            output.extend_from_slice(slice);
1831        }
1832
1833        (self.nvenc_funcs.nvEncUnlockBitstream.unwrap())(self.encoder_session, output_bitstream);
1834        Ok(output)
1835    }
1836
1837    /// Encode a dmabuf frame zero-copy, by importing it through EGL into CUDA and, where the
1838    /// driver allows, handing the mapped plane to NVENC as its input.
1839    ///
1840    /// Applies any pending ConstQP change, then works under the pushed CUDA context:
1841    ///
1842    /// 1. **Import once, cache by fd with an identity check**: the cache is keyed by the dmabuf fd
1843    ///    but each entry stores the buffer's `DmaBufIdentity`; an entry whose identity no longer
1844    ///    matches (a recycled fd) is released first. On a miss, build an `EGLImageKHR` from the
1845    ///    dmabuf's fd / offset / pitch / modifier, register it as a CUDA graphics resource, map it to
1846    ///    a `CUeglFrame`, and settle how it feeds the encoder: a first plane that `direct_plane`
1847    ///    accepts (and `direct_dmabuf` on) is registered with NVENC in place — a pitch-linear plane
1848    ///    as a CUDA device pointer at its own pitch, a four-channel 8-bit CUDA array as a CUDA
1849    ///    array — in the byte order the dmabuf fourcc names, and mapped once (`DmaBufInput::Direct`);
1850    ///    any other plane, or a registration the driver refuses, takes `DmaBufInput::Copy`. The
1851    ///    result is memoized so a recurring capture buffer pays the import cost only once. Each
1852    ///    failure destroys what it created and pops the context.
1853    /// 2. **Feed the encoder**: a direct import is submitted as it is — no copy at all. A copy
1854    ///    import is copied with `cuMemcpy2DAsync` on the default stream — the array plane or the
1855    ///    pitch-linear plane, per `frame_type` — into the packed input surface, re-registered in the
1856    ///    dmabuf's byte order when it differs; NVENC processes its input on that same stream, so the
1857    ///    copy is ordered before the encode without a host wait.
1858    /// 3. **Submit** via `submit_frame`, then pop the context.
1859    ///
1860    /// The dmabuf fd is read out before the context is pushed so an early `?` return cannot leave the
1861    /// CUDA context stack imbalanced.
1862    pub fn encode(
1863        &mut self,
1864        dmabuf: &Dmabuf,
1865        frame_number: u64,
1866        target_qp: u32,
1867        force_idr: bool,
1868    ) -> Result<Vec<u8>, String> {
1869        unsafe {
1870            self.reconfigure_if_needed(target_qp);
1871            let fd = dmabuf.handles().next().ok_or("No handles")?.as_raw_fd();
1872            let fmt = dmabuf.format();
1873            let modifier: u64 = fmt.modifier.into();
1874            let identity = DmaBufIdentity::probe(fd, modifier, self.width, self.height);
1875            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
1876
1877            // A raw fd number is not an identity: the host recycles fd numbers across slot
1878            // renegotiations, so an entry whose stored identity no longer matches is torn down and
1879            // re-imported rather than returning a stale EGLImage for a buffer the fd no longer names.
1880            if self.dmabuf_cache.get(&fd).is_some_and(|c| c.identity != identity)
1881                && let Some(stale) = self.dmabuf_cache.remove(&fd)
1882            {
1883                self.release_dmabuf_import(stale);
1884            }
1885
1886            if !self.dmabuf_cache.contains_key(&fd) {
1887                let stride = dmabuf.strides().next().unwrap_or(0) as i32;
1888                let offset = dmabuf.offsets().next().unwrap_or(0) as i32;
1889
1890                let attribs = [
1891                    EGL_WIDTH,
1892                    self.width as i32,
1893                    EGL_HEIGHT,
1894                    self.height as i32,
1895                    EGL_LINUX_DRM_FOURCC_EXT,
1896                    fmt.code as i32,
1897                    EGL_DMA_BUF_PLANE0_FD_EXT,
1898                    fd,
1899                    EGL_DMA_BUF_PLANE0_OFFSET_EXT,
1900                    offset,
1901                    EGL_DMA_BUF_PLANE0_PITCH_EXT,
1902                    stride,
1903                    EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
1904                    (modifier & 0xFFFFFFFF) as i32,
1905                    EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
1906                    (modifier >> 32) as i32,
1907                    EGL_NONE,
1908                ];
1909
1910                let egl_image = (self.egl.eglCreateImageKHR)(
1911                    self.egl_display,
1912                    ptr::null_mut(),
1913                    EGL_LINUX_DMA_BUF_EXT,
1914                    ptr::null_mut(),
1915                    attribs.as_ptr(),
1916                );
1917                if egl_image == EGL_NO_IMAGE_KHR {
1918                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1919                    return Err("Failed to create EGLImage".into());
1920                }
1921
1922                let mut cuda_resource: CUgraphicsResource = ptr::null_mut();
1923                if (self.cuda.cuGraphicsEGLRegisterImage)(&mut cuda_resource, egl_image, 1)
1924                    != CUresult::CUDA_SUCCESS
1925                {
1926                    (self.egl.eglDestroyImageKHR)(self.egl_display, egl_image);
1927                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1928                    return Err("Failed to register EGLImage".into());
1929                }
1930
1931                let mut egl_frame: CUeglFrame = std::mem::zeroed();
1932                if (self.cuda.cuGraphicsResourceGetMappedEglFrame)(
1933                    &mut egl_frame,
1934                    cuda_resource,
1935                    0,
1936                    0,
1937                ) != CUresult::CUDA_SUCCESS
1938                {
1939                    (self.cuda.cuGraphicsUnregisterResource)(cuda_resource);
1940                    (self.egl.eglDestroyImageKHR)(self.egl_display, egl_image);
1941                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1942                    return Err("Failed to map EGL frame".into());
1943                }
1944
1945                let input = match (
1946                    self.direct_dmabuf,
1947                    direct_plane(&egl_frame, self.width, self.height),
1948                    fourcc_nvenc_format(fmt.code),
1949                ) {
1950                    (true, Some(plane), Some(format)) => {
1951                        self.register_direct_input(&egl_frame, plane, format)
1952                    }
1953                    _ => DmaBufInput::Copy,
1954                };
1955                println!(
1956                    "[NVENC] dmabuf imported as a {} frame ({} planes, {}x{}, pitch {}, {} channels of element format {}): {}.",
1957                    match egl_frame.frame_type {
1958                        CU_EGL_FRAME_TYPE_PITCH => "pitch-linear",
1959                        CU_EGL_FRAME_TYPE_ARRAY => "CUDA-array",
1960                        _ => "unknown-kind",
1961                    },
1962                    egl_frame.plane_count,
1963                    egl_frame.width,
1964                    egl_frame.height,
1965                    egl_frame.pitch,
1966                    egl_frame.num_channels,
1967                    egl_frame.cu_format,
1968                    match input {
1969                        DmaBufInput::Direct { .. } => "encoding in place",
1970                        DmaBufInput::Copy => "copying per frame",
1971                    }
1972                );
1973
1974                self.dmabuf_cache.insert(
1975                    fd,
1976                    CachedDmaBuf {
1977                        identity,
1978                        egl_image,
1979                        cuda_resource,
1980                        egl_frame,
1981                        input,
1982                    },
1983                );
1984            }
1985
1986            let (egl_frame, input) = {
1987                let cached = self.dmabuf_cache.get(&fd).unwrap();
1988                (cached.egl_frame, cached.input)
1989            };
1990            let (mapped, format) = match input {
1991                DmaBufInput::Direct { mapped, format, .. } => (mapped, format),
1992                DmaBufInput::Copy => {
1993                    let mut copy_params = CUDA_MEMCPY2D {
1994                        srcMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
1995                        srcHost: ptr::null(),
1996                        srcDevice: 0,
1997                        srcArray: ptr::null_mut(),
1998                        srcPitch: 0,
1999                        dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2000                        dstHost: ptr::null_mut(),
2001                        dstDevice: self.input_device_ptr,
2002                        dstArray: ptr::null_mut(),
2003                        dstPitch: self.input_pitch,
2004                        WidthInBytes: (self.width * 4) as usize,
2005                        Height: self.height as usize,
2006                        ..Default::default()
2007                    };
2008                    if egl_frame.frame_type == CU_EGL_FRAME_TYPE_ARRAY {
2009                        copy_params.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_ARRAY;
2010                        copy_params.srcArray = egl_frame.frame.p_array[0];
2011                    } else {
2012                        copy_params.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_DEVICE;
2013                        copy_params.srcDevice = egl_frame.frame.p_pitch[0] as CUdeviceptr;
2014                        copy_params.srcPitch = egl_frame.pitch as usize;
2015                    }
2016                    // A fourcc without a packed NVENC equivalent keeps the surface's current
2017                    // registration; the copy still lands the bytes, as it always has.
2018                    if let Some(format) = fourcc_nvenc_format(fmt.code)
2019                        && let Err(e) = self.set_input_format(format)
2020                    {
2021                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2022                        return Err(e);
2023                    }
2024                    if (self.cuda.cuMemcpy2DAsync_v2)(&copy_params, ptr::null_mut())
2025                        != CUresult::CUDA_SUCCESS
2026                    {
2027                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2028                        return Err("Sanitization copy failed".into());
2029                    }
2030                    (self.mapped_input_buffer, self.input_format)
2031                }
2032            };
2033
2034            let result = self.submit_frame(mapped, format, frame_number, force_idr);
2035            if result.is_err() {
2036                (self.cuda.cuStreamSynchronize)(ptr::null_mut());
2037            }
2038            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2039            result
2040        }
2041    }
2042
2043    /// Register the first plane of a mapped dmabuf frame with NVENC in place — as a pitch-linear
2044    /// CUDA device pointer or as a CUDA array, per `plane` — and map it as an input, under the
2045    /// already-current CUDA context. Either step failing falls back to `DmaBufInput::Copy` — the
2046    /// per-frame copy then serves that import for as long as it is cached, so a driver that declines
2047    /// the direct path costs one failed registration, not a failed frame.
2048    unsafe fn register_direct_input(
2049        &mut self,
2050        frame: &CUeglFrame,
2051        plane: DirectPlane,
2052        format: NV_ENC_BUFFER_FORMAT,
2053    ) -> DmaBufInput {
2054        let (resource_type, resource, pitch) = match plane {
2055            DirectPlane::Pitch(pitch) => (
2056                NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
2057                frame.frame.p_pitch[0],
2058                pitch,
2059            ),
2060            DirectPlane::Array(row_bytes) => (
2061                NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDAARRAY,
2062                frame.frame.p_array[0] as *mut c_void,
2063                row_bytes,
2064            ),
2065        };
2066        let mut reg_res = NV_ENC_REGISTER_RESOURCE {
2067            version: sv(NvStruct::RegisterResource),
2068            resourceType: resource_type,
2069            width: self.width,
2070            height: self.height,
2071            resourceToRegister: resource,
2072            pitch,
2073            bufferFormat: format,
2074            bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
2075            ..Default::default()
2076        };
2077        let st = (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res);
2078        if st != NVENCSTATUS::NV_ENC_SUCCESS {
2079            eprintln!("[NVENC] dmabuf plane registration ({plane:?}) refused ({st:?}); copying per frame.");
2080            return DmaBufInput::Copy;
2081        }
2082        let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
2083            version: sv(NvStruct::MapInputResource),
2084            registeredResource: reg_res.registeredResource,
2085            ..Default::default()
2086        };
2087        let st = (self.nvenc_funcs.nvEncMapInputResource.unwrap())(self.encoder_session, &mut map_params);
2088        if st != NVENCSTATUS::NV_ENC_SUCCESS {
2089            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
2090                self.encoder_session,
2091                reg_res.registeredResource,
2092            );
2093            eprintln!("[NVENC] dmabuf plane mapping refused ({st:?}); copying per frame.");
2094            return DmaBufInput::Copy;
2095        }
2096        DmaBufInput::Direct {
2097            registered: reg_res.registeredResource,
2098            mapped: map_params.mappedResource,
2099            format,
2100        }
2101    }
2102
2103    /// Encode a host BGRA frame (B,G,R,A in memory, NVENC's word-ordered `ARGB` — the layout an
2104    /// XShm grab or the pixman framebuffer yields) through `encode_cpu_packed`.
2105    pub fn encode_cpu_argb(
2106        &mut self,
2107        argb: &[u8],
2108        src_stride: usize,
2109        frame_number: u64,
2110        target_qp: u32,
2111        force_idr: bool,
2112    ) -> Result<Vec<u8>, String> {
2113        self.encode_cpu_packed(argb, src_stride, false, frame_number, target_qp, force_idr)
2114    }
2115
2116    /// Encode a host packed-pixel frame by uploading it straight into the packed input surface,
2117    /// with no CPU-side colour conversion: NVENC's hardware RGB→YUV conversion is fixed at BT.601
2118    /// limited range, which is what the session VUI declares, and a CPU prepass to BT.709 would
2119    /// cost this path its copy-free property.
2120    ///
2121    /// `rgba_input` names the byte order — `false` for B,G,R,A (X11 XShm, the pixman framebuffer),
2122    /// `true` for R,G,B,A (a GLES readback) — and the input surface is registered with NVENC in
2123    /// that order (`ARGB` / `ABGR`, re-registered in place when it changes), so both arrive at the
2124    /// hardware CSC untouched. `src_stride` is the source row stride in bytes (`>= width*4`).
2125    /// Steps, under the pushed CUDA context after any pending QP change:
2126    ///
2127    /// 1. **Bounds-check** the source against `stride × (rows-1) + width*4`, erroring rather than
2128    ///    reading past a short buffer.
2129    /// 2. **Pin the source once**: unless pinning was disabled at init (`PIXELFLUX_NVENC_PIN=0`, read
2130    ///    once into `pin_uploads`), page-lock each distinct source base address via `pin_host_source`
2131    ///    so the upload is a direct DMA from the caller's buffer instead of a pageable copy staged
2132    ///    through a driver bounce buffer. The persistent, bounded shm / pool sources make this a
2133    ///    one-time bounded cost.
2134    /// 3. **Upload and submit**: `cuMemcpy2DAsync` the rows into the input surface on the default
2135    ///    stream honoring `src_stride`, then `submit_frame`. NVENC processes its input on that same
2136    ///    stream, so the upload is ordered before the encode without a host wait, and the blocking
2137    ///    bitstream lock inside `submit_frame` (or the stream sync on its error path) guarantees the
2138    ///    upload has finished reading `pixels` by the time this returns — the caller may reuse the
2139    ///    buffer immediately.
2140    pub fn encode_cpu_packed(
2141        &mut self,
2142        pixels: &[u8],
2143        src_stride: usize,
2144        rgba_input: bool,
2145        frame_number: u64,
2146        target_qp: u32,
2147        force_idr: bool,
2148    ) -> Result<Vec<u8>, String> {
2149        unsafe {
2150            self.reconfigure_if_needed(target_qp);
2151            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
2152
2153            let width_bytes = (self.width * 4) as usize;
2154            let rows = self.height as usize;
2155            let needed = if rows == 0 { 0 } else { src_stride * (rows - 1) + width_bytes };
2156            if src_stride < width_bytes || pixels.len() < needed {
2157                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2158                return Err(format!(
2159                    "packed buffer too small: len={} need>={} (stride={}, {}x{})",
2160                    pixels.len(), needed, src_stride, self.width, self.height
2161                ));
2162            }
2163
2164            let format = if rgba_input {
2165                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
2166            } else {
2167                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
2168            };
2169            if let Err(e) = self.set_input_format(format) {
2170                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2171                return Err(e);
2172            }
2173
2174            if self.pin_uploads {
2175                self.pin_host_source(pixels.as_ptr() as usize, pixels.len());
2176            }
2177
2178            let copy = CUDA_MEMCPY2D {
2179                srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2180                srcHost: pixels.as_ptr() as *const c_void,
2181                srcPitch: src_stride,
2182                dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2183                dstDevice: self.input_device_ptr,
2184                dstPitch: self.input_pitch,
2185                WidthInBytes: width_bytes,
2186                Height: rows,
2187                ..Default::default()
2188            };
2189            if (self.cuda.cuMemcpy2DAsync_v2)(&copy, ptr::null_mut()) != CUresult::CUDA_SUCCESS {
2190                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2191                return Err("packed host->device upload failed".into());
2192            }
2193
2194            let result = self.submit_frame(
2195                self.mapped_input_buffer,
2196                self.input_format,
2197                frame_number,
2198                force_idr,
2199            );
2200            if result.is_err() {
2201                (self.cuda.cuStreamSynchronize)(ptr::null_mut());
2202            }
2203            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2204            result
2205        }
2206    }
2207
2208    /// Encode a raw planar frame — NV12 (4:2:0) or YUV444 — uploaded host→device.
2209    ///
2210    /// The planar-input counterpart to `encode_cpu_argb`, used when the caller has already produced
2211    /// YUV. The chroma format follows the session's `chromaFormatIDC` (3 ⇒ YUV444, else NV12). Flow
2212    /// under the pushed CUDA context after any pending QP change:
2213    ///
2214    /// 1. **Pin the source once** (unless disabled at init): the caller reuses one planar buffer
2215    ///    across frames, so page-locking its base via `pin_host_source` turns each upload into a
2216    ///    direct pinned DMA instead of a pageable copy through a bounce buffer.
2217    /// 2. **Lazily allocate** the planar device buffer on first use: a pitched allocation tall enough
2218    ///    for three full planes (YUV444) or Y plus half-height interleaved UV (NV12), registered and
2219    ///    mapped with the matching buffer format.
2220    /// 3. **Upload each plane** with its own `cuMemcpy2D`. Every copy is bounds-checked against the
2221    ///    host slice: the Y plane is required in full (a short buffer errors), and each chroma plane
2222    ///    is copied only if its **entire** span — not merely its start offset — is present, so a
2223    ///    truncated buffer never reads past its end.
2224    /// 4. **Submit** the mapped planar input via `submit_frame`.
2225    pub fn encode_raw(
2226        &mut self,
2227        raw_data: &[u8],
2228        frame_number: u64,
2229        target_qp: u32,
2230        force_idr: bool,
2231    ) -> Result<Vec<u8>, String> {
2232        unsafe {
2233            self.reconfigure_if_needed(target_qp);
2234            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
2235
2236            if self.pin_uploads {
2237                self.pin_host_source(raw_data.as_ptr() as usize, raw_data.len());
2238            }
2239
2240            let is_444 = self.encode_config.encodeCodecConfig.h264Config.chromaFormatIDC == 3;
2241
2242            if self.nv12_device_ptr.is_none() {
2243                let mut d_ptr: CUdeviceptr = 0;
2244                let mut pitch: usize = 0;
2245
2246                let alloc_height = if is_444 {
2247                    self.height * 3
2248                } else {
2249                    self.height + (self.height / 2)
2250                };
2251
2252                let res = (self.cuda.cuMemAllocPitch_v2)(
2253                    &mut d_ptr,
2254                    &mut pitch,
2255                    self.width as usize,
2256                    alloc_height as usize,
2257                    16,
2258                );
2259                if res != CUresult::CUDA_SUCCESS {
2260                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2261                    return Err("Failed to allocate GPU buffer for raw input".into());
2262                }
2263
2264                let buffer_fmt = if is_444 {
2265                    NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444
2266                } else {
2267                    NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_NV12
2268                };
2269
2270                let mut reg_res = NV_ENC_REGISTER_RESOURCE {
2271                    version: sv(NvStruct::RegisterResource),
2272                    resourceType:
2273                        NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
2274                    width: self.width,
2275                    height: self.height,
2276                    resourceToRegister: d_ptr as *mut c_void,
2277                    pitch: pitch as u32,
2278                    bufferFormat: buffer_fmt,
2279                    bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
2280                    ..Default::default()
2281                };
2282
2283                let register_fn = self.nvenc_funcs.nvEncRegisterResource.unwrap();
2284                if register_fn(self.encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
2285                    (self.cuda.cuMemFree_v2)(d_ptr);
2286                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2287                    return Err("Failed to register raw input buffer".into());
2288                }
2289
2290                let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
2291                    version: sv(NvStruct::MapInputResource),
2292                    registeredResource: reg_res.registeredResource,
2293                    ..Default::default()
2294                };
2295                let map_fn = self.nvenc_funcs.nvEncMapInputResource.unwrap();
2296                if map_fn(self.encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
2297                    (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
2298                        self.encoder_session,
2299                        reg_res.registeredResource,
2300                    );
2301                    (self.cuda.cuMemFree_v2)(d_ptr);
2302                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2303                    return Err("Failed to map raw input buffer".into());
2304                }
2305
2306                self.nv12_device_ptr = Some(d_ptr);
2307                self.nv12_pitch = pitch;
2308                self.nv12_registered_resource = Some(reg_res.registeredResource);
2309                self.nv12_mapped_buffer = Some(map_params.mappedResource);
2310            }
2311
2312            let dev_ptr = self.nv12_device_ptr.unwrap();
2313            let dev_pitch = self.nv12_pitch;
2314            let width_bytes = self.width as usize;
2315            let height = self.height as usize;
2316
2317            if is_444 {
2318                let plane_size = width_bytes * height;
2319                if raw_data.len() < plane_size {
2320                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2321                    return Err("raw frame smaller than the Y plane (444)".into());
2322                }
2323
2324                let copy_y = CUDA_MEMCPY2D {
2325                    srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2326                    srcHost: raw_data.as_ptr() as *const c_void,
2327                    srcPitch: width_bytes,
2328                    dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2329                    dstDevice: dev_ptr,
2330                    dstPitch: dev_pitch,
2331                    WidthInBytes: width_bytes,
2332                    Height: height,
2333                    ..Default::default()
2334                };
2335                if (self.cuda.cuMemcpy2D_v2)(&copy_y) != CUresult::CUDA_SUCCESS {
2336                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2337                    return Err("Failed to copy Y plane (444)".into());
2338                }
2339
2340                if raw_data.len() >= 2 * plane_size {
2341                    let copy_u = CUDA_MEMCPY2D {
2342                        srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2343                        srcHost: raw_data[plane_size..].as_ptr() as *const c_void,
2344                        srcPitch: width_bytes,
2345                        dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2346                        dstDevice: dev_ptr + (dev_pitch * height) as u64,
2347                        dstPitch: dev_pitch,
2348                        WidthInBytes: width_bytes,
2349                        Height: height,
2350                        ..Default::default()
2351                    };
2352                    if (self.cuda.cuMemcpy2D_v2)(&copy_u) != CUresult::CUDA_SUCCESS {
2353                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2354                        return Err("Failed to copy U plane (444)".into());
2355                    }
2356                }
2357
2358                if raw_data.len() >= 3 * plane_size {
2359                    let copy_v = CUDA_MEMCPY2D {
2360                        srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2361                        srcHost: raw_data[2 * plane_size..].as_ptr() as *const c_void,
2362                        srcPitch: width_bytes,
2363                        dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2364                        dstDevice: dev_ptr + (dev_pitch * height * 2) as u64,
2365                        dstPitch: dev_pitch,
2366                        WidthInBytes: width_bytes,
2367                        Height: height,
2368                        ..Default::default()
2369                    };
2370                    if (self.cuda.cuMemcpy2D_v2)(&copy_v) != CUresult::CUDA_SUCCESS {
2371                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2372                        return Err("Failed to copy V plane (444)".into());
2373                    }
2374                }
2375            } else {
2376                let y_size = width_bytes * height;
2377                if raw_data.len() < y_size {
2378                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2379                    return Err("raw frame smaller than the Y plane".into());
2380                }
2381                let copy_y = CUDA_MEMCPY2D {
2382                    srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2383                    srcHost: raw_data.as_ptr() as *const c_void,
2384                    srcPitch: width_bytes,
2385                    dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2386                    dstDevice: dev_ptr,
2387                    dstPitch: dev_pitch,
2388                    WidthInBytes: width_bytes,
2389                    Height: height,
2390                    ..Default::default()
2391                };
2392                if (self.cuda.cuMemcpy2D_v2)(&copy_y) != CUresult::CUDA_SUCCESS {
2393                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2394                    return Err("Failed to copy Y plane".into());
2395                }
2396
2397                let uv_offset = y_size;
2398                if raw_data.len() >= uv_offset + width_bytes * (height / 2) {
2399                    let copy_uv = CUDA_MEMCPY2D {
2400                        srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2401                        srcHost: raw_data[uv_offset..].as_ptr() as *const c_void,
2402                        srcPitch: width_bytes,
2403                        dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2404                        dstDevice: dev_ptr + (dev_pitch * height) as u64,
2405                        dstPitch: dev_pitch,
2406                        WidthInBytes: width_bytes,
2407                        Height: height / 2,
2408                        ..Default::default()
2409                    };
2410                    if (self.cuda.cuMemcpy2D_v2)(&copy_uv) != CUresult::CUDA_SUCCESS {
2411                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2412                        return Err("Failed to copy UV plane".into());
2413                    }
2414                }
2415            }
2416
2417            let raw_format = if self.encode_config.encodeCodecConfig.h264Config.chromaFormatIDC == 3 {
2418                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444
2419            } else {
2420                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_NV12
2421            };
2422            let result =
2423                self.submit_frame(self.nv12_mapped_buffer.unwrap(), raw_format, frame_number, force_idr);
2424            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2425            result
2426        }
2427    }
2428}
2429
2430#[cfg(test)]
2431mod gpu_tests {
2432    use super::*;
2433
2434    /// Test helper: H.264 full-frame capture settings at `w×h`, `fps`, CRF 25.
2435    fn settings(w: i32, h: i32, fps: f64) -> RustCaptureSettings {
2436        RustCaptureSettings {
2437            width: w,
2438            height: h,
2439            output_mode: 1,
2440            target_fps: fps,
2441            video_crf: 25,
2442            ..Default::default()
2443        }
2444    }
2445
2446    /// Test helper: a `w×h` BGRA frame filled with a hashed gradient (offset by `seed`) so
2447    /// the content has structure and encodes are non-trivial.
2448    fn frame(w: usize, h: usize, seed: u8) -> Vec<u8> {
2449        let mut f = vec![0u8; w * h * 4];
2450        for (i, px) in f.chunks_exact_mut(4).enumerate() {
2451            let v = ((i as u32).wrapping_mul(2654435761) >> 24) as u8;
2452            px[0] = v.wrapping_add(seed);
2453            px[1] = v ^ seed;
2454            px[2] = seed;
2455            px[3] = 255;
2456        }
2457        f
2458    }
2459
2460    /// Test helper: read the big-endian width/height (bytes 6-9) from a 10-byte wire header.
2461    fn wire_dims(pkt: &[u8]) -> (u16, u16) {
2462        (
2463            u16::from_be_bytes([pkt[6], pkt[7]]),
2464            u16::from_be_bytes([pkt[8], pkt[9]]),
2465        )
2466    }
2467
2468    /// End-to-end in-place resize on a real GPU: encode 720p, grow to 1080p and verify the
2469    /// first post-resize frame is an IDR (steady frames are P), shrink to 480p, exercise the
2470    /// rejection cases (beyond headroom, chroma flip, RC-mode flip) and confirm the session still
2471    /// encodes afterward, and optionally dump a decodable stream. Ignored by default; run with
2472    /// `cargo test gpu_ -- --ignored --nocapture --test-threads=1` — building NVENC sessions
2473    /// concurrently races in the driver and intermittently faults, so the GPU set runs serially.
2474    #[test]
2475    #[ignore]
2476    fn gpu_resolution_reconfigure_roundtrip() {
2477        let mut s = settings(1280, 720, 60.0);
2478        let t0 = std::time::Instant::now();
2479        let mut enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init");
2480        let init_ms = t0.elapsed().as_secs_f64() * 1000.0;
2481
2482        let mut stream: Vec<u8> = Vec::new();
2483        let f720 = frame(1280, 720, 10);
2484        for i in 0..5u64 {
2485            let pkt = enc
2486                .encode_cpu_argb(&f720, 1280 * 4, i, 25, i == 0)
2487                .expect("encode 720p");
2488            assert_eq!(wire_dims(&pkt), (1280, 720));
2489            stream.extend_from_slice(&pkt[10..]);
2490        }
2491
2492        s.width = 1920;
2493        s.height = 1080;
2494        let t1 = std::time::Instant::now();
2495        enc.reconfigure_resolution(&s).expect("grow reconfigure");
2496        let grow_ms = t1.elapsed().as_secs_f64() * 1000.0;
2497        let f1080 = frame(1920, 1080, 40);
2498        let pkt = enc
2499            .encode_cpu_argb(&f1080, 1920 * 4, 5, 25, false)
2500            .expect("encode 1080p");
2501        assert_eq!(pkt[0], 0x04);
2502        assert_eq!(pkt[1], 0x01, "first frame after a resize must be an IDR");
2503        assert_eq!(wire_dims(&pkt), (1920, 1080));
2504        stream.extend_from_slice(&pkt[10..]);
2505        for i in 6..10u64 {
2506            let pkt = enc
2507                .encode_cpu_argb(&f1080, 1920 * 4, i, 25, false)
2508                .expect("encode 1080p");
2509            assert_eq!(pkt[1], 0x00, "steady frames after the IDR are P frames");
2510            stream.extend_from_slice(&pkt[10..]);
2511        }
2512
2513        s.width = 640;
2514        s.height = 480;
2515        let t2 = std::time::Instant::now();
2516        enc.reconfigure_resolution(&s).expect("shrink reconfigure");
2517        let shrink_ms = t2.elapsed().as_secs_f64() * 1000.0;
2518        let f480 = frame(640, 480, 70);
2519        let pkt = enc
2520            .encode_cpu_argb(&f480, 640 * 4, 10, 25, false)
2521            .expect("encode 480p");
2522        assert_eq!(pkt[1], 0x01);
2523        assert_eq!(wire_dims(&pkt), (640, 480));
2524        stream.extend_from_slice(&pkt[10..]);
2525
2526        s.width = 4100;
2527        s.height = 2400;
2528        assert!(enc.reconfigure_resolution(&s).is_err(), "beyond headroom");
2529        s.width = 640;
2530        s.height = 480;
2531        s.video_fullcolor = true;
2532        assert!(enc.reconfigure_resolution(&s).is_err(), "chroma flip");
2533        s.video_fullcolor = false;
2534        s.video_cbr_mode = true;
2535        assert!(enc.reconfigure_resolution(&s).is_err(), "RC mode flip");
2536        s.video_cbr_mode = false;
2537        let pkt = enc
2538            .encode_cpu_argb(&f480, 640 * 4, 11, 25, false)
2539            .expect("session survives rejected reconfigures");
2540        stream.extend_from_slice(&pkt[10..]);
2541
2542        println!(
2543            "init={init_ms:.1}ms grow(720p->1080p)={grow_ms:.1}ms shrink(1080p->480p)={shrink_ms:.1}ms"
2544        );
2545        if let Ok(path) = std::env::var("NVENC_TEST_DUMP") {
2546            std::fs::write(&path, &stream).unwrap();
2547            println!("wrote {} bytes to {path}", stream.len());
2548        }
2549    }
2550
2551    /// On a real GPU, a CBR session resized 720p→1080p folds the new bitrate into the resize
2552    /// reconfigure (asserts `averageBitRate` updated to 8 Mbit/s) and the first post-resize frame is
2553    /// an IDR at the new dimensions. Ignored by default.
2554    #[test]
2555    #[ignore]
2556    fn gpu_resolution_reconfigure_cbr() {
2557        let mut s = settings(1280, 720, 60.0);
2558        s.video_cbr_mode = true;
2559        s.video_bitrate_kbps = 4000;
2560        let mut enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init");
2561        let f720 = frame(1280, 720, 10);
2562        for i in 0..3u64 {
2563            enc.encode_cpu_argb(&f720, 1280 * 4, i, 25, i == 0)
2564                .expect("encode 720p");
2565        }
2566        s.width = 1920;
2567        s.height = 1080;
2568        s.video_bitrate_kbps = 8000;
2569        enc.reconfigure_resolution(&s).expect("cbr resize+rate");
2570        assert_eq!(enc.encode_config.rcParams.averageBitRate, 8_000_000);
2571        let f1080 = frame(1920, 1080, 40);
2572        let pkt = enc
2573            .encode_cpu_argb(&f1080, 1920 * 4, 3, 25, false)
2574            .expect("encode 1080p");
2575        assert_eq!(pkt[1], 0x01);
2576        assert_eq!(wire_dims(&pkt), (1920, 1080));
2577    }
2578
2579    /// On a real GPU, print the device-memory cost of one 1080p session (via `nvidia-smi`),
2580    /// for measuring the reconfigure-headroom overhead. Ignored by default.
2581    #[test]
2582    #[ignore]
2583    fn gpu_vram_probe() {
2584        fn used_mb() -> i64 {
2585            let out = std::process::Command::new("nvidia-smi")
2586                .args(["--query-gpu=memory.used", "--format=csv,noheader,nounits"])
2587                .output()
2588                .expect("nvidia-smi");
2589            String::from_utf8_lossy(&out.stdout).trim().parse().expect("parse MiB")
2590        }
2591        let s = settings(1920, 1080, 60.0);
2592        let before = used_mb();
2593        let mut enc = NvencEncoder::new(&s, ptr::null()).expect("init");
2594        let f = frame(1920, 1080, 5);
2595        for i in 0..3u64 {
2596            enc.encode_cpu_argb(&f, 1920 * 4, i, 25, i == 0).expect("encode");
2597        }
2598        println!("VRAM delta for one 1080p session: {} MiB", used_mb() - before);
2599    }
2600
2601    /// On a real GPU, a session that starts taller than the default 2304 headroom (portrait
2602    /// 4K: 2160×4096, within NVENC's 4096 H.264 cap) takes its own size as the `maxEncode` ceiling
2603    /// and encodes at that resolution. Ignored by default.
2604    /// The VUI has to describe what the session actually emits. NVENC converts its ARGB
2605    /// input with a fixed BT.601 matrix at limited range in both chroma formats, so the
2606    /// matrix and range follow the hardware; the primaries and transfer follow the source,
2607    /// which is sRGB desktop pixels and therefore BT.709. A client that inverts the wrong
2608    /// matrix, or expands a limited-range frame as full-range, shifts colour visibly.
2609    #[test]
2610    #[ignore]
2611    fn gpu_vui_describes_the_hardware_csc() {
2612        for fullcolor in [false, true] {
2613            let mut s = settings(1280, 720, 60.0);
2614            s.video_fullcolor = fullcolor;
2615            let enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init");
2616            // encodeCodecConfig is a union; a successful init leaves the H.264 arm live.
2617            let h264 = unsafe { &enc.encode_config.encodeCodecConfig.h264Config };
2618            let vui = &h264.h264VUIParameters;
2619            assert_eq!(vui.colourMatrix as u32,
2620                NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M as u32);
2621            assert_eq!(vui.colourPrimaries as u32,
2622                NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709 as u32);
2623            assert_eq!(vui.transferCharacteristics as u32,
2624                NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709 as u32);
2625            assert_eq!(vui.videoFullRangeFlag, 0, "fullcolor={fullcolor}");
2626            assert_eq!(h264.chromaFormatIDC, if fullcolor { 3 } else { 1 });
2627        }
2628    }
2629
2630    #[test]
2631    #[ignore]
2632    fn gpu_init_above_default_headroom() {
2633        let s = settings(2160, 4096, 30.0);
2634        let mut enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init portrait 4K");
2635        assert_eq!(enc.init_params.maxEncodeWidth, 4096);
2636        assert_eq!(enc.init_params.maxEncodeHeight, 4096);
2637        let f = frame(2160, 4096, 20);
2638        let pkt = enc
2639            .encode_cpu_argb(&f, 2160 * 4, 0, 25, true)
2640            .expect("encode portrait 4K");
2641        assert_eq!(wire_dims(&pkt), (2160, 4096));
2642    }
2643
2644    /// Thread CPU time, for the per-frame CPU cost of an encode path independent of how long
2645    /// the thread waited on the GPU.
2646    fn thread_cpu() -> std::time::Duration {
2647        let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
2648        unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) };
2649        std::time::Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
2650    }
2651
2652    /// Test helper: run `f` `n` times and report wall and thread-CPU microseconds per call.
2653    fn per_frame(label: &str, n: usize, mut f: impl FnMut(usize)) -> (f64, f64) {
2654        let t0 = std::time::Instant::now();
2655        let c0 = thread_cpu();
2656        for i in 0..n {
2657            f(i);
2658        }
2659        let wall = t0.elapsed().as_secs_f64() * 1e6 / n as f64;
2660        let cpu = (thread_cpu() - c0).as_secs_f64() * 1e6 / n as f64;
2661        println!("{label}: {wall:.0} us wall/frame, {cpu:.0} us cpu/frame ({n} frames)");
2662        (wall, cpu)
2663    }
2664
2665    /// Test helper: the raw GBM device and GLES renderer of the render node named by
2666    /// `PIXELFLUX_TEST_RENDER_NODE` (default `/dev/dri/renderD128`), brought up exactly as the
2667    /// compositor brings them up.
2668    fn gpu_render() -> (gbm::Device<std::fs::File>, smithay::backend::renderer::gles::GlesRenderer) {
2669        let node = std::env::var("PIXELFLUX_TEST_RENDER_NODE")
2670            .unwrap_or_else(|_| "/dev/dri/renderD128".to_string());
2671        crate::gpu_render_init(std::path::Path::new(&node)).expect("GPU render init")
2672    }
2673
2674    /// Background and block colours painted into test dmabufs, as `Color32F` components.
2675    const BG: [f32; 3] = [0.1, 0.2, 0.8];
2676    const FG: [f32; 3] = [0.9, 0.3, 0.1];
2677
2678    /// BT.601 limited-range Y/Cb/Cr of an RGB triple in 0..1 — what NVENC's hardware CSC emits.
2679    fn ycbcr_601(rgb: [f32; 3]) -> [f64; 3] {
2680        let [r, g, b] = rgb.map(|c| c as f64);
2681        [
2682            16.0 + 219.0 * (0.299 * r + 0.587 * g + 0.114 * b),
2683            128.0 + 224.0 * (-0.168736 * r - 0.331264 * g + 0.5 * b),
2684            128.0 + 224.0 * (0.5 * r - 0.418688 * g - 0.081312 * b),
2685        ]
2686    }
2687
2688    /// Where the foreground block of a `seed`-painted `w×h` frame sits: a quarter-size block
2689    /// whose origin moves with the seed.
2690    fn block_rect(w: u32, h: u32, seed: u32) -> (i32, i32, i32, i32) {
2691        let x = ((seed * 37) % (w / 2)) as i32 & !1;
2692        let y = ((seed * 53) % (h / 2)) as i32 & !1;
2693        (x, y, (w / 4) as i32 & !1, (h / 4) as i32 & !1)
2694    }
2695
2696    /// Test helper: allocate a `w×h` ARGB8888 render-target dmabuf on `gbm` and paint it with
2697    /// the GLES renderer — `BG` everywhere and an `FG` block at `block_rect(seed)` — waiting for
2698    /// the render to land before returning, as the compositor does before encoding.
2699    fn painted_dmabuf(
2700        gbm: &gbm::Device<std::fs::File>,
2701        renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
2702        w: u32,
2703        h: u32,
2704        seed: u32,
2705    ) -> (gbm::BufferObject<()>, Dmabuf) {
2706        use gbm::{BufferObjectFlags, Format as GbmFormat};
2707        use smithay::backend::renderer::{Bind, Color32F, Frame, Renderer};
2708        use smithay::utils::{Physical, Rectangle, Size, Transform};
2709        let bo = gbm
2710            .create_buffer_object::<()>(w, h, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
2711            .expect("GBM buffer");
2712        let mut dmabuf = crate::create_dmabuf_from_bo(&bo);
2713        {
2714            let mut fb = renderer.bind(&mut dmabuf).expect("bind dmabuf");
2715            let size: Size<i32, Physical> = (w as i32, h as i32).into();
2716            let mut frame = renderer.render(&mut fb, size, Transform::Normal).expect("render");
2717            let full: Rectangle<i32, Physical> = Rectangle::from_size(size);
2718            frame.clear(Color32F::new(BG[0], BG[1], BG[2], 1.0), &[full]).expect("clear");
2719            let (x, y, bw, bh) = block_rect(w, h, seed);
2720            let block: Rectangle<i32, Physical> = Rectangle::new((x, y).into(), (bw, bh).into());
2721            frame
2722                .draw_solid(
2723                    block,
2724                    &[Rectangle::from_size(block.size)],
2725                    Color32F::new(FG[0], FG[1], FG[2], 1.0),
2726                )
2727                .expect("draw block");
2728            let sync = frame.finish().expect("finish");
2729            let _ = sync.wait();
2730        }
2731        (bo, dmabuf)
2732    }
2733
2734    /// Test helper: decode one H.264 access unit (the bytes behind the 10-byte wire header)
2735    /// with the crate's avcodec decoder and return the mean Y/Cb/Cr inside `rect` and outside it.
2736    fn decoded_means(
2737        dec: &mut crate::webcam::decode::AvDecoder,
2738        pkt: &[u8],
2739        rect: (i32, i32, i32, i32),
2740    ) -> ([f64; 3], [f64; 3]) {
2741        use crate::webcam::decode::Decoder;
2742        assert!(dec.decode(&pkt[10..]).expect("decode"), "no picture from this access unit");
2743        let v = dec.frame().expect("decoded frame");
2744        let (rx, ry, rw, rh) = rect;
2745        let inside = |x: usize, y: usize| {
2746            x as i32 >= rx && (x as i32) < rx + rw && y as i32 >= ry && (y as i32) < ry + rh
2747        };
2748        let mut acc = [[0f64; 3]; 2];
2749        let mut cnt = [0f64; 2];
2750        for y in 0..v.height {
2751            for x in 0..v.width {
2752                let k = if inside(x, y) { 0 } else { 1 };
2753                acc[k][0] += v.y[y * v.y_stride + x] as f64;
2754                acc[k][1] += v.u[(y / 2) * v.uv_stride + x / 2] as f64;
2755                acc[k][2] += v.v[(y / 2) * v.uv_stride + x / 2] as f64;
2756                cnt[k] += 1.0;
2757            }
2758        }
2759        let mean = |k: usize| [acc[k][0] / cnt[k], acc[k][1] / cnt[k], acc[k][2] / cnt[k]];
2760        (mean(0), mean(1))
2761    }
2762
2763    /// Assert decoded region means sit within `tol` of the BT.601 limited-range values of the
2764    /// painted colours — a wrong pitch, byte order or stale buffer lands far outside this.
2765    fn assert_painted(label: &str, block: [f64; 3], bg: [f64; 3], tol: f64) {
2766        let (eb, eg) = (ycbcr_601(FG), ycbcr_601(BG));
2767        for i in 0..3 {
2768            assert!(
2769                (block[i] - eb[i]).abs() <= tol,
2770                "{label}: block plane {i} = {:.1}, expected {:.1}",
2771                block[i],
2772                eb[i]
2773            );
2774            assert!(
2775                (bg[i] - eg[i]).abs() <= tol,
2776                "{label}: background plane {i} = {:.1}, expected {:.1}",
2777                bg[i],
2778                eg[i]
2779            );
2780        }
2781    }
2782
2783    /// Whether every cached dmabuf import of `enc` is registered with NVENC in place.
2784    fn all_direct(enc: &NvencEncoder) -> bool {
2785        !enc.dmabuf_cache.is_empty()
2786            && enc.dmabuf_cache.values().all(|c| matches!(c.input, DmaBufInput::Direct { .. }))
2787    }
2788
2789    /// How the driver mapped the cached dmabuf imports of `enc`, for the test output.
2790    fn mapped_kind(enc: &NvencEncoder) -> &'static str {
2791        match enc.dmabuf_cache.values().next().map(|c| c.egl_frame.frame_type) {
2792            Some(CU_EGL_FRAME_TYPE_PITCH) => "pitch-linear",
2793            Some(CU_EGL_FRAME_TYPE_ARRAY) => "as a CUDA array",
2794            _ => "in an unknown frame kind",
2795        }
2796    }
2797
2798    /// On a real GPU with a render node: two GLES-painted dmabufs encode through the dmabuf path
2799    /// and decode to the painted colours at the painted positions, first with the direct
2800    /// registration enabled (in place when the driver maps the import pitch-linear, otherwise the
2801    /// copy arm) and then with it disabled — the two streams must agree, and the decoded content
2802    /// of both must match the paint. Prints which path the driver gave. Ignored by default; needs
2803    /// `PIXELFLUX_TEST_RENDER_NODE` or `/dev/dri/renderD128` backed by the NVIDIA GPU.
2804    #[test]
2805    #[ignore]
2806    fn gpu_dmabuf_direct_and_copy_paths_decode_to_the_paint() {
2807        use crate::webcam::decode::{AvDecoder, Codec};
2808        let (w, h) = (1920u32, 1080u32);
2809        let s = settings(w as i32, h as i32, 60.0);
2810        let (gbm, mut renderer) = gpu_render();
2811        let egl_display = renderer.egl_context().display().get_display_handle().handle;
2812        let bufs: Vec<_> = (1..=2u32).map(|seed| (seed, painted_dmabuf(&gbm, &mut renderer, w, h, seed))).collect();
2813        let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
2814
2815        let run = |enc: &mut NvencEncoder, label: &str| -> Vec<Vec<u8>> {
2816            let mut dec = AvDecoder::new(Codec::H264).expect("avcodec h264");
2817            let mut out = Vec::new();
2818            for i in 0..6u64 {
2819                let (seed, (_, dmabuf)) = &bufs[(i % 2) as usize];
2820                let pkt = enc.encode(dmabuf, i, 25, i == 0).expect("dmabuf encode");
2821                assert_eq!(wire_dims(&pkt), (w as u16, h as u16));
2822                let (block, bg) = decoded_means(&mut dec, &pkt, block_rect(w, h, *seed));
2823                assert_painted(&format!("{label} frame {i}"), block, bg, 6.0);
2824                out.push(pkt[10..].to_vec());
2825            }
2826            out
2827        };
2828
2829        let direct = run(&mut enc, "direct");
2830        println!(
2831            "driver mapped the dmabuf {}: {}",
2832            mapped_kind(&enc),
2833            if all_direct(&enc) { "registered in place" } else { "direct registration unavailable, copy arm used" }
2834        );
2835
2836        enc.direct_dmabuf = false;
2837        enc.reconfigure_resolution(&s).expect("same-size reconfigure drains the import cache");
2838        let copied = run(&mut enc, "copy");
2839        assert!(!all_direct(&enc));
2840        let identical = direct.iter().zip(&copied).all(|(a, b)| a == b);
2841        println!(
2842            "direct vs copy streams: {} ({} vs {} bytes)",
2843            if identical { "byte-identical" } else { "differ" },
2844            direct.iter().map(Vec::len).sum::<usize>(),
2845            copied.iter().map(Vec::len).sum::<usize>()
2846        );
2847        if let Ok(dir) = std::env::var("NVENC_TEST_DUMP_DIR") {
2848            std::fs::write(format!("{dir}/dmabuf-direct.h264"), direct.concat()).unwrap();
2849            std::fs::write(format!("{dir}/dmabuf-copy.h264"), copied.concat()).unwrap();
2850        }
2851    }
2852
2853    /// On a real GPU: a host frame handed over as BGRA (`rgba_input = false`) and the same image
2854    /// handed over as RGBA bytes (`rgba_input = true`) both decode to the painted colours — the
2855    /// input surface is re-registered in the other byte order in place — and the session keeps
2856    /// encoding across the switch. Ignored by default.
2857    #[test]
2858    #[ignore]
2859    fn gpu_packed_bgra_and_rgba_inputs_agree() {
2860        use crate::webcam::decode::{AvDecoder, Codec};
2861        let (w, h) = (1280u32, 720u32);
2862        let s = settings(w as i32, h as i32, 60.0);
2863        let rect = block_rect(w, h, 3);
2864        let paint = |rgba: bool| -> Vec<u8> {
2865            let to_u8 = |c: f32| (c * 255.0).round() as u8;
2866            let mut f = vec![0u8; (w * h * 4) as usize];
2867            for y in 0..h as i32 {
2868                for x in 0..w as i32 {
2869                    let inside = x >= rect.0 && x < rect.0 + rect.2 && y >= rect.1 && y < rect.1 + rect.3;
2870                    let c = if inside { FG } else { BG };
2871                    let px = &mut f[((y as u32 * w + x as u32) * 4) as usize..][..4];
2872                    let (r, g, b) = (to_u8(c[0]), to_u8(c[1]), to_u8(c[2]));
2873                    if rgba {
2874                        px.copy_from_slice(&[r, g, b, 255]);
2875                    } else {
2876                        px.copy_from_slice(&[b, g, r, 255]);
2877                    }
2878                }
2879            }
2880            f
2881        };
2882        let bgra = paint(false);
2883        let rgba = paint(true);
2884        let mut enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init");
2885        let mut dec = AvDecoder::new(Codec::H264).expect("avcodec h264");
2886        let stride = (w * 4) as usize;
2887        for (i, (buf, is_rgba)) in [(&bgra, false), (&rgba, true), (&bgra, false), (&rgba, true)]
2888            .into_iter()
2889            .enumerate()
2890        {
2891            let pkt = enc
2892                .encode_cpu_packed(buf, stride, is_rgba, i as u64, 25, i == 0)
2893                .expect("packed encode");
2894            assert_eq!(
2895                enc.input_format,
2896                if is_rgba {
2897                    NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
2898                } else {
2899                    NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
2900                }
2901            );
2902            let (block, bg) = decoded_means(&mut dec, &pkt, rect);
2903            assert_painted(&format!("frame {i} rgba={is_rgba}"), block, bg, 6.0);
2904        }
2905    }
2906
2907    /// On a real GPU with a render node: per-frame wall and CPU cost of the dmabuf path with the
2908    /// in-place registration (when the driver maps the import pitch-linear) against the per-frame
2909    /// copy, 1080p, two painted buffers alternating. Prints both; ignored by default.
2910    #[test]
2911    #[ignore]
2912    fn gpu_bench_dmabuf_paths() {
2913        let (w, h) = (1920u32, 1080u32);
2914        let n: usize = std::env::var("NVENC_BENCH_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(300);
2915        let s = settings(w as i32, h as i32, 60.0);
2916        let (gbm, mut renderer) = gpu_render();
2917        let egl_display = renderer.egl_context().display().get_display_handle().handle;
2918        let bufs: Vec<_> = (1..=2u32).map(|seed| painted_dmabuf(&gbm, &mut renderer, w, h, seed).1).collect();
2919        let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
2920        for pass in 0..2 {
2921            enc.direct_dmabuf = pass == 0;
2922            enc.reconfigure_resolution(&s).expect("reconfigure drains the import cache");
2923            enc.encode(&bufs[0], 0, 25, true).expect("warm-up");
2924            enc.encode(&bufs[1], 1, 25, false).expect("warm-up");
2925            let label = if all_direct(&enc) {
2926                format!("dmabuf registered in place ({})", mapped_kind(&enc))
2927            } else if pass == 0 {
2928                format!("dmabuf copy arm (direct registration unavailable, mapped {})", mapped_kind(&enc))
2929            } else {
2930                format!("dmabuf per-frame copy (mapped {})", mapped_kind(&enc))
2931            };
2932            per_frame(&label, n, |i| {
2933                enc.encode(&bufs[i % 2], 2 + i as u64, 25, false).expect("encode");
2934            });
2935        }
2936    }
2937
2938    /// On a real GPU: per-frame wall and CPU cost of the readback→NVENC hand-over, 1080p host
2939    /// frames: the CPU NV12 conversion plus planar upload the readback path used to run, against
2940    /// the packed upload with the hardware CSC (BGRA and RGBA), and that same packed upload as a
2941    /// synchronous copy. Prints all; ignored by default.
2942    #[test]
2943    #[ignore]
2944    fn gpu_bench_readback_upload() {
2945        use yuv::{BufferStoreMut, YuvBiPlanarImageMut, YuvConversionMode, YuvRange, YuvStandardMatrix};
2946        let (w, h) = (1920u32, 1080u32);
2947        let n: usize = std::env::var("NVENC_BENCH_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(300);
2948        let s = settings(w as i32, h as i32, 60.0);
2949        let frames: Vec<Vec<u8>> = (0..4u8).map(|k| frame(w as usize, h as usize, 10 + 40 * k)).collect();
2950        let stride = (w * 4) as usize;
2951        let mut enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init");
2952
2953        let mut nv12 = vec![0u8; (w * h * 3 / 2) as usize];
2954        enc.encode_raw(&nv12, 0, 25, true).expect("warm-up");
2955        per_frame("CPU NV12 conversion + encode_raw (former readback path)", n, |i| {
2956            let (y, uv) = nv12.split_at_mut((w * h) as usize);
2957            let mut planar = YuvBiPlanarImageMut {
2958                y_plane: BufferStoreMut::Borrowed(y),
2959                y_stride: w,
2960                uv_plane: BufferStoreMut::Borrowed(uv),
2961                uv_stride: w,
2962                width: w,
2963                height: h,
2964            };
2965            yuv::bgra_to_yuv_nv12(&mut planar, &frames[i % 4], w * 4, YuvRange::Limited, YuvStandardMatrix::Bt601, YuvConversionMode::Fast)
2966                .expect("csc");
2967            enc.encode_raw(&nv12, 1 + i as u64, 25, false).expect("encode_raw");
2968        });
2969
2970        enc.reconfigure_resolution(&s).expect("reconfigure");
2971        enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true).expect("warm-up");
2972        per_frame("encode_cpu_packed BGRA (pinned, async upload, hardware CSC)", n, |i| {
2973            enc.encode_cpu_packed(&frames[i % 4], stride, false, 1 + i as u64, 25, false).expect("packed");
2974        });
2975
2976        enc.reconfigure_resolution(&s).expect("reconfigure");
2977        enc.encode_cpu_packed(&frames[0], stride, true, 0, 25, true).expect("warm-up");
2978        per_frame("encode_cpu_packed RGBA (pinned, async upload, hardware CSC)", n, |i| {
2979            enc.encode_cpu_packed(&frames[i % 4], stride, true, 1 + i as u64, 25, false).expect("packed");
2980        });
2981
2982        enc.reconfigure_resolution(&s).expect("reconfigure");
2983        enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true).expect("warm-up");
2984        per_frame("packed BGRA with a synchronous cuMemcpy2D upload", n, |i| unsafe {
2985            let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
2986            let src = &frames[i % 4];
2987            enc.pin_host_source(src.as_ptr() as usize, src.len());
2988            let copy = CUDA_MEMCPY2D {
2989                srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2990                srcHost: src.as_ptr() as *const c_void,
2991                srcPitch: stride,
2992                dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2993                dstDevice: enc.input_device_ptr,
2994                dstPitch: enc.input_pitch,
2995                WidthInBytes: stride,
2996                Height: h as usize,
2997                ..Default::default()
2998            };
2999            assert_eq!((enc.cuda.cuMemcpy2D_v2)(&copy), CUresult::CUDA_SUCCESS);
3000            enc.submit_frame(enc.mapped_input_buffer, enc.input_format, 1 + i as u64, false)
3001                .expect("submit");
3002            (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3003        });
3004    }
3005}
3006
3007#[cfg(test)]
3008mod version_tests {
3009    use super::*;
3010
3011    /// Every version-tagged struct, in one fixed order, paired with its pinned compile-time
3012    /// `NV_ENC_*_VER` constant — the reference both version tests iterate.
3013    const ALL: [(NvStruct, u32); 13] = [
3014        (NvStruct::FunctionList, NV_ENCODE_API_FUNCTION_LIST_VER),
3015        (NvStruct::OpenSessionExParams, NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER),
3016        (NvStruct::Config, NV_ENC_CONFIG_VER),
3017        (NvStruct::RcParams, NV_ENC_RC_PARAMS_VER),
3018        (NvStruct::PresetConfig, NV_ENC_PRESET_CONFIG_VER),
3019        (NvStruct::InitializeParams, NV_ENC_INITIALIZE_PARAMS_VER),
3020        (NvStruct::ReconfigureParams, NV_ENC_RECONFIGURE_PARAMS_VER),
3021        (NvStruct::RegisterResource, NV_ENC_REGISTER_RESOURCE_VER),
3022        (NvStruct::MapInputResource, NV_ENC_MAP_INPUT_RESOURCE_VER),
3023        (NvStruct::CreateBitstreamBuffer, NV_ENC_CREATE_BITSTREAM_BUFFER_VER),
3024        (NvStruct::PicParams, NV_ENC_PIC_PARAMS_VER),
3025        (NvStruct::LockBitstream, NV_ENC_LOCK_BITSTREAM_VER),
3026        (NvStruct::CapsParam, NV_ENC_CAPS_PARAM_VER),
3027    ];
3028
3029    /// For the pinned nvcodec-sys version, `nvenc_struct_ver` must reproduce every
3030    /// compile-time `NV_ENC_*_VER` constant exactly — guaranteeing a current driver is stamped
3031    /// byte-for-byte identically — and the packed major/minor must round-trip `NVENCAPI_VERSION`.
3032    /// Fails loudly if the bundled header is bumped without extending the revision table.
3033    #[test]
3034    fn table_is_identity_for_pinned_version() {
3035        let maj = NVENCAPI_VERSION & 0xFF;
3036        let min = (NVENCAPI_VERSION >> 24) & 0xFF;
3037        for (s, base) in ALL {
3038            assert_eq!(nvenc_struct_ver(s, maj, min), base, "{:?}", s);
3039        }
3040        assert_eq!(maj | (min << 24), NVENCAPI_VERSION);
3041    }
3042
3043    /// `nvenc_struct_ver` must reproduce the exact `NV_ENC_*_VER` words each SDK defined, for
3044    /// every negotiable version 10.0 through 13.0.
3045    ///
3046    /// The expected words are hardcoded from `nvEncodeAPI.h` at the FFmpeg nv-codec-headers tags
3047    /// listed in `NvStruct::rev`, one row per SDK version in `ALL` order. (The n10.0.26.2 header
3048    /// spells the flag `1<<31` rather than `1u<<31`; the bit is the same.) This is what lets the
3049    /// 13.0-layout structs be stamped with an older SDK's word when the session down-negotiates.
3050    #[test]
3051    fn table_matches_historical_headers() {
3052        #[rustfmt::skip]
3053        let expected: [(u32, u32, [u32; 12]); 7] = [
3054            (10, 0, [0x7002000A, 0x7001000A, 0xF007000A, 0x7001000A, 0xF004000A, 0xF005000A, 0xF001000A,
3055                     0x7003000A, 0x7004000A, 0x7001000A, 0xF004000A, 0x7001000A]),
3056            (11, 0, [0x7002000B, 0x7001000B, 0xF007000B, 0x7001000B, 0xF004000B, 0xF005000B, 0xF001000B,
3057                     0x7003000B, 0x7004000B, 0x7001000B, 0xF004000B, 0x7001000B]),
3058            (11, 1, [0x7102000B, 0x7101000B, 0xF107000B, 0x7101000B, 0xF104000B, 0xF105000B, 0xF101000B,
3059                     0x7103000B, 0x7104000B, 0x7101000B, 0xF104000B, 0x7101000B]),
3060            (12, 0, [0x7002000C, 0x7001000C, 0xF008000C, 0x7001000C, 0xF004000C, 0xF005000C, 0xF001000C,
3061                     0x7004000C, 0x7004000C, 0x7001000C, 0xF006000C, 0x7002000C]),
3062            (12, 1, [0x7102000C, 0x7101000C, 0xF108000C, 0x7101000C, 0xF104000C, 0xF106000C, 0xF101000C,
3063                     0x7104000C, 0x7104000C, 0x7101000C, 0xF106000C, 0xF101000C]),
3064            (12, 2, [0x7202000C, 0x7201000C, 0xF209000C, 0x7201000C, 0xF205000C, 0xF207000C, 0xF202000C,
3065                     0x7205000C, 0x7204000C, 0x7201000C, 0xF207000C, 0xF202000C]),
3066            (13, 0, [0x7002000D, 0x7001000D, 0xF009000D, 0x7001000D, 0xF005000D, 0xF007000D, 0xF002000D,
3067                     0x7005000D, 0x7004000D, 0x7001000D, 0xF007000D, 0xF002000D]),
3068        ];
3069        for (maj, min, words) in expected {
3070            for ((s, _), want) in ALL.iter().zip(words) {
3071                assert_eq!(
3072                    nvenc_struct_ver(*s, maj, min),
3073                    want,
3074                    "{:?} at {}.{}",
3075                    s,
3076                    maj,
3077                    min
3078                );
3079            }
3080        }
3081    }
3082}
3083
3084#[cfg(test)]
3085mod decision_tests {
3086    use super::*;
3087
3088    /// A caps query that returns `Some(0)` means the GPU lacks 4:4:4, so a 4:4:4 request is met
3089    /// with 4:2:0 and flagged as a downgrade; `Some(1)` keeps 4:4:4; an unqueryable cap (`None`)
3090    /// leaves the request untouched rather than downgrading on missing information.
3091    #[test]
3092    fn caps_chroma_downgrade() {
3093        let d = decide_caps(true, 1920, 1080, Some(0), None, None);
3094        assert!(!d.fullcolor && d.downgraded_color && d.too_large.is_none());
3095
3096        let d = decide_caps(true, 1920, 1080, Some(1), None, None);
3097        assert!(d.fullcolor && !d.downgraded_color);
3098
3099        let d = decide_caps(true, 1920, 1080, None, None, None);
3100        assert!(d.fullcolor && !d.downgraded_color);
3101
3102        // A 4:2:0 request is never a downgrade whatever the cap says.
3103        let d = decide_caps(false, 1920, 1080, Some(0), None, None);
3104        assert!(!d.fullcolor && !d.downgraded_color);
3105    }
3106
3107    /// A capture beyond the driver's reported maximum dimensions is flagged `too_large` (the
3108    /// caller then declines NVENC and uses software); a capture within them, or one whose caps are
3109    /// unknown, is not.
3110    #[test]
3111    fn caps_dimension_gate() {
3112        assert_eq!(
3113            decide_caps(false, 5120, 2160, None, Some(4096), Some(4096)).too_large,
3114            Some((4096, 4096))
3115        );
3116        assert_eq!(
3117            decide_caps(false, 3840, 4320, None, Some(4096), Some(4096)).too_large,
3118            Some((4096, 4096))
3119        );
3120        assert!(decide_caps(false, 3840, 2160, None, Some(4096), Some(4096)).too_large.is_none());
3121        assert!(decide_caps(false, 7680, 4320, None, None, None).too_large.is_none());
3122        // A zero cap is treated as unknown, not as "everything is too large".
3123        assert!(decide_caps(false, 3840, 2160, None, Some(0), Some(0)).too_large.is_none());
3124    }
3125
3126    /// The resize headroom lifts the request to the 5.2-ceiling floor but never past the driver
3127    /// maximum, so initializing with headroom cannot itself exceed what the GPU supports.
3128    #[test]
3129    fn headroom_is_floored_and_capped() {
3130        assert_eq!(nvenc_headroom(1920, 4096, Some(8192)), 4096);
3131        assert_eq!(nvenc_headroom(3840, 4096, Some(4096)), 4096);
3132        assert_eq!(nvenc_headroom(6000, 4096, Some(8192)), 6000);
3133        assert_eq!(nvenc_headroom(1920, 4096, None), 4096);
3134        assert_eq!(nvenc_headroom(1920, 4096, Some(2048)), 2048);
3135    }
3136
3137    /// Two buffers that reuse one fd number but differ in any identity field are distinct, so a
3138    /// cache keyed by fd cannot return a stale import: a new inode (the kernel reissues one per
3139    /// dma-buf since 5.3), a new size, or new geometry each breaks the match.
3140    #[test]
3141    fn dmabuf_identity_distinguishes_recycled_fd() {
3142        let base = DmaBufIdentity { dev: 1, ino: 10, size: 100, modifier: 0, width: 1920, height: 1080 };
3143        assert_eq!(base, base);
3144        let mut new_ino = base;
3145        new_ino.ino = 11;
3146        assert_ne!(base, new_ino);
3147        let mut new_size = base;
3148        new_size.size = 200;
3149        assert_ne!(base, new_size);
3150        let mut new_mod = base;
3151        new_mod.modifier = 1;
3152        assert_ne!(base, new_mod);
3153        let mut new_geom = base;
3154        new_geom.width = 1280;
3155        assert_ne!(base, new_geom);
3156    }
3157
3158    /// `probe` reads a real fd deterministically and folds the modifier and geometry into the
3159    /// identity: the same fd and parameters yield equal identities, and changing the modifier or the
3160    /// geometry changes the identity even on the same fd.
3161    #[test]
3162    fn dmabuf_identity_probe_is_stable_and_parameterized() {
3163        use std::os::fd::AsRawFd;
3164        let f = std::fs::File::open("/dev/null").expect("open /dev/null");
3165        let fd = f.as_raw_fd();
3166        let a = DmaBufIdentity::probe(fd, 0x1234, 1920, 1080);
3167        assert_eq!(a, DmaBufIdentity::probe(fd, 0x1234, 1920, 1080));
3168        assert_ne!(a, DmaBufIdentity::probe(fd, 0x9999, 1920, 1080));
3169        assert_ne!(a, DmaBufIdentity::probe(fd, 0x1234, 1280, 720));
3170    }
3171
3172    /// Test helper: a mapped `CUeglFrame` of one plane with the given kind, geometry and pitch,
3173    /// its first plane at `plane` (a device pointer for the pitch kind, an array handle for the
3174    /// array kind) in four 8-bit channels.
3175    fn egl_frame(frame_type: u32, w: u32, h: u32, pitch: u32, plane: usize) -> CUeglFrame {
3176        let mut f: CUeglFrame = unsafe { std::mem::zeroed() };
3177        f.frame_type = frame_type;
3178        f.plane_count = 1;
3179        f.width = w;
3180        f.height = h;
3181        f.pitch = pitch;
3182        f.num_channels = 4;
3183        f.cu_format = CU_AD_FORMAT_U8;
3184        f.frame.p_pitch = [plane as *mut c_void, ptr::null_mut(), ptr::null_mut()];
3185        f
3186    }
3187
3188    /// A pitch-linear mapping whose first plane covers the session geometry at a 4-byte-aligned
3189    /// pitch of at least `width * 4` is registered with NVENC in place as a device pointer at its
3190    /// own pitch; a null plane, a short or unaligned pitch, a frame smaller than the session, or a
3191    /// frame without planes each take the per-frame copy.
3192    #[test]
3193    fn pitch_linear_frame_direct_registration_rules() {
3194        let pitch_ok = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7680, 0x1000);
3195        assert_eq!(direct_plane(&pitch_ok, 1920, 1080), Some(DirectPlane::Pitch(7680)));
3196        let padded = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 8192, 0x1000);
3197        assert_eq!(direct_plane(&padded, 1920, 1080), Some(DirectPlane::Pitch(8192)));
3198        let larger = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 2048, 1200, 8192, 0x1000);
3199        assert_eq!(direct_plane(&larger, 1920, 1080), Some(DirectPlane::Pitch(8192)));
3200
3201        let null_plane = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7680, 0);
3202        assert_eq!(direct_plane(&null_plane, 1920, 1080), None);
3203        let short_pitch = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7676, 0x1000);
3204        assert_eq!(direct_plane(&short_pitch, 1920, 1080), None);
3205        let unaligned = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7682, 0x1000);
3206        assert_eq!(direct_plane(&unaligned, 1920, 1080), None);
3207        let smaller = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1280, 720, 7680, 0x1000);
3208        assert_eq!(direct_plane(&smaller, 1920, 1080), None);
3209        let mut no_planes = pitch_ok;
3210        no_planes.plane_count = 0;
3211        assert_eq!(direct_plane(&no_planes, 1920, 1080), None);
3212        assert_eq!(direct_plane(&pitch_ok, 0, 1080), None);
3213        let mut unknown_kind = pitch_ok;
3214        unknown_kind.frame_type = 7;
3215        assert_eq!(direct_plane(&unknown_kind, 1920, 1080), None);
3216    }
3217
3218    /// A CUDA-array mapping of four 8-bit channels covering the session geometry is registered in
3219    /// place as a CUDA array whose pitch word is the array's row width in bytes; an array of any
3220    /// other element layout, a null array, or one smaller than the session takes the copy.
3221    #[test]
3222    fn cuda_array_frame_direct_registration_rules() {
3223        let array = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1920, 1080, 0, 0x2000);
3224        assert_eq!(direct_plane(&array, 1920, 1080), Some(DirectPlane::Array(7680)));
3225        let wider = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 2048, 1080, 0, 0x2000);
3226        assert_eq!(direct_plane(&wider, 1920, 1080), Some(DirectPlane::Array(8192)));
3227
3228        let null_array = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1920, 1080, 0, 0);
3229        assert_eq!(direct_plane(&null_array, 1920, 1080), None);
3230        let mut one_channel = array;
3231        one_channel.num_channels = 1;
3232        assert_eq!(direct_plane(&one_channel, 1920, 1080), None);
3233        let mut wide_elements = array;
3234        wide_elements.cu_format = 3;
3235        assert_eq!(direct_plane(&wide_elements, 1920, 1080), None);
3236        let smaller = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1280, 720, 0, 0x2000);
3237        assert_eq!(direct_plane(&smaller, 1920, 1080), None);
3238    }
3239
3240    /// The dmabuf fourcc picks the NVENC packed format with the same byte order: XR24 / AR24 are
3241    /// B,G,R,A in memory and so NVENC `ARGB`; XB24 / AB24 are R,G,B,A and so `ABGR`; anything else
3242    /// has no packed 8-bit NVENC equivalent.
3243    #[test]
3244    fn fourcc_selects_nvenc_byte_order() {
3245        for code in [Fourcc::Argb8888, Fourcc::Xrgb8888] {
3246            assert_eq!(fourcc_nvenc_format(code), Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB));
3247        }
3248        for code in [Fourcc::Abgr8888, Fourcc::Xbgr8888] {
3249            assert_eq!(fourcc_nvenc_format(code), Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR));
3250        }
3251        for code in [Fourcc::Rgb565, Fourcc::Nv12, Fourcc::Argb2101010, Fourcc::Bgra8888, Fourcc::Rgba8888] {
3252            assert_eq!(fourcc_nvenc_format(code), None, "{code:?}");
3253        }
3254    }
3255}