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 / HEVC / AV1 encoder: CUDA-bound sessions that encode packed BGRA or
8//! RGBA frames from the X11 host and Wayland readback paths, or Wayland dmabufs in place.
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 two 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), and a pinned host→device upload of packed BGRA / RGBA.
16//! Color is converted on the GPU either way. The codec is a session parameter: the same rate
17//! control, GOP, VUI, and latency posture is programmed into whichever of the three codec
18//! configurations the device offers, and a codec the device lacks (AV1 before Ada) is refused at
19//! open so the caller falls back. Sessions reconfigure resolution and rate control in place, so a
20//! resize or bitrate change costs a few milliseconds instead of a full rebuild.
21
22// The NVENC and CUDA entry points are called through function pointers
23// resolved at runtime, so the safety contract is carried by the function
24// signatures rather than by a block around each call.
25#![allow(unsafe_op_in_unsafe_fn)]
26#![allow(non_camel_case_types)]
27#![allow(non_snake_case)]
28
29use std::collections::HashMap;
30use std::ffi::{CStr, CString, c_char, c_void};
31use std::os::unix::io::AsRawFd;
32use std::ptr;
33use std::sync::Arc;
34use std::sync::atomic::{AtomicBool, Ordering};
35
36use libloading::{Library, Symbol};
37use smithay::backend::allocator::{Buffer, Fourcc, dmabuf::Dmabuf};
38
39use super::codec::{
40    Codec, FRAME_ANCHOR, FRAME_DELTA, FRAME_INTRA, FRAME_KEY, Hardware, VIDEO_HEADER_LEN,
41    av1_level, h264_dpb_frames, h264_level, h265_dpb_frames, h265_level, h265_tier,
42    push_video_header,
43};
44use super::frame_rate::FrameRate;
45use super::reference::{ANCHORS, Invalidation, Reference, ReferenceReport, ReferenceWindow};
46use super::sps::{WideFrameNum, h264_frame_num_range};
47use crate::RustCaptureSettings;
48use nvcodec_sys::cuda::*;
49use nvcodec_sys::*;
50
51/// Opaque CUDA module and kernel handles; the driver API's own types, which the committed
52/// bindings do not carry.
53type CUmodule = *mut c_void;
54type CUfunction = *mut c_void;
55
56/// The ARGB/ABGR → NV12 convert and the band hash, as PTX the driver JIT-compiles at session
57/// open. PTX is the portable form: `libcuda` compiles it for whatever GPU is present, so nothing
58/// beyond the driver NVENC already needs has to be installed, and `.version 3.1`/`.target sm_30`
59/// keeps every NVENC-capable GPU in range.
60const ARGB_TO_NV12_PTX: &[u8] = include_bytes!("argb_to_nv12.ptx");
61
62/// JIT the kernels' module into the current context; `None` where the driver refuses it.
63unsafe fn load_kernels(cuda: &CudaFunctions) -> Option<CUmodule> {
64    let mut ptx = ARGB_TO_NV12_PTX.to_vec();
65    ptx.push(0);
66    let mut module: CUmodule = ptr::null_mut();
67    ((cuda.cuModuleLoadData)(&mut module, ptx.as_ptr() as *const c_void) == CUresult::CUDA_SUCCESS)
68        .then_some(module)
69}
70
71/// EGL C-interop type aliases and the `EGL_*` attribute constants used to wrap a dmabuf as
72/// an `EGLImageKHR` for CUDA import.
73type EGLDisplay = *const c_void;
74type EGLImageKHR = *mut c_void;
75type EGLint = i32;
76type EGLenum = u32;
77type EGLBoolean = u32;
78
79const EGL_NO_IMAGE_KHR: EGLImageKHR = ptr::null_mut();
80const EGL_LINUX_DMA_BUF_EXT: u32 = 0x3270;
81const EGL_DMA_BUF_PLANE0_FD_EXT: EGLint = 0x3272;
82const EGL_DMA_BUF_PLANE0_OFFSET_EXT: EGLint = 0x3273;
83const EGL_DMA_BUF_PLANE0_PITCH_EXT: EGLint = 0x3274;
84const EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT: EGLint = 0x3443;
85const EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT: EGLint = 0x3444;
86const EGL_WIDTH: EGLint = 0x3057;
87const EGL_HEIGHT: EGLint = 0x3056;
88const EGL_LINUX_DRM_FOURCC_EXT: EGLint = 0x3271;
89const EGL_NONE: EGLint = 0x3038;
90
91/// Opaque CUDA graphics-resource handle for the EGL interop path — an `EGLImageKHR`
92/// registered with CUDA maps to one of these.
93type CUgraphicsResource = *mut c_void;
94
95/// `CUeglFrame::frame_type` values: the mapped planes are CUDA arrays, or pitch-linear device
96/// memory.
97const CU_EGL_FRAME_TYPE_ARRAY: u32 = 0;
98const CU_EGL_FRAME_TYPE_PITCH: u32 = 1;
99/// `CUeglFrame::cu_format` of an 8-bit-per-channel plane (`CU_AD_FORMAT_UNSIGNED_INT8`).
100const CU_AD_FORMAT_U8: u32 = 1;
101
102/// `CU_TRSF_READ_AS_INTEGER`, a `cuda.h` macro the bindings do not carry: a texture fetch
103/// returns the stored bytes rather than normalized floats.
104const CU_TRSF_READ_AS_INTEGER: u32 = 1;
105
106/// A CUDA frame mapped from an EGLImage: the `cuGraphicsResourceGetMappedEglFrame` result
107/// describing the imported dmabuf's plane pointers, geometry, pitch, and pixel format.
108#[repr(C)]
109#[derive(Clone, Copy)]
110struct CUeglFrame {
111    frame: CUeglFrameUnion,
112    width: u32,
113    height: u32,
114    depth: u32,
115    pitch: u32,
116    plane_count: u32,
117    num_channels: u32,
118    frame_type: u32,
119    egl_color_format: u32,
120    cu_format: u32,
121}
122
123/// The mapped frame's plane pointers, as either CUDA arrays or pitch-linear device
124/// pointers — `CUeglFrame::frame_type` selects which arm of the union is valid.
125#[repr(C)]
126#[derive(Clone, Copy)]
127union CUeglFrameUnion {
128    p_array: [CUarray; 3],
129    p_pitch: [*mut c_void; 3],
130}
131
132type EglCreateImageKhrFn = unsafe extern "C" fn(
133    dpy: EGLDisplay,
134    ctx: *mut c_void,
135    target: EGLenum,
136    buffer: *mut c_void,
137    attrib_list: *const EGLint,
138) -> EGLImageKHR;
139type EglDestroyImageKhrFn = unsafe extern "C" fn(dpy: EGLDisplay, image: EGLImageKHR) -> EGLBoolean;
140
141/// Dynamically loaded EGL entry points (from `libEGL`) for creating and destroying the
142/// `EGLImageKHR` that wraps a dmabuf. `_lib` keeps the library resident for the pointers' life.
143struct EglFunctions {
144    _lib: Library,
145    eglGetProcAddress: unsafe extern "C" fn(procname: *const c_char) -> *mut c_void,
146    eglCreateImageKHR: EglCreateImageKhrFn,
147    eglDestroyImageKHR: EglDestroyImageKhrFn,
148}
149
150/// Dynamically loaded CUDA driver-API entry points (from `libcuda`) for context, device,
151/// memory, host-pin, and EGL-interop calls. `_lib` keeps the library resident for the pointers' life.
152struct CudaFunctions {
153    _lib: Library,
154    cuInit: unsafe extern "C" fn(flags: u32) -> CUresult,
155    cuDeviceGet: unsafe extern "C" fn(device: *mut CUdevice, ordinal: i32) -> CUresult,
156    cuDeviceGetByPCIBusId:
157        unsafe extern "C" fn(dev: *mut CUdevice, pciBusId: *const c_char) -> CUresult,
158    cuDevicePrimaryCtxRetain: unsafe extern "C" fn(pctx: *mut CUcontext, dev: CUdevice) -> CUresult,
159    cuCtxPushCurrent_v2: unsafe extern "C" fn(ctx: CUcontext) -> CUresult,
160    cuCtxPopCurrent_v2: unsafe extern "C" fn(pctx: *mut CUcontext) -> CUresult,
161    cuDevicePrimaryCtxRelease_v2: unsafe extern "C" fn(dev: CUdevice) -> CUresult,
162    cuMemAlloc_v2: unsafe extern "C" fn(dptr: *mut CUdeviceptr, bytesize: usize) -> CUresult,
163    cuMemAllocPitch_v2: unsafe extern "C" fn(
164        dptr: *mut CUdeviceptr,
165        pPitch: *mut usize,
166        WidthInBytes: usize,
167        Height: usize,
168        ElementSizeBytes: u32,
169    ) -> CUresult,
170    cuMemFree_v2: unsafe extern "C" fn(dptr: CUdeviceptr) -> CUresult,
171    cuMemcpyHtoD_v2: unsafe extern "C" fn(
172        dstDevice: CUdeviceptr,
173        srcHost: *const c_void,
174        ByteCount: usize,
175    ) -> CUresult,
176    cuMemcpyDtoH_v2: unsafe extern "C" fn(
177        dstHost: *mut c_void,
178        srcDevice: CUdeviceptr,
179        ByteCount: usize,
180    ) -> CUresult,
181    cuMemcpy2D_v2: unsafe extern "C" fn(pCopy: *const CUDA_MEMCPY2D) -> CUresult,
182    cuMemcpy2DAsync_v2:
183        unsafe extern "C" fn(pCopy: *const CUDA_MEMCPY2D, hStream: CUstream) -> CUresult,
184    cuStreamSynchronize: unsafe extern "C" fn(hStream: CUstream) -> CUresult,
185    cuModuleLoadData: unsafe extern "C" fn(module: *mut CUmodule, image: *const c_void) -> CUresult,
186    cuModuleGetFunction: unsafe extern "C" fn(
187        hfunc: *mut CUfunction,
188        hmod: CUmodule,
189        name: *const c_char,
190    ) -> CUresult,
191    cuModuleUnload: unsafe extern "C" fn(hmod: CUmodule) -> CUresult,
192    cuTexObjectCreate: unsafe extern "C" fn(
193        tex: *mut CUtexObject,
194        res: *const CUDA_RESOURCE_DESC,
195        sampling: *const CUDA_TEXTURE_DESC,
196        view: *const c_void,
197    ) -> CUresult,
198    cuTexObjectDestroy: unsafe extern "C" fn(tex: CUtexObject) -> CUresult,
199    #[allow(clippy::type_complexity)]
200    cuLaunchKernel: unsafe extern "C" fn(
201        f: CUfunction,
202        gx: u32,
203        gy: u32,
204        gz: u32,
205        bx: u32,
206        by: u32,
207        bz: u32,
208        shared: u32,
209        stream: CUstream,
210        params: *mut *mut c_void,
211        extra: *mut *mut c_void,
212    ) -> CUresult,
213    cuMemHostRegister_v2:
214        unsafe extern "C" fn(p: *mut c_void, bytesize: usize, flags: u32) -> CUresult,
215    cuMemHostUnregister: unsafe extern "C" fn(p: *mut c_void) -> CUresult,
216    cuGraphicsEGLRegisterImage: unsafe extern "C" fn(
217        pCudaResource: *mut CUgraphicsResource,
218        image: EGLImageKHR,
219        flags: u32,
220    ) -> CUresult,
221    cuGraphicsUnregisterResource: unsafe extern "C" fn(resource: CUgraphicsResource) -> CUresult,
222    cuGraphicsResourceGetMappedEglFrame: unsafe extern "C" fn(
223        pEglFrame: *mut CUeglFrame,
224        resource: CUgraphicsResource,
225        index: u32,
226        mipLevel: u32,
227    ) -> CUresult,
228    cuDeviceGetCount: unsafe extern "C" fn(count: *mut i32) -> CUresult,
229    cuDeviceGetName: unsafe extern "C" fn(name: *mut c_char, len: i32, dev: CUdevice) -> CUresult,
230    cuDeviceGetUuid: unsafe extern "C" fn(uuid: *mut CUuuid, dev: CUdevice) -> CUresult,
231    cuGetErrorName: unsafe extern "C" fn(error: CUresult, pStr: *mut *const c_char) -> CUresult,
232    /// External semaphores (CUDA 10), bound only where present: the X server's blit semaphore is
233    /// imported and waited on through them, and a driver without them keeps the GetImage fence.
234    cuImportExternalSemaphore: Option<
235        unsafe extern "C" fn(
236            out: *mut CUexternalSemaphore,
237            desc: *const CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC,
238        ) -> CUresult,
239    >,
240    cuWaitExternalSemaphoresAsync: Option<
241        unsafe extern "C" fn(
242            semaphores: *const CUexternalSemaphore,
243            params: *const CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS,
244            count: u32,
245            stream: CUstream,
246        ) -> CUresult,
247    >,
248    cuDestroyExternalSemaphore: Option<unsafe extern "C" fn(sem: CUexternalSemaphore) -> CUresult>,
249}
250
251/// Dynamically loaded NVENC entry points (from `libnvidia-encode`).
252///
253/// - **`create_instance`** (`NvEncodeAPICreateInstance`): fills an `NV_ENCODE_API_FUNCTION_LIST`
254///   with the driver's encode entry points for a requested API-version word.
255/// - **`get_max_version`** (`NvEncodeAPIGetMaxSupportedVersion`): the highest API version the
256///   driver supports, used to cap version probing. `Option` because very old drivers lack it, in
257///   which case probing relies on `create_instance` acceptance alone.
258///
259/// `_lib` keeps the library resident for the function pointers' life.
260struct NvencLibrary {
261    _lib: Library,
262    create_instance:
263        unsafe extern "C" fn(functionList: *mut NV_ENCODE_API_FUNCTION_LIST) -> NVENCSTATUS,
264    get_max_version: Option<unsafe extern "C" fn(*mut u32) -> NVENCSTATUS>,
265}
266
267/// Negotiated NVENC API version `(major, minor)`, resolved once per process. `None` until
268/// `nvenc_negotiate` runs; every struct-version word and the session `apiVersion` derive from it.
269static NVENC_NEG_VER: std::sync::OnceLock<(u32, u32)> = std::sync::OnceLock::new();
270
271/// The NVENCAPI structs this encoder must stamp with a per-SDK version word — enumerated
272/// here precisely because getting that word exactly right is the whole mechanism that lets one
273/// compiled binary satisfy every driver's version check.
274///
275/// Each NVENCAPI struct carries a `version` field that the driver validates against the exact word
276/// its own SDK defined for that struct, rejecting anything else outright with
277/// `NV_ENC_ERR_INVALID_VERSION`. Only two parts of that packed word move between SDKs — the struct
278/// **revision** (bits 16-23) and the **`1<<31` flag** — so a session that has down-negotiated to an
279/// older API cannot send the compiled 13.0 words; it must stamp each struct with precisely the word
280/// that older SDK defined, while a current driver still receives its own native word. Naming the
281/// structs here is what lets `NvStruct::rev` supply the per-version `(revision, flag)` and
282/// `nvenc_struct_ver` assemble the word.
283#[derive(Clone, Copy, Debug)]
284enum NvStruct {
285    FunctionList,
286    OpenSessionExParams,
287    Config,
288    RcParams,
289    PresetConfig,
290    InitializeParams,
291    ReconfigureParams,
292    RegisterResource,
293    MapInputResource,
294    CreateBitstreamBuffer,
295    PicParams,
296    LockBitstream,
297    CapsParam,
298}
299
300impl NvStruct {
301    /// The `(struct revision, 1<<31 flag)` this struct uses under the SDK identified by the
302    /// packed API version `api` (`(major<<4)|minor`) — the only two sub-fields that move between
303    /// SDKs, and thus the entire per-version knowledge stamping a struct actually needs.
304    ///
305    /// The revision lands in bits 16-23 of the version word and the flag in bit 31; every other bit
306    /// is fixed, which is exactly why matching just these two on `api` reproduces each SDK's word.
307    /// The values are transcribed verbatim from `nvEncodeAPI.h` at the FFmpeg nv-codec-headers tags
308    /// 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
309    /// are ground truth rather than anything derived that could drift. Structs whose layout is stable
310    /// across those SDKs return a constant pair; the rest match on `api`. 10.0 is the negotiation
311    /// floor, so the oldest match arm also covers anything below it.
312    fn rev(self, api: u32) -> (u32, bool) {
313        match self {
314            NvStruct::FunctionList => (2, false),
315            NvStruct::OpenSessionExParams => (1, false),
316            NvStruct::Config => match api {
317                0xC2.. => (9, true),
318                0xC0..=0xC1 => (8, true),
319                _ => (7, true),
320            },
321            NvStruct::RcParams => (1, false),
322            NvStruct::PresetConfig => (if api >= 0xC2 { 5 } else { 4 }, true),
323            NvStruct::InitializeParams => match api {
324                0xC2.. => (7, true),
325                0xC1 => (6, true),
326                _ => (5, true),
327            },
328            NvStruct::ReconfigureParams => (if api >= 0xC2 { 2 } else { 1 }, true),
329            NvStruct::RegisterResource => match api {
330                0xC2.. => (5, false),
331                0xC0..=0xC1 => (4, false),
332                _ => (3, false),
333            },
334            NvStruct::MapInputResource => (4, false),
335            NvStruct::CreateBitstreamBuffer => (1, false),
336            NvStruct::PicParams => match api {
337                0xC2.. => (7, true),
338                0xC0..=0xC1 => (6, true),
339                _ => (4, true),
340            },
341            NvStruct::LockBitstream => match api {
342                0xC2.. => (2, true),
343                0xC1 => (1, true),
344                0xC0 => (2, false),
345                _ => (1, false),
346            },
347            NvStruct::CapsParam => (1, false),
348        }
349    }
350}
351
352/// Assemble the `NVENCAPI_STRUCT_VERSION` word for struct `s` at API version `(maj, min)`.
353///
354/// The 32-bit word packs, from `NvStruct::rev` and the API version:
355///
356/// 1. **API major** in bits 0-7, **API minor** in bits 24-27.
357/// 2. **Struct revision** (`rev`) in bits 16-23.
358/// 3. **Magic `0x7`** in bits 28-30.
359/// 4. **The `1<<31` flag** in bit 31, when this struct sets it at this version.
360///
361/// For the pinned nvcodec-sys headers this reproduces the compile-time `NV_ENC_*_VER` constants
362/// exactly, so a current driver is stamped byte-for-byte identically to its own SDK's constant; the
363/// `version_tests` module asserts that identity.
364fn nvenc_struct_ver(s: NvStruct, maj: u32, min: u32) -> u32 {
365    let (rev, high_bit) = s.rev((maj << 4) | (min & 0xF));
366    (maj & 0xFF) | ((min & 0xF) << 24) | (rev << 16) | (0x7 << 28) | ((high_bit as u32) << 31)
367}
368
369/// The process's effective NVENC API version `(major, minor)`: the negotiated value once
370/// `nvenc_negotiate` has run, otherwise the pinned `NVENCAPI_VERSION` decomposed (major in the low
371/// byte, minor at bit 24) as the pre-negotiation fallback.
372#[inline]
373fn nvenc_cur_ver() -> (u32, u32) {
374    NVENC_NEG_VER
375        .get()
376        .copied()
377        .unwrap_or((NVENCAPI_VERSION & 0xFF, (NVENCAPI_VERSION >> 24) & 0xFF))
378}
379
380/// The struct-version word for `s` tagged with the process's negotiated API version — the
381/// value every NVENCAPI struct literal assigns to its `version` field.
382#[inline]
383fn sv(s: NvStruct) -> u32 {
384    let (m, n) = nvenc_cur_ver();
385    nvenc_struct_ver(s, m, n)
386}
387
388/// The negotiated session `apiVersion` word (`major | minor<<24`) passed to
389/// `NvEncOpenEncodeSessionEx` — note the minor sits at bit 24 here, unlike the `(major<<4)|minor`
390/// packing that `NvStruct::rev` matches on.
391#[inline]
392fn neg_api() -> u32 {
393    let (m, n) = nvenc_cur_ver();
394    m | (n << 24)
395}
396
397/// A struct as long as the negotiated API's layout of it, which can run eight bytes past the
398/// pinned header's: `NV_ENC_INITIALIZE_PARAMS` below 12.2, and so the reconfigure params that
399/// embed it, and `NV_ENC_LOCK_BITSTREAM` at 12.1. The tail is zeroed and pixelflux's, so the
400/// driver reads and writes inside memory this owns.
401#[repr(C)]
402struct Negotiated<T> {
403    value: T,
404    tail: [u32; 2],
405}
406
407impl<T> Negotiated<T> {
408    fn new(value: T) -> Self {
409        Self {
410            value,
411            tail: [0; 2],
412        }
413    }
414}
415
416/// The reconfigure params re-initializing with `init` at API `(maj, min)`, resetting the encoder
417/// and forcing an IDR as asked; below 12.2 the driver reads the two flags from the tail.
418fn reconfigure_params(
419    init: NV_ENC_INITIALIZE_PARAMS,
420    (maj, min): (u32, u32),
421    reset: bool,
422    force_idr: bool,
423) -> Negotiated<NV_ENC_RECONFIGURE_PARAMS> {
424    let mut p = Negotiated::new(NV_ENC_RECONFIGURE_PARAMS {
425        version: nvenc_struct_ver(NvStruct::ReconfigureParams, maj, min),
426        reInitEncodeParams: init,
427        ..Default::default()
428    });
429    if (maj << 4) | min < 0xC2 {
430        p.tail[0] = reset as u32 | (force_idr as u32) << 1;
431    } else {
432        p.value.set_resetEncoder(reset as u32);
433        p.value.set_forceIDR(force_idr as u32);
434    }
435    p
436}
437
438/// Resolve the process-wide NVENC API version once, by probing the driver newest-first and
439/// remembering the highest version it accepts.
440///
441/// The bundled nv-codec-headers are NVENC 13.0 (`pinned`), so a current driver negotiates 13.0
442/// natively while older drivers down-negotiate through 12.x / 11.x to the 10.0 floor (~R445). The
443/// compiled struct *layouts* are the 13.0 ones, handed to older drivers with the tail their own
444/// layouts run longer (`Negotiated`); only the version *words* change per negotiated version
445/// (via the `NvStruct::rev` table), so each struct is stamped with the exact word the negotiated
446/// SDK defined. Steps:
447///
448/// 1. **Cap the search** by the driver's max: query `get_max_version` when present, then optionally
449///    lower it further from `PIXELFLUX_NVENC_MAX_API` (e.g. `"11.0"`) for testing / pinning. A cap
450///    of 0 means unknown, and probing then relies on `create_instance` acceptance alone.
451/// 2. **Probe candidates** newest-first (`pinned`, 12.1, 12.0, 11.1, 11.0, 10.0), skipping any
452///    above the cap. Each probe stamps an `NV_ENCODE_API_FUNCTION_LIST` with that version's word and
453///    calls `create_instance`.
454/// 3. **Require the whole encode path**, not just a success code: the session opener plus
455///    `nvEncInitializeEncoder`, `nvEncGetEncodePresetConfigEx`, `nvEncEncodePicture`, and
456///    `nvEncLockBitstream` must all be non-null, because a driver can accept the function-list word
457///    yet leave newer entry points null. The first fully-populated version wins.
458/// 4. **Fall back** to `pinned` if nothing qualifies. Stored in `NVENC_NEG_VER`, set-once.
459fn nvenc_negotiate(lib: &NvencLibrary) {
460    NVENC_NEG_VER.get_or_init(|| {
461        let pinned = (NVENCAPI_VERSION & 0xFF, (NVENCAPI_VERSION >> 24) & 0xFF);
462        let mut drv_max: u32 = 0;
463        if let Some(get_max) = lib.get_max_version {
464            let mut m: u32 = 0;
465            if unsafe { get_max(&mut m) } == NVENCSTATUS::NV_ENC_SUCCESS {
466                drv_max = m;
467            }
468        }
469        if let Ok(cap) = std::env::var("PIXELFLUX_NVENC_MAX_API") {
470            let mut it = cap.split('.');
471            if let (Some(a), Some(b)) = (it.next(), it.next())
472                && let (Ok(cm), Ok(cn)) = (a.parse::<u32>(), b.parse::<u32>())
473            {
474                let capv = (cm << 4) | (cn & 0xF);
475                if capv != 0 && (drv_max == 0 || capv < drv_max) {
476                    drv_max = capv;
477                }
478            }
479        }
480        let candidates = [pinned, (12, 1), (12, 0), (11, 1), (11, 0), (10, 0)];
481        for (maj, min) in candidates {
482            let vv = (maj << 4) | min;
483            if drv_max != 0 && vv > drv_max {
484                continue;
485            }
486            let mut probe = NV_ENCODE_API_FUNCTION_LIST {
487                version: nvenc_struct_ver(NvStruct::FunctionList, maj, min),
488                ..Default::default()
489            };
490            let st = unsafe { (lib.create_instance)(&mut probe) };
491            if st == NVENCSTATUS::NV_ENC_SUCCESS
492                && probe.nvEncOpenEncodeSessionEx.is_some()
493                && probe.nvEncInitializeEncoder.is_some()
494                && probe.nvEncGetEncodePresetConfigEx.is_some()
495                && probe.nvEncEncodePicture.is_some()
496                && probe.nvEncLockBitstream.is_some()
497            {
498                crate::log::debug!("[pixelflux] NVENC API version negotiated: {}.{}", maj, min);
499                return (maj, min);
500            }
501        }
502        pinned
503    });
504}
505
506/// Cached CUDA import of a dmabuf, keyed by fd so a recurring capture buffer is imported
507/// once: the `EGLImageKHR`, the CUDA graphics resource it registers as, the mapped `CUeglFrame`,
508/// and how that frame reaches NVENC. Torn down on drop / reconfigure, or evicted when the fd it is
509/// keyed by no longer names the same buffer (see `DmaBufIdentity`).
510struct CachedDmaBuf {
511    identity: DmaBufIdentity,
512    egl_image: EGLImageKHR,
513    cuda_resource: CUgraphicsResource,
514    egl_frame: CUeglFrame,
515    input: DmaBufInput,
516    /// The texture the chroma convert reads an array-typed import through, so its RGB is never
517    /// copied into linear memory. Zero where the import is linear or carries no convert.
518    tex: CUtexObject,
519}
520
521/// An input surface the session does not own: a device pointer produced by another component on
522/// the same CUDA context — the NvFBC capture buffer — registered and mapped with NVENC in place
523/// so the encoder reads the captured frame where it was composited.
524///
525/// The registration is kept for as long as the pointer and geometry hold, because a capture
526/// source hands back the same buffer every frame; it is released and rebuilt when any of them
527/// changes, and always before the component that owns the memory tears it down.
528#[derive(Clone, Copy)]
529struct ExternalInput {
530    device_ptr: CUdeviceptr,
531    pitch: usize,
532    width: u32,
533    height: u32,
534    format: NV_ENC_BUFFER_FORMAT,
535    registered: NV_ENC_REGISTERED_PTR,
536    mapped: NV_ENC_INPUT_PTR,
537}
538
539/// How a cached dmabuf import feeds the encoder.
540///
541/// `Direct` is the zero-copy case: the mapped frame's first plane is itself registered and mapped
542/// as an NVENC input — as a pitch-linear device pointer or as a CUDA array, per `DirectPlane` — so
543/// encoding reads the capture buffer in place. `Copy` covers the rest: a plane `direct_plane`
544/// rules out, one the driver declined to register, or the direct path switched off; the plane is
545/// then copied into the session's packed input surface each frame.
546#[derive(Clone, Copy)]
547enum DmaBufInput {
548    Direct {
549        registered: NV_ENC_REGISTERED_PTR,
550        mapped: NV_ENC_INPUT_PTR,
551        format: NV_ENC_BUFFER_FORMAT,
552    },
553    Copy,
554}
555
556/// The NVENC packed input format whose byte order is a dmabuf's: the XR24 / AR24 family is
557/// B,G,R,A in memory (NVENC's word-ordered `ARGB`), the XB24 / AB24 family R,G,B,A (`ABGR`).
558/// `None` for any other fourcc — nothing NVENC reads as packed 8-bit RGB.
559/// The fourcc a dmabuf is described with for CUDA's EGL import: the alpha-carrying twin of an
560/// X-format, since the NVIDIA driver refuses XRGB/XBGR images there while the bytes lie identically
561/// and NVENC ignores the alpha channel of ARGB/ABGR input.
562fn egl_import_fourcc(code: Fourcc) -> Fourcc {
563    match code {
564        Fourcc::Xrgb8888 => Fourcc::Argb8888,
565        Fourcc::Xbgr8888 => Fourcc::Abgr8888,
566        other => other,
567    }
568}
569
570fn fourcc_nvenc_format(code: Fourcc) -> Option<NV_ENC_BUFFER_FORMAT> {
571    match code {
572        Fourcc::Argb8888 | Fourcc::Xrgb8888 => {
573            Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB)
574        }
575        Fourcc::Abgr8888 | Fourcc::Xbgr8888 => {
576            Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR)
577        }
578        _ => None,
579    }
580}
581
582/// How the first plane of a mapped `CUeglFrame` registers with NVENC in place: the
583/// `NV_ENC_REGISTER_RESOURCE` resource type and the `pitch` word that type expects.
584#[derive(Debug, Clone, Copy, PartialEq, Eq)]
585enum DirectPlane {
586    /// Pitch-linear device memory, registered as a CUDA device pointer at this row pitch.
587    Pitch(u32),
588    /// A two-dimensional CUDA array of four 8-bit channels, registered as a CUDA array; the
589    /// value is the array's row width in bytes (`Width × NumChannels`), which is what NVENC
590    /// takes as the pitch of an array resource.
591    Array(u32),
592}
593
594/// Whether NVENC can read a mapped `CUeglFrame` in place, and how.
595///
596/// Either frame kind has to be usable as the session's `width × height` input: a first plane
597/// present and non-null, and a geometry of at least the session's. A pitch-linear plane also needs
598/// a row pitch covering `width * 4` bytes at the 4-byte alignment `NV_ENC_REGISTER_RESOURCE`
599/// requires; a CUDA-array plane has to be four 8-bit channels, the layout NVENC's packed formats
600/// describe. `None` sends the frame down the per-frame copy into the session's own input surface.
601fn direct_plane(frame: &CUeglFrame, width: u32, height: u32) -> Option<DirectPlane> {
602    if frame.plane_count < 1 || width == 0 || height == 0 {
603        return None;
604    }
605    if frame.width < width || frame.height < height {
606        return None;
607    }
608    match frame.frame_type {
609        CU_EGL_FRAME_TYPE_PITCH => {
610            let plane = unsafe { frame.frame.p_pitch[0] };
611            let pitch_ok = frame.pitch >= width.saturating_mul(4) && frame.pitch.is_multiple_of(4);
612            (!plane.is_null() && pitch_ok).then_some(DirectPlane::Pitch(frame.pitch))
613        }
614        CU_EGL_FRAME_TYPE_ARRAY => {
615            let array = unsafe { frame.frame.p_array[0] };
616            let packed_8bit = frame.cu_format == CU_AD_FORMAT_U8 && frame.num_channels == 4;
617            (!array.is_null() && packed_8bit).then_some(DirectPlane::Array(frame.width * 4))
618        }
619        _ => None,
620    }
621}
622
623/// The stable identity of a dmabuf, so a cache keyed by the raw fd number cannot hand back a
624/// stale EGLImage after that fd was closed and recycled onto a different buffer.
625///
626/// The fd integer alone is not an identity: the host's slot renegotiation frees the backing buffer
627/// objects and the kernel reissues the same small fd numbers for the new ones. Since Linux 5.3 each
628/// dma-buf carries its own inode, so `(st_dev, st_ino)` distinguishes two buffers that reuse one fd
629/// number, and `size` reports the true allocation; the DRM format modifier and the geometry round
630/// out the identity for older kernels where the inode is shared. A cache hit requires every field to
631/// match, so a recycled fd whose buffer differs in any of them is re-imported instead of reused.
632#[derive(Debug, Clone, Copy, PartialEq, Eq)]
633struct DmaBufIdentity {
634    dev: u64,
635    ino: u64,
636    size: i64,
637    modifier: u64,
638    width: u32,
639    height: u32,
640}
641
642impl DmaBufIdentity {
643    /// Read the identity of the buffer behind `fd`: `fstat` supplies the inode and allocation
644    /// size, and the caller supplies the modifier and geometry from the dmabuf descriptor. A failed
645    /// `fstat` leaves the inode/size zero, which still combines with the modifier and geometry.
646    fn probe(fd: i32, modifier: u64, width: u32, height: u32) -> Self {
647        let mut st: libc::stat = unsafe { std::mem::zeroed() };
648        let (dev, ino, size) = if unsafe { libc::fstat(fd, &mut st) } == 0 {
649            (st.st_dev as u64, st.st_ino as u64, st.st_size as i64)
650        } else {
651            (0, 0, 0)
652        };
653        Self {
654            dev,
655            ino,
656            size,
657            modifier,
658            width,
659            height,
660        }
661    }
662}
663
664/// The chroma format and dimensional feasibility a session settles on given the driver's
665/// reported capabilities, so init degrades cleanly instead of failing opaquely.
666///
667/// - `fullcolor` is the chroma actually used: 4:4:4 only when it was requested and the GPU carries
668///   it, otherwise 4:2:0.
669/// - `downgraded_color` records that a 4:4:4 request was met with 4:2:0, so the caller says so once.
670/// - `too_large` carries the driver's `(max_w, max_h)` when the requested geometry exceeds it; the
671///   caller then declines NVENC and falls back to software rather than failing to initialize.
672///
673/// A capability that could not be queried is `None` and does not gate: 4:4:4 stays as requested and
674/// the dimension test is skipped, so an unavailable answer never forces a false downgrade or refusal.
675#[derive(Debug, Clone, Copy, PartialEq, Eq)]
676struct CapsDecision {
677    fullcolor: bool,
678    downgraded_color: bool,
679    too_large: Option<(i32, i32)>,
680}
681
682/// Resolve the requested chroma and geometry against the driver caps (`None` = unknown, ungated).
683fn decide_caps(
684    req_fullcolor: bool,
685    req_w: i32,
686    req_h: i32,
687    cap_yuv444: Option<i32>,
688    cap_width_max: Option<i32>,
689    cap_height_max: Option<i32>,
690) -> CapsDecision {
691    let downgraded_color = req_fullcolor && cap_yuv444 == Some(0);
692    let fullcolor = req_fullcolor && !downgraded_color;
693    let exceeds = |req: i32, cap: Option<i32>| cap.is_some_and(|m| m > 0 && req > m);
694    let too_large = if exceeds(req_w, cap_width_max) || exceeds(req_h, cap_height_max) {
695        Some((cap_width_max.unwrap_or(0), cap_height_max.unwrap_or(0)))
696    } else {
697        None
698    };
699    CapsDecision {
700        fullcolor,
701        downgraded_color,
702        too_large,
703    }
704}
705
706/// The geometry every session is initialized to hold, so `reconfigure_resolution` can grow into
707/// it in place. It spans both UHD and DCI 4K and is the largest picture the 5.x levels of all
708/// three codecs admit, which is what lets AV1 -- pinned to this level -- stay inside a level a
709/// decoder accepts; a taller resize rebuilds the session instead.
710const HEADROOM_WIDTH: u32 = 4096;
711const HEADROOM_HEIGHT: u32 = 2160;
712
713/// The frame rate the level is read at when the session's is lower, so a live change between
714/// the common rates keeps the level, as the geometry headroom keeps it across a resize.
715const HEADROOM_FPS: u32 = 60;
716
717/// The frames a reference window holds for a decoded picture buffer of `dpb`: an H.265 session
718/// keeping anchors still reaches, on Pascal's NVENC, the frame the next frame's reference picture
719/// set lets go, one past the buffer it declares. Turing's, Ampere's and Ada's do not, and
720/// predict from the anchor before that frame where the window names it: a dependency the window
721/// states no older than the real one, so a client holding the frame named holds the one used.
722fn window_frames(codec: Codec, dpb: u32, anchored: bool) -> u32 {
723    dpb + u32::from(codec == Codec::H265 && anchored)
724}
725
726/// Persistent anchors only where the negotiated API exposes them, the device can invalidate
727/// references and the DPB leaves two recent pictures beside them. AV1 gained the LTR fields in
728/// SDK 13; drivers negotiated down to an older API keep the unanchored path. AV1 keeps one, a
729/// frame every forty-eight, unless the consumer `acknowledged` what its clients hold and the
730/// device has room for two: then the newest every client holds is pinned beside the one the
731/// schedule marks. H.264 keeps one whatever the consumer says: OpenH264's decoder, Firefox's for
732/// WebRTC H.264 where it has no FFmpeg that decodes it, finds a long-term picture of a CABAC
733/// stream by a number no marking sets, so of two it reaches only the first, and decodes a frame
734/// naming the second against another picture without an error; and a key frame marked
735/// long-term leaves a second index past `MaxLongTermFrameIdx` besides.
736fn anchor_count(
737    codec: Codec,
738    api_major: u32,
739    invalidation: bool,
740    ltr: Option<i32>,
741    dpb: u32,
742    acknowledged: bool,
743) -> usize {
744    let fits =
745        |n: usize| invalidation && ltr.is_some_and(|l| l >= n as i32) && dpb as usize >= n + 2;
746    let count = match codec {
747        Codec::Av1 if api_major < 13 => 0,
748        Codec::H265 => ANCHORS,
749        Codec::Av1 if acknowledged && fits(ANCHORS) => ANCHORS,
750        _ => 1,
751    };
752    if fits(count) { count } else { 0 }
753}
754
755/// The in-place resize headroom for one axis: the requested size lifted to `floor` but never past
756/// the driver's reported maximum, so initializing with headroom cannot itself exceed what the GPU
757/// supports.
758fn nvenc_headroom(size: u32, floor: u32, cap: Option<i32>) -> u32 {
759    let want = size.max(floor);
760    match cap {
761        Some(m) if m > 0 => want.min(m as u32),
762        _ => want,
763    }
764}
765
766/// The share of an AV1 level's Annex A MaxBitrate the strictest driver lets a session declare
767/// at it, as a fraction: two thirds on 595.71.05, where 595.91.07 and 615.71.09 take the whole.
768const NVENC_AV1_RATE: (u64, u64) = (2, 3);
769
770/// Whether the process's driver holds an AV1 level to `NVENC_AV1_RATE` of its Annex A rate: set
771/// where it refused a session or a rate change at the Annex A level and took the weighted one,
772/// so later sessions declare the weighted level without the refusal.
773static NVENC_AV1_WEIGHTED: AtomicBool = AtomicBool::new(false);
774
775/// The rate a `codec` level has to admit for a peak of `bps`: AV1's weighed by `NVENC_AV1_RATE`
776/// where the driver holds a level to that share.
777fn level_rate(codec: Codec, bps: u64, weighted: bool) -> u64 {
778    if codec == Codec::Av1 && weighted {
779        (bps * NVENC_AV1_RATE.1).div_ceil(NVENC_AV1_RATE.0)
780    } else {
781        bps
782    }
783}
784
785/// The highest CBR target an NVENC session of `codec` opens at, its top level's bitrate ceiling:
786/// H.264 6.2's 1 Gbit/s, HEVC 6.2's 800 Mbit/s at the High tier production sessions declare,
787/// and `NVENC_AV1_RATE` of AV1 6.3's Main-tier 160 Mbit/s. No level admits a rate past it, so
788/// the driver would refuse the session, or the change, as an invalid level.
789fn nvenc_rate_ceiling(codec: Codec) -> u32 {
790    match codec {
791        Codec::Av1 => (160_000_000 * NVENC_AV1_RATE.0 / NVENC_AV1_RATE.1) as u32,
792        Codec::H265 => 800_000_000,
793        _ => 1_000_000_000,
794    }
795}
796
797/// The split-frame mode a `codec` session asks for at `width` x `height` on a device of `engines`
798/// NVENC engines under the negotiated `api`, as `gpu_bench_tuning_on_content` measured it. AV1
799/// splits across the engines whatever the picture: a 1080p frame encodes in 1.36 ms rather than
800/// 2.07 on an RTX 4090, a 1440p one in 2.27 rather than 3.58, and both code as well or better,
801/// where the driver's own choice splits only from 4K. HEVC splits from a 4K picture, which the
802/// driver's choice splits on Ada but not on Pascal: a GTX 1080 encodes the 2160p frame in 10.3 ms
803/// rather than 16.1, more than 5% of the 81 ms such a session measures glass to glass, for 2.5 dB
804/// of PSNR on scrolling text. Below 4K a split saves HEVC about a millisecond for up to 2.3 dB,
805/// so the driver decides there, and H.264 never splits. The forced mode cuts two strips on three
806/// engines as on two, so AV1 asks for three there: an L40S encodes a 1080p frame in 1.45 ms
807/// rather than 1.62 and a 2160p one in 4.2 rather than 5.3, coding better in five of six rows
808/// (up to 1.5 dB on text) and 0.5 dB worse in one, where HEVC's third strip saves nothing at 4K
809/// and costs 0.1 to 0.2 dB. Before API 12.1 the field's bits are another flag's, so an older
810/// session leaves them clear.
811fn split_mode(
812    codec: Codec,
813    width: u32,
814    height: u32,
815    engines: Option<i32>,
816    api: (u32, u32),
817) -> NV_ENC_SPLIT_ENCODE_MODE {
818    let forced = api >= (12, 1)
819        && engines.is_some_and(|n| n > 1)
820        && match codec {
821            Codec::Av1 => true,
822            Codec::H265 => width as u64 * height as u64 >= 3840 * 2160,
823            _ => false,
824        };
825    match (forced, codec, engines) {
826        (true, Codec::Av1, Some(3)) => NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_THREE_FORCED_MODE,
827        (true, ..) => NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_AUTO_FORCED_MODE,
828        _ => NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_AUTO_MODE,
829    }
830}
831
832/// The CBR target of a session at `settings`, in bits per second, held to the codec's
833/// `nvenc_rate_ceiling`.
834fn cbr_bps(settings: &RustCaptureSettings) -> u32 {
835    (settings.video_bitrate_kbps.max(0) as u32)
836        .saturating_mul(1000)
837        .min(nvenc_rate_ceiling(settings.codec))
838}
839
840/// Say so where `cbr_bps` held the target `settings` asks for to the codec's ceiling.
841fn log_held_rate(settings: &RustCaptureSettings, bps: u32) {
842    let asked = settings.video_bitrate_kbps.max(0) as u64 * 1000;
843    if (bps as u64) < asked {
844        eprintln!(
845            "[NVENC] {} opens at {} kbps at most, its top level's ceiling: {} kbps is held there",
846            settings.codec.display(),
847            bps / 1000,
848            asked / 1000
849        );
850    }
851}
852
853/// The VBV of a CBR session at `bps`, from the session's frame rate, key-frame interval, and
854/// explicit multiplier.
855fn cbr_vbv(settings: &RustCaptureSettings, bps: u32) -> u32 {
856    crate::encoders::vbv_bits(
857        bps,
858        settings.target_fps,
859        settings.keyframe_interval_s,
860        settings.video_vbv_multiplier,
861    )
862}
863
864/// Write a CBR target into `rc`: the average and peak rate, the VBV, and an initial delay of the
865/// whole buffer. That delay is the driver's default (`gpu_bench_cbr_policy` finds the two
866/// identical), stated so the HRD's starting point is explicit and a rate change restates it
867/// with the buffer it belongs to.
868fn set_cbr_rate(rc: &mut NV_ENC_RC_PARAMS, bps: u32, vbv: u32) {
869    rc.averageBitRate = bps;
870    rc.maxBitRate = bps;
871    rc.vbvBufferSize = vbv;
872    rc.vbvInitialDelay = vbv;
873}
874
875/// Fill `map`, the QP delta map of a `width` x `height` picture of `codec` (one entry a 16x16
876/// macroblock for H.264, a 32x32 coding tree block for HEVC, a 64x64 superblock for AV1, in
877/// raster order), with `delta` over the blocks the share of the picture `from..to` covers,
878/// rounded outward, and 0 elsewhere.
879fn fill_band_map(
880    map: &mut Vec<i8>,
881    codec: Codec,
882    width: u32,
883    height: u32,
884    (from, to): (f64, f64),
885    delta: i32,
886) {
887    let block = match codec {
888        Codec::H264 => 16,
889        Codec::H265 => 32,
890        _ => 64,
891    };
892    let blocks = (width.div_ceil(block) * height.div_ceil(block)) as usize;
893    let first = ((from.clamp(0.0, 1.0) * blocks as f64).floor() as usize).min(blocks);
894    let last = ((to.clamp(0.0, 1.0) * blocks as f64).ceil() as usize).clamp(first, blocks);
895    map.clear();
896    map.resize(blocks, 0);
897    map[first..last].fill(delta.clamp(-(codec.quantizer_max().min(128) as i32), 0) as i8);
898}
899
900/// The driver's message for the last failure on `session`, or a stand-in when it has none: NVENC
901/// clears the string once a session is torn down, so it is only meaningful read straight after
902/// the call that failed.
903unsafe fn last_error(funcs: &NV_ENCODE_API_FUNCTION_LIST, session: *mut c_void) -> String {
904    funcs
905        .nvEncGetLastErrorString
906        .map(|f| f(session))
907        .filter(|p| !p.is_null())
908        .map(|p| CStr::from_ptr(p).to_string_lossy().into_owned())
909        .filter(|s| !s.is_empty())
910        .unwrap_or_else(|| "no error string".to_string())
911}
912
913/// Query one NVENC capability of `codec` on an open session, returning the driver's integer
914/// answer or `None` when the entry point is absent or the query fails — `decide_caps` reads
915/// `None` as "do not gate", so a query failure never becomes a false refusal.
916unsafe fn query_cap(
917    funcs: &NV_ENCODE_API_FUNCTION_LIST,
918    session: *mut c_void,
919    codec: GUID,
920    cap: NV_ENC_CAPS,
921) -> Option<i32> {
922    let get = funcs.nvEncGetEncodeCaps?;
923    let mut param = NV_ENC_CAPS_PARAM {
924        version: sv(NvStruct::CapsParam),
925        capsToQuery: cap,
926        reserved: [0u32; 62],
927    };
928    let mut val: i32 = 0;
929    if get(session, codec, &mut param, &mut val) == NVENCSTATUS::NV_ENC_SUCCESS {
930        Some(val)
931    } else {
932        None
933    }
934}
935
936/// GUID selecting the H.264 **High** profile (4:2:0) for `NV_ENC_CONFIG::profileGUID`.
937const NV_ENC_H264_PROFILE_HIGH_GUID: GUID = GUID {
938    Data1: 0x205b553d,
939    Data2: 0x5f01,
940    Data3: 0x4d9e,
941    Data4: [0x91, 0x84, 0xda, 0x32, 0x77, 0x5b, 0x55, 0x9b],
942};
943
944/// GUID selecting the H.264 **High 4:4:4 Predictive** profile for full-color encoding.
945const NV_ENC_H264_PROFILE_HIGH_444_GUID: GUID = GUID {
946    Data1: 0x7ac663cb,
947    Data2: 0xa598,
948    Data3: 0x4960,
949    Data4: [0xb8, 0x44, 0x33, 0x9b, 0x26, 0x1a, 0x7d, 0x5c],
950};
951
952/// Whether two GUIDs are the same.
953fn guid_eq(a: &GUID, b: &GUID) -> bool {
954    a.Data1 == b.Data1 && a.Data2 == b.Data2 && a.Data3 == b.Data3 && a.Data4 == b.Data4
955}
956
957/// The NVENC codec GUID of a codec the encoder serves, or `None` for one NVENC has no engine
958/// for.
959fn codec_guid(codec: Codec) -> Option<GUID> {
960    match codec {
961        Codec::H264 => Some(NV_ENC_CODEC_H264_GUID),
962        Codec::H265 => Some(NV_ENC_CODEC_HEVC_GUID),
963        Codec::Av1 => Some(NV_ENC_CODEC_AV1_GUID),
964        Codec::Jpeg | Codec::Vp8 | Codec::Vp9 => None,
965    }
966}
967
968/// The video codecs the NVENC of the GPU behind `encode_node_index` has an engine for, read
969/// from a bare session's GUID list the way a real session reads it (`device_encodes`): the
970/// driver is negotiated, the device bound by the render node's PCI bus id (the first CUDA
971/// device where the node names none), and the session opened on its primary context with no
972/// input buffers, EGL, or encoder initialization. An error names the step that failed: no
973/// driver, no device, or a session that would not open, each of which a real session would
974/// fail on too. The CUDA and NVENC libraries stay loaded like a session's, since the driver
975/// does not promise to survive `libcuda` being unloaded after `cuInit`.
976pub(crate) fn probe_codecs(encode_node_index: i32) -> Result<Vec<(Codec, super::Formats)>, String> {
977    let cuda = std::mem::ManuallyDrop::new(NvencEncoder::load_cuda()?);
978    let nvenc_lib = std::mem::ManuallyDrop::new(NvencEncoder::load_nvenc()?);
979    nvenc_negotiate(&nvenc_lib);
980    crate::nvgpufilter::install();
981    unsafe {
982        let res = (cuda.cuInit)(0);
983        if res != CUresult::CUDA_SUCCESS {
984            return Err(format!(
985                "Init CUDA failed: {}",
986                NvencEncoder::get_error_string(&cuda, res)
987            ));
988        }
989        let mut cu_device: CUdevice = 0;
990        let bound = NvencEncoder::get_pci_bus_id(encode_node_index.max(0))
991            .and_then(|id| CString::new(id).ok())
992            .is_some_and(|id| {
993                (cuda.cuDeviceGetByPCIBusId)(&mut cu_device, id.as_ptr()) == CUresult::CUDA_SUCCESS
994            });
995        if !bound && (cuda.cuDeviceGet)(&mut cu_device, 0) != CUresult::CUDA_SUCCESS {
996            return Err("Failed to get default CUDA device".into());
997        }
998        let mut cu_context: CUcontext = ptr::null_mut();
999        if (cuda.cuDevicePrimaryCtxRetain)(&mut cu_context, cu_device) != CUresult::CUDA_SUCCESS {
1000            return Err("Failed to retain the device's primary CUDA context".into());
1001        }
1002        if (cuda.cuCtxPushCurrent_v2)(cu_context) != CUresult::CUDA_SUCCESS {
1003            (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1004            return Err("Failed to make the primary CUDA context current".into());
1005        }
1006        let result = probe_session_codecs(&nvenc_lib, cu_context);
1007        (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1008        (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1009        result
1010    }
1011}
1012
1013/// The probe's answer where the device refuses a session for want of one to spare (the
1014/// session cap of consumer boards, or its memory): nothing lasting, unlike its other refusals.
1015pub(crate) const SESSIONS_TAKEN: &str = "the device has no NVENC session to spare";
1016
1017/// Why the driver refused to open a session: `SESSIONS_TAKEN` for the statuses it answers when
1018/// none is left, `NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY` at the cap of a consumer board and
1019/// `NV_ENC_ERR_OUT_OF_MEMORY` when its memory runs out.
1020fn session_refusal(status: NVENCSTATUS) -> String {
1021    match status {
1022        NVENCSTATUS::NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY | NVENCSTATUS::NV_ENC_ERR_OUT_OF_MEMORY => {
1023            SESSIONS_TAKEN.into()
1024        }
1025        _ => "Failed to open NVENC session".into(),
1026    }
1027}
1028
1029/// Whether a session of `codec` codes 10 bits: HEVC and AV1 where the device reports it, from
1030/// the API that names a session's input and output depths (12.2). The session converts its
1031/// 8-bit RGB to 10-bit samples itself (`ConvertLayout`). H.264 stays 8-bit, which only the
1032/// newest engines code at 10.
1033unsafe fn codes_ten_bit(
1034    function_list: &NV_ENCODE_API_FUNCTION_LIST,
1035    session: *mut c_void,
1036    codec: Codec,
1037    guid: GUID,
1038) -> bool {
1039    let (major, minor) = nvenc_cur_ver();
1040    matches!(codec, Codec::H265 | Codec::Av1)
1041        && (major << 4) | minor >= 0xC2
1042        && query_cap(
1043            function_list,
1044            session,
1045            guid,
1046            NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_10BIT_ENCODE,
1047        ) == Some(1)
1048}
1049
1050/// Open a bare NVENC session on a current CUDA context, list the codecs its device encodes
1051/// and the formats past 8-bit 4:2:0 it encodes each in, and close it.
1052unsafe fn probe_session_codecs(
1053    nvenc_lib: &NvencLibrary,
1054    cu_context: CUcontext,
1055) -> Result<Vec<(Codec, super::Formats)>, String> {
1056    let mut function_list = NV_ENCODE_API_FUNCTION_LIST {
1057        version: sv(NvStruct::FunctionList),
1058        ..Default::default()
1059    };
1060    if (nvenc_lib.create_instance)(&mut function_list) != NVENCSTATUS::NV_ENC_SUCCESS {
1061        return Err("NvEncodeAPICreateInstance failed".into());
1062    }
1063    let (Some(open_fn), Some(destroy_fn)) = (
1064        function_list.nvEncOpenEncodeSessionEx,
1065        function_list.nvEncDestroyEncoder,
1066    ) else {
1067        return Err("the driver's NVENC function list has no session entry points".into());
1068    };
1069    let mut session_params = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
1070        version: sv(NvStruct::OpenSessionExParams),
1071        deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
1072        device: cu_context as *mut c_void,
1073        apiVersion: neg_api(),
1074        ..Default::default()
1075    };
1076    let mut session: *mut c_void = ptr::null_mut();
1077    match open_fn(&mut session_params, &mut session) {
1078        NVENCSTATUS::NV_ENC_SUCCESS => {}
1079        status => return Err(session_refusal(status)),
1080    }
1081    let codecs = Codec::VIDEO
1082        .into_iter()
1083        .filter_map(|codec| {
1084            let guid = codec_guid(codec)
1085                .filter(|guid| NvencEncoder::device_encodes(&function_list, session, guid))?;
1086            let fullcolor = codec.fullcolor()
1087                && query_cap(
1088                    &function_list,
1089                    session,
1090                    guid,
1091                    NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE,
1092                ) == Some(1);
1093            let ten_bit = codes_ten_bit(&function_list, session, codec, guid);
1094            Some((
1095                codec,
1096                super::Formats {
1097                    fullcolor,
1098                    ten_bit: [ten_bit, ten_bit && fullcolor],
1099                },
1100            ))
1101        })
1102        .collect();
1103    destroy_fn(session);
1104    Ok(codecs)
1105}
1106
1107/// `sliceMode = 3`: `sliceModeData` is the number of slices in the picture, which the driver
1108/// divides evenly; the other modes count macroblocks, bytes, or rows and drift with geometry.
1109const SLICE_MODE_COUNT: u32 = 3;
1110
1111/// Slices per H.264 and HEVC frame, the count the VA-API (`SLICES`; one for H.264 on AMD's VCE)
1112/// and OpenH264 (`SM_FIXEDSLCNUM_SLICE`) sessions emit too: a client decoding in software threads
1113/// a frame across its slices, and more than four upsets some Chromium decoders. What the slices cost at
1114/// a fixed quantizer is measured by `gpu_bench_slices`. AV1 partitions by tiles instead, asked
1115/// for as 1x1 in `configure_codec`.
1116const SLICES_PER_FRAME: u32 = 4;
1117
1118/// The frames an AV1 session keeps, as many as a frame predicts from, LAST, LAST2, LAST3, and
1119/// GOLDEN (`numFwdRefs` tops out at four). The device keeps eight as well, predicting past a loss
1120/// from further back, but takes 112 MiB more a session at the resize headroom. With SDK 13
1121/// and LTR support, the anchors among them let an invalidation reach further back.
1122const AV1_REFERENCES: u32 = 4;
1123
1124/// Output bitstream buffers per session: one, because `submit_frame` locks, copies, and unlocks
1125/// each frame's bitstream before it returns, so no second buffer is ever outstanding (the lock
1126/// blocks; a `doNotWait` lock on Linux answers an unfinished encode with an empty bitstream
1127/// rather than `NV_ENC_ERR_LOCK_BUSY`). The ring stays, so a pipelined depth is one constant
1128/// away.
1129const BITSTREAM_BUFFERS: usize = 1;
1130
1131/// The encoder-side quality knobs of a session: the preset, the rate-control passes of a CBR
1132/// session, and adaptive quantization. Production sessions take the default, which the tuning
1133/// bench (`gpu_bench_tuning`) measures against the alternatives: P3 encodes a 1080p H.264
1134/// frame in 3.7 ms where P4 takes 5.2 ms on a V100, for 0.001 of SSIM at the same bitrate,
1135/// and the presets above P4 buy nothing; a single pass saves half a millisecond but overshoots
1136/// a CBR target by five to twelve percent; adaptive quantization moves neither time nor
1137/// quality measurably. The quarter-resolution first pass needs the 1.5-frame VBV `vbv_bits`
1138/// gives it: on a one-frame buffer its miss on a scene cut runs an 8 Mbit/s H.264 session at
1139/// 13.7 Mbit/s (`gpu_bench_cbr_rate_control`). P3 holds on real content as well
1140/// (`gpu_bench_tuning_on_content`, scrolling text and a panned texture at 1080p to 2160p): on a
1141/// GTX 1080 and an RTX 4090 P1 encodes 0.5 to 2.4 ms sooner and P2 less, at most about 5% of the
1142/// glass to glass latency a session of that size measures (25, 41, and 81 ms over WebSockets),
1143/// and both code the text worse, by up to 0.8 dB in H.264 and 2.4 dB in AV1.
1144#[derive(Clone, Copy, Debug)]
1145pub(crate) struct NvencTuning {
1146    pub preset: GUID,
1147    pub multipass: NV_ENC_MULTI_PASS,
1148    pub spatial_aq: bool,
1149    /// Temporal adaptive quantization, refused at open where the driver reports no support for
1150    /// it: a Volta HEVC session given it faults inside its first encode instead of failing to
1151    /// open.
1152    pub temporal_aq: bool,
1153    /// Whether an HEVC session declares the tier `h265_tier` names for its level; off opens the
1154    /// same session at Main tier, the ceiling `gpu_hevc_high_tier_opens_above_the_main_tier_ceiling`
1155    /// measures.
1156    pub hevc_high_tier: bool,
1157    /// Slices per H.264 and HEVC frame: `SLICES_PER_FRAME` in production, one to measure their
1158    /// cost (`gpu_bench_slices`).
1159    pub slices: u32,
1160    /// Leave the chroma convert out, so the session encodes through NVENC's own conversion —
1161    /// the path a driver refusing the kernel takes, which
1162    /// `gpu_hardware_conversion_matches_the_declared_matrix` measures.
1163    #[cfg(test)]
1164    pub hardware_csc: bool,
1165    /// The L0 references of an H.264 or HEVC frame, AV1's forward references: the preset's
1166    /// choice in production, where one reference saves no time, set to measure.
1167    #[cfg(test)]
1168    pub ref_l0: Option<NV_ENC_NUM_REF_FRAMES>,
1169    /// A split-frame mode in place of `split_mode`'s, to measure.
1170    #[cfg(test)]
1171    pub split: Option<NV_ENC_SPLIT_ENCODE_MODE>,
1172}
1173
1174impl Default for NvencTuning {
1175    fn default() -> Self {
1176        Self {
1177            preset: NV_ENC_PRESET_P3_GUID,
1178            multipass: NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
1179            spatial_aq: false,
1180            temporal_aq: false,
1181            hevc_high_tier: true,
1182            slices: SLICES_PER_FRAME,
1183            #[cfg(test)]
1184            hardware_csc: false,
1185            #[cfg(test)]
1186            ref_l0: None,
1187            #[cfg(test)]
1188            split: None,
1189        }
1190    }
1191}
1192
1193/// The profile of a session: the 4:2:0 profile of the codec at its depth, or its 4:4:4 one
1194/// where the codec has it. HEVC's range extensions carry 4:4:4 at either depth, and AV1's main
1195/// profile both depths.
1196fn profile_guid(codec: Codec, fullcolor: bool, bit_depth: u32) -> GUID {
1197    match (codec, fullcolor) {
1198        (Codec::H264, true) => NV_ENC_H264_PROFILE_HIGH_444_GUID,
1199        (Codec::H264, false) => NV_ENC_H264_PROFILE_HIGH_GUID,
1200        (Codec::H265, true) => NV_ENC_HEVC_PROFILE_FREXT_GUID,
1201        (Codec::H265, false) if bit_depth > 8 => NV_ENC_HEVC_PROFILE_MAIN10_GUID,
1202        (Codec::H265, false) => NV_ENC_HEVC_PROFILE_MAIN_GUID,
1203        _ => NV_ENC_AV1_PROFILE_MAIN_GUID,
1204    }
1205}
1206
1207/// The 4:2:0 convert that replaces NVENC's own, because the hardware's fixed-function
1208/// conversion weights the two columns of a block 3:1 instead of averaging them, leaving half the
1209/// color of a subpixel-antialiased glyph edge in the chroma plane where the software and VA-API
1210/// converts leave none. It follows the matrix the session declares
1211/// (`gpu_hardware_conversion_matches_the_declared_matrix`), so only the siting is at stake, and
1212/// 4:4:4 — which subsamples nothing — keeps it.
1213///
1214/// The kernels ship as PTX the driver JIT-compiles (`cuModuleLoadData`), so the only library
1215/// involved is the `libcuda` NVENC already needs — nothing to install, and no runtime compiler.
1216/// They read the session's packed ARGB surface, or a texture over an array-typed dmabuf import,
1217/// and write the NV12 surface NVENC then encodes, averaging each block's RGB before the matrix
1218/// as the host convert does.
1219/// The surface a `ChromaConvert` writes for NVENC: NV12 for an 8-bit 4:2:0 session, and for a
1220/// 10-bit one P010 or planar 4:4:4, whose samples the kernel computes from the 8-bit RGB at
1221/// 10 bits. NVENC upconverts an 8-bit surface itself, but from samples already rounded to
1222/// eight: a 4:4:4 HEVC session coded that way measured 41.6 dB against its 8-bit 45.5 at one
1223/// quantizer on an RTX 3060, its luma half a level low.
1224#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1225enum ConvertLayout {
1226    Nv12,
1227    P010,
1228    Yuv444P10,
1229}
1230
1231impl ConvertLayout {
1232    /// The layout a session of this chroma and depth converts into; None for 8-bit 4:4:4,
1233    /// which subsamples nothing and keeps NVENC's own conversion.
1234    fn of(fullcolor: bool, bit_depth: u32) -> Option<Self> {
1235        match (fullcolor, bit_depth > 8) {
1236            (false, false) => Some(Self::Nv12),
1237            (false, true) => Some(Self::P010),
1238            (true, true) => Some(Self::Yuv444P10),
1239            (true, false) => None,
1240        }
1241    }
1242
1243    /// The kernels reading a linear surface and a texture.
1244    fn kernels(self) -> (&'static CStr, &'static CStr) {
1245        match self {
1246            Self::Nv12 => (c"argb_to_nv12", c"argb_tex_to_nv12"),
1247            Self::P010 => (c"argb_to_p010", c"argb_tex_to_p010"),
1248            Self::Yuv444P10 => (c"argb_to_yuv444p10", c"argb_tex_to_yuv444p10"),
1249        }
1250    }
1251
1252    /// The bytes a row and the rows a `width`x`height` picture take: a luma plane and half as
1253    /// many rows of interleaved chroma, or three whole planes.
1254    fn extent(self, width: u32, height: u32) -> (usize, usize) {
1255        let (width, height) = (width as usize, height as usize);
1256        match self {
1257            Self::Nv12 => (width, height + height.div_ceil(2)),
1258            Self::P010 => (2 * width, height + height.div_ceil(2)),
1259            Self::Yuv444P10 => (2 * width, 3 * height),
1260        }
1261    }
1262
1263    fn format(self) -> NV_ENC_BUFFER_FORMAT {
1264        match self {
1265            Self::Nv12 => NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_NV12,
1266            Self::P010 => NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV420_10BIT,
1267            Self::Yuv444P10 => NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444_10BIT,
1268        }
1269    }
1270}
1271
1272struct ChromaConvert {
1273    module: CUmodule,
1274    kernel: CUfunction,
1275    kernel_tex: CUfunction,
1276    layout: ConvertLayout,
1277    surface: CUdeviceptr,
1278    pitch: usize,
1279    registered: NV_ENC_REGISTERED_PTR,
1280    mapped: NV_ENC_INPUT_PTR,
1281}
1282
1283impl ChromaConvert {
1284    /// JIT the module, allocate the `width`x`height` surface of `layout`, and register it with
1285    /// the session. `None` where any step refuses: the session then encodes the packed RGB itself,
1286    /// which sites chroma at the left of each block but converts with the matrix the session
1287    /// declares all the same.
1288    unsafe fn new(
1289        cuda: &CudaFunctions,
1290        funcs: &NV_ENCODE_API_FUNCTION_LIST,
1291        session: *mut c_void,
1292        width: u32,
1293        height: u32,
1294        layout: ConvertLayout,
1295    ) -> Option<Self> {
1296        let module = load_kernels(cuda)?;
1297        let mut kernel: CUfunction = ptr::null_mut();
1298        let mut kernel_tex: CUfunction = ptr::null_mut();
1299        let (linear, texture) = layout.kernels();
1300        if (cuda.cuModuleGetFunction)(&mut kernel, module, linear.as_ptr())
1301            != CUresult::CUDA_SUCCESS
1302            || (cuda.cuModuleGetFunction)(&mut kernel_tex, module, texture.as_ptr())
1303                != CUresult::CUDA_SUCCESS
1304        {
1305            (cuda.cuModuleUnload)(module);
1306            return None;
1307        }
1308        let (mut nv12, mut pitch): (CUdeviceptr, usize) = (0, 0);
1309        // The planes follow one another at the same pitch, which is the one allocation NVENC
1310        // reads all of. The chroma rows of a 4:2:0 layout round up, so the kernel's last row
1311        // is inside the allocation even at an odd height, which the capture paths do not
1312        // produce for a video codec but nothing here relies on.
1313        let (row_bytes, rows) = layout.extent(width, height);
1314        if (cuda.cuMemAllocPitch_v2)(&mut nv12, &mut pitch, row_bytes, rows, 4)
1315            != CUresult::CUDA_SUCCESS
1316        {
1317            (cuda.cuModuleUnload)(module);
1318            return None;
1319        }
1320        let mut reg = NV_ENC_REGISTER_RESOURCE {
1321            version: sv(NvStruct::RegisterResource),
1322            resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
1323            width,
1324            height,
1325            resourceToRegister: nv12 as *mut c_void,
1326            pitch: pitch as u32,
1327            bufferFormat: layout.format(),
1328            bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
1329            ..Default::default()
1330        };
1331        if (funcs.nvEncRegisterResource.unwrap())(session, &mut reg) != NVENCSTATUS::NV_ENC_SUCCESS
1332        {
1333            (cuda.cuMemFree_v2)(nv12);
1334            (cuda.cuModuleUnload)(module);
1335            return None;
1336        }
1337        let mut map = NV_ENC_MAP_INPUT_RESOURCE {
1338            version: sv(NvStruct::MapInputResource),
1339            registeredResource: reg.registeredResource,
1340            ..Default::default()
1341        };
1342        if (funcs.nvEncMapInputResource.unwrap())(session, &mut map) != NVENCSTATUS::NV_ENC_SUCCESS
1343        {
1344            (funcs.nvEncUnregisterResource.unwrap())(session, reg.registeredResource);
1345            (cuda.cuMemFree_v2)(nv12);
1346            (cuda.cuModuleUnload)(module);
1347            return None;
1348        }
1349        Some(ChromaConvert {
1350            module,
1351            kernel,
1352            kernel_tex,
1353            layout,
1354            surface: nv12,
1355            pitch,
1356            registered: reg.registeredResource,
1357            mapped: map.mappedResource,
1358        })
1359    }
1360
1361    /// The registered surface and its format, as NVENC is handed them.
1362    fn input(&self) -> (NV_ENC_INPUT_PTR, NV_ENC_BUFFER_FORMAT) {
1363        (self.mapped, self.layout.format())
1364    }
1365
1366    /// Convert the `width`x`height` packed surface at `src`/`src_pitch` into the surface,
1367    /// on the default stream so it is ordered behind the upload and ahead of the encode.
1368    /// `swap_rb` marks an RGBA byte order rather than BGRA.
1369    unsafe fn run(
1370        &self,
1371        cuda: &CudaFunctions,
1372        src: CUdeviceptr,
1373        src_pitch: usize,
1374        width: u32,
1375        height: u32,
1376        swap_rb: bool,
1377    ) -> Result<(), String> {
1378        let (mut src, mut sp) = (src, src_pitch as i32);
1379        let (mut dst, mut dp) = (self.surface, self.pitch as i32);
1380        let (mut w, mut h, mut swap) = (width as i32, height as i32, i32::from(swap_rb));
1381        let mut params: [*mut c_void; 7] = [
1382            &mut src as *mut _ as *mut c_void,
1383            &mut sp as *mut _ as *mut c_void,
1384            &mut dst as *mut _ as *mut c_void,
1385            &mut dp as *mut _ as *mut c_void,
1386            &mut w as *mut _ as *mut c_void,
1387            &mut h as *mut _ as *mut c_void,
1388            &mut swap as *mut _ as *mut c_void,
1389        ];
1390        self.launch(cuda, self.kernel, &mut params, width, height)
1391    }
1392
1393    /// The same convert reading an array-typed import through `tex`, which leaves its RGB where
1394    /// the compositor put it instead of copying it into linear memory first.
1395    unsafe fn run_texture(
1396        &self,
1397        cuda: &CudaFunctions,
1398        tex: CUtexObject,
1399        width: u32,
1400        height: u32,
1401        swap_rb: bool,
1402    ) -> Result<(), String> {
1403        let mut tex = tex;
1404        let (mut dst, mut dp) = (self.surface, self.pitch as i32);
1405        let (mut w, mut h, mut swap) = (width as i32, height as i32, i32::from(swap_rb));
1406        let mut params: [*mut c_void; 6] = [
1407            &mut tex as *mut _ as *mut c_void,
1408            &mut dst as *mut _ as *mut c_void,
1409            &mut dp as *mut _ as *mut c_void,
1410            &mut w as *mut _ as *mut c_void,
1411            &mut h as *mut _ as *mut c_void,
1412            &mut swap as *mut _ as *mut c_void,
1413        ];
1414        self.launch(cuda, self.kernel_tex, &mut params, width, height)
1415    }
1416
1417    /// One chroma sample per thread, on the default stream so the convert is ordered behind
1418    /// whatever produced the source and ahead of the encode.
1419    unsafe fn launch(
1420        &self,
1421        cuda: &CudaFunctions,
1422        kernel: CUfunction,
1423        params: &mut [*mut c_void],
1424        width: u32,
1425        height: u32,
1426    ) -> Result<(), String> {
1427        const BLOCK: u32 = 16;
1428        let grid = (
1429            width.div_ceil(2).div_ceil(BLOCK),
1430            height.div_ceil(2).div_ceil(BLOCK),
1431        );
1432        if (cuda.cuLaunchKernel)(
1433            kernel,
1434            grid.0,
1435            grid.1,
1436            1,
1437            BLOCK,
1438            BLOCK,
1439            1,
1440            0,
1441            ptr::null_mut(),
1442            params.as_mut_ptr(),
1443            ptr::null_mut(),
1444        ) != CUresult::CUDA_SUCCESS
1445        {
1446            return Err("the chroma convert kernel failed to launch".into());
1447        }
1448        Ok(())
1449    }
1450
1451    /// A point-sampled, byte-valued texture over an array-typed import, or zero where the driver
1452    /// refuses one — the frame then stays on NVENC's own conversion.
1453    unsafe fn texture_for(cuda: &CudaFunctions, array: CUarray) -> CUtexObject {
1454        let mut res: CUDA_RESOURCE_DESC = std::mem::zeroed();
1455        res.resType = CUresourcetype::CU_RESOURCE_TYPE_ARRAY;
1456        res.res.array.hArray = array;
1457        let mut sampling: CUDA_TEXTURE_DESC = std::mem::zeroed();
1458        sampling.addressMode = [CUaddress_mode::CU_TR_ADDRESS_MODE_CLAMP; 3];
1459        sampling.filterMode = CUfilter_mode::CU_TR_FILTER_MODE_POINT;
1460        sampling.flags = CU_TRSF_READ_AS_INTEGER;
1461        let mut tex: CUtexObject = 0;
1462        if (cuda.cuTexObjectCreate)(&mut tex, &res, &sampling, ptr::null())
1463            != CUresult::CUDA_SUCCESS
1464        {
1465            return 0;
1466        }
1467        tex
1468    }
1469
1470    /// Inner handle before outer, as the rest of the teardown does.
1471    unsafe fn release(
1472        &self,
1473        cuda: &CudaFunctions,
1474        funcs: &NV_ENCODE_API_FUNCTION_LIST,
1475        session: *mut c_void,
1476    ) {
1477        (funcs.nvEncUnmapInputResource.unwrap())(session, self.mapped);
1478        (funcs.nvEncUnregisterResource.unwrap())(session, self.registered);
1479        (cuda.cuMemFree_v2)(self.surface);
1480        (cuda.cuModuleUnload)(self.module);
1481    }
1482}
1483
1484/// A hash per band of rows of a packed 32-bit frame in video memory (`band_hash`), computed
1485/// where the frame lies and read back as one word a band and column segment.
1486struct BandHash {
1487    module: CUmodule,
1488    kernel: CUfunction,
1489    sums: CUdeviceptr,
1490    capacity: usize,
1491}
1492
1493/// The threads of a `band_hash` block, as the kernel sizes its reduction, and the blocks a band
1494/// is split into across its columns, so a frame's few bands fill the GPU: a P100 hashed 1080p in
1495/// 0.08 ms this way and in 0.12 with a block a band, its 34 bands leaving 22 of the 56
1496/// multiprocessors idle.
1497const HASH_THREADS: u32 = 256;
1498const HASH_SEGMENTS: u32 = 4;
1499
1500impl BandHash {
1501    unsafe fn new(cuda: &CudaFunctions) -> Option<Self> {
1502        let module = load_kernels(cuda)?;
1503        let mut kernel: CUfunction = ptr::null_mut();
1504        if (cuda.cuModuleGetFunction)(&mut kernel, module, c"band_hash".as_ptr())
1505            != CUresult::CUDA_SUCCESS
1506        {
1507            (cuda.cuModuleUnload)(module);
1508            return None;
1509        }
1510        Some(Self {
1511            module,
1512            kernel,
1513            sums: 0,
1514            capacity: 0,
1515        })
1516    }
1517
1518    /// On the default stream so the hash is ordered behind whatever produced the frame; the copy
1519    /// back waits for it.
1520    unsafe fn run(
1521        &mut self,
1522        cuda: &CudaFunctions,
1523        src: CUdeviceptr,
1524        pitch: usize,
1525        width: u32,
1526        height: u32,
1527        rows: u32,
1528    ) -> Option<Vec<u64>> {
1529        let words = self.launch(cuda, src, pitch, width, height, rows)?;
1530        self.read(cuda, words)
1531    }
1532
1533    /// The kernel alone, queued on the default stream behind whatever produced the frame: the
1534    /// words `read` copies back, or None where it did not launch.
1535    unsafe fn launch(
1536        &mut self,
1537        cuda: &CudaFunctions,
1538        src: CUdeviceptr,
1539        pitch: usize,
1540        width: u32,
1541        height: u32,
1542        rows: u32,
1543    ) -> Option<usize> {
1544        let bands = height.div_ceil(rows.max(1)) as usize;
1545        let words = bands * HASH_SEGMENTS as usize;
1546        if words > self.capacity {
1547            if self.sums != 0 {
1548                (cuda.cuMemFree_v2)(self.sums);
1549            }
1550            self.capacity = 0;
1551            if (cuda.cuMemAlloc_v2)(&mut self.sums, words * 8) != CUresult::CUDA_SUCCESS {
1552                self.sums = 0;
1553                return None;
1554            }
1555            self.capacity = words;
1556        }
1557        let (mut src, mut sp, mut out) = (src, pitch as i32, self.sums);
1558        let (mut w, mut h, mut r) = (width as i32, height as i32, rows.max(1) as i32);
1559        let mut params: [*mut c_void; 6] = [
1560            &mut src as *mut _ as *mut c_void,
1561            &mut sp as *mut _ as *mut c_void,
1562            &mut w as *mut _ as *mut c_void,
1563            &mut h as *mut _ as *mut c_void,
1564            &mut r as *mut _ as *mut c_void,
1565            &mut out as *mut _ as *mut c_void,
1566        ];
1567        ((cuda.cuLaunchKernel)(
1568            self.kernel,
1569            bands as u32,
1570            HASH_SEGMENTS,
1571            1,
1572            HASH_THREADS,
1573            1,
1574            1,
1575            0,
1576            ptr::null_mut(),
1577            params.as_mut_ptr(),
1578            ptr::null_mut(),
1579        ) == CUresult::CUDA_SUCCESS)
1580            .then_some(words)
1581    }
1582
1583    /// The hashes a `launch` of `words` left, a word a band: the copy back waits for the kernel.
1584    unsafe fn read(&mut self, cuda: &CudaFunctions, words: usize) -> Option<Vec<u64>> {
1585        let mut sums = vec![0u64; words];
1586        ((cuda.cuMemcpyDtoH_v2)(sums.as_mut_ptr() as *mut c_void, self.sums, words * 8)
1587            == CUresult::CUDA_SUCCESS)
1588            .then(|| {
1589                sums.chunks(HASH_SEGMENTS as usize)
1590                    .map(|band| band.iter().fold(0u64, |a, &b| a.wrapping_add(b)))
1591                    .collect()
1592            })
1593    }
1594
1595    unsafe fn release(self, cuda: &CudaFunctions) {
1596        if self.sums != 0 {
1597            (cuda.cuMemFree_v2)(self.sums);
1598        }
1599        (cuda.cuModuleUnload)(self.module);
1600    }
1601}
1602
1603/// A live NVENC encoder session with its CUDA context and interop resources.
1604///
1605/// One instance owns a CUDA context bound to a specific GPU plus an NVENC session and everything
1606/// the two input paths need:
1607///
1608/// - **Packed path**: a pitched device buffer (`input_device_ptr` / `input_pitch`) registered and
1609///   mapped as the NVENC input (`registered_input_resource` / `mapped_input_buffer`) in the byte
1610///   order `input_format` names (re-registered in place when a source of the other order arrives),
1611///   fed either by a host→device upload or by the copy arm of the dmabuf path.
1612/// - **Zero-copy dmabuf path**: `dmabuf_cache` memoizes each fd's EGLImage → CUDA import, keyed by
1613///   fd but validated against the buffer's `DmaBufIdentity` so a recycled fd re-imports; an import
1614///   whose plane NVENC can take as it is — pitch-linear memory or a packed 8-bit CUDA array — is
1615///   registered in place (`DmaBufInput::Direct`) unless `direct_dmabuf` was switched off, anything
1616///   else is copied into the packed input each frame.
1617///
1618/// `bitstream_buffers` is a ring of `BITSTREAM_BUFFERS` output buffers (`current_buffer_idx`
1619/// cycles it).
1620/// `pinned_hosts` maps each page-locked host upload source's base pointer to its registered length,
1621/// with a `0` length recording a failed registration so that address is never re-pinned.
1622/// `codec` and `fullcolor` name the session's codec and negotiated chroma; `current_qp` tracks the
1623/// live ConstQP so a paint-over reconfigure is skipped when unchanged. `encode_config` and
1624/// `init_params` are retained so in-place reconfigure can resubmit them.
1625/// `omit_stripe_headers` drops the wire
1626/// header, and `node_index` is the effective CUDA device this session is bound to — a reuse across
1627/// captures that now targets a different device must rebuild rather than reconfigure.
1628pub struct NvencEncoder {
1629    encoder_session: *mut c_void,
1630    cuda_context: CUcontext,
1631    cuda_device: CUdevice,
1632    /// The GPU's marketing name, for the one line that says which device encodes.
1633    device_name: String,
1634    egl_display: EGLDisplay,
1635    codec: Codec,
1636    fullcolor: bool,
1637    /// The bits per sample the session codes.
1638    bit_depth: u32,
1639    width: u32,
1640    height: u32,
1641    current_qp: u32,
1642    /// The quality index the next frame is held at whatever the rate control
1643    /// (`hold_quantizer`), the band of it that quantizer covers, and whether the driver has
1644    /// refused one, said once.
1645    held_qp: Option<u32>,
1646    held_band: Option<(f64, f64)>,
1647    hold_refused: bool,
1648    /// Bytes of the last held frame, and the QP delta map a held band is coded through.
1649    held_bytes: usize,
1650    qp_map: Vec<i8>,
1651    /// The quality index the rate control last coded a frame at (held frames aside), from the
1652    /// driver's average quantizer.
1653    last_quality: Option<u32>,
1654    /// Bytes of the last frame the rate control coded, held frames aside.
1655    last_bytes: Option<usize>,
1656    encode_config: NV_ENC_CONFIG,
1657    init_params: NV_ENC_INITIALIZE_PARAMS,
1658    input_device_ptr: CUdeviceptr,
1659    input_pitch: usize,
1660    input_format: NV_ENC_BUFFER_FORMAT,
1661    registered_input_resource: NV_ENC_REGISTERED_PTR,
1662    mapped_input_buffer: NV_ENC_INPUT_PTR,
1663    bitstream_buffers: Vec<NV_ENC_OUTPUT_PTR>,
1664    current_buffer_idx: usize,
1665    dmabuf_cache: HashMap<i32, CachedDmaBuf>,
1666    external_input: Option<ExternalInput>,
1667    pinned_hosts: HashMap<usize, usize>,
1668    cuda: Arc<CudaFunctions>,
1669    /// The EGL entry points a dmabuf import takes, loaded for a session given an EGL display.
1670    egl: Option<Arc<EglFunctions>>,
1671    _nvenc_lib: Arc<NvencLibrary>,
1672    nvenc_funcs: NV_ENCODE_API_FUNCTION_LIST,
1673    omit_stripe_headers: bool,
1674    node_index: i32,
1675    /// Resolved once at init from `PIXELFLUX_NVENC_PIN`: page-lock the host upload sources so the
1676    /// copy is a direct pinned DMA rather than a pageable copy staged through a bounce buffer.
1677    pin_uploads: bool,
1678    /// Resolved once at init from `PIXELFLUX_NVENC_DIRECT`: register pitch-linear dmabuf imports
1679    /// with NVENC in place instead of copying them into the packed input each frame.
1680    direct_dmabuf: bool,
1681    /// The 4:2:0 chroma convert, where the driver took the kernel and the session is not 4:4:4.
1682    /// `None` leaves NVENC's own conversion in place.
1683    csc: Option<ChromaConvert>,
1684    /// The decoded picture buffer the session declares, in frames, which a resize lowers where
1685    /// the new level admits fewer and never raises.
1686    dpb: u32,
1687    /// The device's NVENC engines, which `split_mode` reads at every geometry.
1688    engines: Option<i32>,
1689    /// The frames the decoder holds, so a lost one can be left out of the predictions and each
1690    /// frame can name what it predicts from; None where the device cannot invalidate a
1691    /// reference, and nothing is tracked.
1692    references: Option<ReferenceWindow>,
1693    last_reference: Reference,
1694    /// The H.264 stream's `frame_num` a byte wider, where the session tracks its references: the
1695    /// frame where the counter wraps, which a loss covering it costs a key frame for, comes once
1696    /// in 65536 frames instead of 256.
1697    frame_num: WideFrameNum,
1698    /// The X server's blit semaphore imported into this context (`blit_semaphore_fd`), which
1699    /// `encode_after_blit` queues each frame's wait on; null until one is made.
1700    blit_semaphore: CUexternalSemaphore,
1701    /// The band hash (`band_hashes`), loaded by the first frame that asks for one; `Some(None)`
1702    /// where the driver refused it.
1703    band_hash: Option<Option<BandHash>>,
1704    /// The bands of rows the next host upload is hashed in on the device (`hash_next_upload`),
1705    /// the words that hash left for the frame being coded, and its hashes once read back
1706    /// (`take_upload_hashes`).
1707    upload_hash: Option<u32>,
1708    upload_words: Option<usize>,
1709    upload_hashes: Option<Vec<u64>>,
1710}
1711
1712unsafe impl Send for NvencEncoder {}
1713
1714/// Release every GPU resource the session holds, in the one teardown order the drivers
1715/// tolerate, so nothing leaks and no still-referenced handle is ever freed out from under the
1716/// driver.
1717///
1718/// The whole sequence runs with the owning CUDA context pushed current, because the `cuMemFree` /
1719/// `cuGraphicsUnregisterResource` / `cuMemHostUnregister` calls each act on the *current* context —
1720/// pop it first and the frees silently do nothing, leaking device memory. Within that, resources
1721/// go inner-handle before the outer handle that owns it, since freeing an owner first orphans or
1722/// faults on what still points into it: unmap the packed input before unregistering it, free its
1723/// device buffer, destroy the bitstream buffers, and release every cached dmabuf
1724/// import (`release_dmabuf_import`: its NVENC mapping and registration, then the CUDA resource and
1725/// the EGLImage) — all session-owned — before the encoder session itself, and destroy that
1726/// session before releasing the device's primary CUDA context it was opened against (the retain is
1727/// refcounted, so the context lives until the last session on that device releases it). The
1728/// page-locked host sources are unpinned in the same pass, each only when its recorded length is
1729/// non-zero (a `0` marks a registration that failed and so was never pinned).
1730impl Drop for NvencEncoder {
1731    fn drop(&mut self) {
1732        unsafe {
1733            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
1734
1735            self.unmap_external_input();
1736            if let Some(csc) = self.csc.take() {
1737                csc.release(&self.cuda, &self.nvenc_funcs, self.encoder_session);
1738            }
1739            if let Some(Some(hash)) = self.band_hash.take() {
1740                hash.release(&self.cuda);
1741            }
1742            if !self.mapped_input_buffer.is_null() {
1743                (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
1744                    self.encoder_session,
1745                    self.mapped_input_buffer,
1746                );
1747            }
1748            if !self.registered_input_resource.is_null() {
1749                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1750                    self.encoder_session,
1751                    self.registered_input_resource,
1752                );
1753            }
1754            if self.input_device_ptr != 0 {
1755                (self.cuda.cuMemFree_v2)(self.input_device_ptr);
1756            }
1757
1758            for &bs in &self.bitstream_buffers {
1759                (self.nvenc_funcs.nvEncDestroyBitstreamBuffer.unwrap())(self.encoder_session, bs);
1760            }
1761
1762            let imports: Vec<CachedDmaBuf> = self.dmabuf_cache.drain().map(|(_, c)| c).collect();
1763            for cache in imports {
1764                self.release_dmabuf_import(cache);
1765            }
1766
1767            for (base, len) in &self.pinned_hosts {
1768                if *len > 0 {
1769                    (self.cuda.cuMemHostUnregister)(*base as *mut c_void);
1770                }
1771            }
1772
1773            if !self.blit_semaphore.is_null()
1774                && let Some(destroy) = self.cuda.cuDestroyExternalSemaphore
1775            {
1776                destroy(self.blit_semaphore);
1777            }
1778
1779            if !self.encoder_session.is_null() {
1780                (self.nvenc_funcs.nvEncDestroyEncoder.unwrap())(self.encoder_session);
1781            }
1782
1783            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1784            (self.cuda.cuDevicePrimaryCtxRelease_v2)(self.cuda_device);
1785        }
1786    }
1787}
1788
1789/// Hold one reference on `device`'s primary CUDA context for the life of the process, once a
1790/// session on it has opened. A capture restarted while a game fills the GPU's memory then
1791/// retains the context it had instead of creating one, which can fail there and leave the
1792/// stream on the CPU. The cost is the idle context's 106 MiB of VRAM on a device that has
1793/// encoded once, kept also while no session runs. Its own copy of the driver's entry points
1794/// keeps `libcuda` loaded after the last session's copy goes.
1795fn keep_primary_context(device: CUdevice) {
1796    static KEPT: std::sync::Mutex<Vec<CUdevice>> = std::sync::Mutex::new(Vec::new());
1797    static CUDA: std::sync::OnceLock<Option<CudaFunctions>> = std::sync::OnceLock::new();
1798    let mut kept = KEPT.lock().unwrap_or_else(|e| e.into_inner());
1799    if kept.contains(&device) {
1800        return;
1801    }
1802    let Some(cuda) = CUDA.get_or_init(|| NvencEncoder::load_cuda().ok()) else {
1803        return;
1804    };
1805    let mut context: CUcontext = ptr::null_mut();
1806    if unsafe { (cuda.cuDevicePrimaryCtxRetain)(&mut context, device) } == CUresult::CUDA_SUCCESS {
1807        kept.push(device);
1808    }
1809}
1810
1811/// The level an NVENC session advertises for a `width` x `height` stream at `fps` carrying up
1812/// to `bitrate_bps` (`NV_ENC_LEVEL` shares each codec's own numbering: level_idc for H.264,
1813/// general_level_idc for HEVC, seq_level_idx for AV1).
1814///
1815/// H.264 and HEVC carry the current geometry's level, the lowest a decoder is asked to accept,
1816/// so a hardware decoder that gates on it -- older Apple and Intel fixed-function parts refuse
1817/// a stream whose SPS names a level above their ceiling even for a picture they could hold --
1818/// takes the stream. `reconfigure_resolution` re-declares the level with a forced IDR on every
1819/// resize; the frame rate is floored at `HEADROOM_FPS`, so a live rate change between the
1820/// common rates, which carries no IDR, never moves it. AV1 instead holds the resize headroom's
1821/// level: NVENC validates an AV1 session's level against `maxEncodeWidth` x `maxEncodeHeight`
1822/// at init and refuses one that cannot hold it, and an AV1 hardware decoder is recent enough to
1823/// take that level whatever the picture. The driver holds every codec's level to its bitrate
1824/// ceiling as well, refusing a CBR target past it as an invalid level, so a declared rate
1825/// raises the level to the first that admits it; `hevc_high_tier` names the HEVC tier the
1826/// session declares, whose ceiling is the one that applies. Driver 595.71.05 holds an AV1
1827/// level to two thirds of its Annex A MaxBitrate, `NVENC_AV1_RATE` (5.1 26.7 Mbit/s rather
1828/// than 40, on an RTX 4090 and an L4, whatever the frame rate or buffer), where 595.91.07 and
1829/// 615.71.09 hold Annex A's, so a session asks for the Annex A level first and, refused, for
1830/// the level `level_rate` weighs the rate to (`NVENC_AV1_WEIGHTED`). An HEVC picture is counted
1831/// in whole 32-pixel coding tree blocks, NVENC's, as the driver counts it: 1280x720 at 144 fps
1832/// is inside 4.1 by its own samples, and the driver refuses it as 4.1.
1833fn nvenc_level(
1834    codec: Codec,
1835    width: u32,
1836    height: u32,
1837    fps: u32,
1838    bitrate_bps: u64,
1839    hevc_high_tier: bool,
1840) -> u32 {
1841    let fps = fps.max(HEADROOM_FPS);
1842    match codec {
1843        Codec::Av1 => av1_level(
1844            width.max(HEADROOM_WIDTH),
1845            height.max(HEADROOM_HEIGHT),
1846            fps,
1847            bitrate_bps,
1848        ),
1849        Codec::H265 => h265_level(
1850            width.next_multiple_of(32),
1851            height.next_multiple_of(32),
1852            fps,
1853            bitrate_bps,
1854            hevc_high_tier,
1855        ),
1856        _ => h264_level(width, height, fps, bitrate_bps),
1857    }
1858}
1859
1860impl NvencEncoder {
1861    /// Resolve EGL at runtime rather than link against it, so one binary boots even on hosts
1862    /// without EGL — it is needed only by the zero-copy dmabuf path — and reach the
1863    /// `eglCreateImageKHR` / `eglDestroyImageKHR` entry points through `eglGetProcAddress` because
1864    /// they are KHR *extensions* the base `libEGL` is not obliged to export as plain symbols.
1865    /// Erroring when the library or either extension is missing lets the caller fall back to another
1866    /// encoder instead of crashing at the first dmabuf import.
1867    fn load_egl() -> Result<EglFunctions, String> {
1868        unsafe {
1869            let lib_name = "libEGL.so.1";
1870            let lib = Library::new(lib_name)
1871                .or_else(|_| Library::new("libEGL.so"))
1872                .map_err(|e| format!("Could not load EGL library: {}", e))?;
1873
1874            let get_proc_addr_sym: Symbol<unsafe extern "C" fn(*const c_char) -> *mut c_void> = lib
1875                .get(b"eglGetProcAddress\0")
1876                .map_err(|e| format!("Missing symbol eglGetProcAddress: {}", e))?;
1877
1878            let eglGetProcAddress = *get_proc_addr_sym;
1879
1880            let load_extension = |name: &str| -> Result<*mut c_void, String> {
1881                let c_name = CString::new(name).unwrap();
1882                let addr = eglGetProcAddress(c_name.as_ptr());
1883                if addr.is_null() {
1884                    Err(format!("EGL Extension not found: {}", name))
1885                } else {
1886                    Ok(addr)
1887                }
1888            };
1889
1890            let create_addr = load_extension("eglCreateImageKHR")?;
1891            let destroy_addr = load_extension("eglDestroyImageKHR")?;
1892
1893            Ok(EglFunctions {
1894                _lib: lib,
1895                eglGetProcAddress,
1896                eglCreateImageKHR: std::mem::transmute::<*mut c_void, EglCreateImageKhrFn>(
1897                    create_addr,
1898                ),
1899                eglDestroyImageKHR: std::mem::transmute::<*mut c_void, EglDestroyImageKhrFn>(
1900                    destroy_addr,
1901                ),
1902            })
1903        }
1904    }
1905
1906    /// Resolve the CUDA driver library (`libcuda.so.1`, or `nvcuda.dll` on Windows) at
1907    /// runtime so the crate links against no CUDA SDK and still runs wherever a driver is installed,
1908    /// binding every `cu*` entry point up front so the per-frame hot path is plain indirect calls
1909    /// with no repeated symbol lookups. A missing symbol errors with its name, turning an ABI
1910    /// mismatch into a legible message instead of a later null-pointer call.
1911    fn load_cuda() -> Result<CudaFunctions, String> {
1912        unsafe {
1913            let lib_name = if cfg!(windows) {
1914                "nvcuda.dll"
1915            } else {
1916                "libcuda.so.1"
1917            };
1918            let lib = Library::new(lib_name)
1919                .map_err(|e| format!("Could not load CUDA library ({}): {}", lib_name, e))?;
1920
1921            macro_rules! load {
1922                ($lib:expr, $name:expr) => {
1923                    *$lib.get($name).map_err(|e| {
1924                        format!(
1925                            "Missing symbol {}: {}",
1926                            std::str::from_utf8($name).unwrap(),
1927                            e
1928                        )
1929                    })?
1930                };
1931            }
1932
1933            Ok(CudaFunctions {
1934                cuInit: load!(lib, b"cuInit\0"),
1935                cuDeviceGet: load!(lib, b"cuDeviceGet\0"),
1936                cuDeviceGetByPCIBusId: load!(lib, b"cuDeviceGetByPCIBusId\0"),
1937                cuDevicePrimaryCtxRetain: load!(lib, b"cuDevicePrimaryCtxRetain\0"),
1938                cuCtxPushCurrent_v2: load!(lib, b"cuCtxPushCurrent_v2\0"),
1939                cuCtxPopCurrent_v2: load!(lib, b"cuCtxPopCurrent_v2\0"),
1940                cuDevicePrimaryCtxRelease_v2: load!(lib, b"cuDevicePrimaryCtxRelease_v2\0"),
1941                cuMemAlloc_v2: load!(lib, b"cuMemAlloc_v2\0"),
1942                cuMemAllocPitch_v2: load!(lib, b"cuMemAllocPitch_v2\0"),
1943                cuMemFree_v2: load!(lib, b"cuMemFree_v2\0"),
1944                cuMemcpyHtoD_v2: load!(lib, b"cuMemcpyHtoD_v2\0"),
1945                cuMemcpyDtoH_v2: load!(lib, b"cuMemcpyDtoH_v2\0"),
1946                cuMemcpy2D_v2: load!(lib, b"cuMemcpy2D_v2\0"),
1947                cuMemcpy2DAsync_v2: load!(lib, b"cuMemcpy2DAsync_v2\0"),
1948                cuStreamSynchronize: load!(lib, b"cuStreamSynchronize\0"),
1949                cuModuleLoadData: load!(lib, b"cuModuleLoadData\0"),
1950                cuModuleGetFunction: load!(lib, b"cuModuleGetFunction\0"),
1951                cuModuleUnload: load!(lib, b"cuModuleUnload\0"),
1952                cuTexObjectCreate: load!(lib, b"cuTexObjectCreate\0"),
1953                cuTexObjectDestroy: load!(lib, b"cuTexObjectDestroy\0"),
1954                cuLaunchKernel: load!(lib, b"cuLaunchKernel\0"),
1955                cuMemHostRegister_v2: load!(lib, b"cuMemHostRegister_v2\0"),
1956                cuMemHostUnregister: load!(lib, b"cuMemHostUnregister\0"),
1957                cuGraphicsEGLRegisterImage: load!(lib, b"cuGraphicsEGLRegisterImage\0"),
1958                cuGraphicsUnregisterResource: load!(lib, b"cuGraphicsUnregisterResource\0"),
1959                cuGraphicsResourceGetMappedEglFrame: load!(
1960                    lib,
1961                    b"cuGraphicsResourceGetMappedEglFrame\0"
1962                ),
1963                cuDeviceGetCount: load!(lib, b"cuDeviceGetCount\0"),
1964                cuDeviceGetName: load!(lib, b"cuDeviceGetName\0"),
1965                cuDeviceGetUuid: load!(lib, b"cuDeviceGetUuid\0"),
1966                cuGetErrorName: load!(lib, b"cuGetErrorName\0"),
1967                cuImportExternalSemaphore: lib.get(b"cuImportExternalSemaphore\0").ok().map(|s| *s),
1968                cuWaitExternalSemaphoresAsync: lib
1969                    .get(b"cuWaitExternalSemaphoresAsync\0")
1970                    .ok()
1971                    .map(|s| *s),
1972                cuDestroyExternalSemaphore: lib
1973                    .get(b"cuDestroyExternalSemaphore\0")
1974                    .ok()
1975                    .map(|s| *s),
1976                _lib: lib,
1977            })
1978        }
1979    }
1980
1981    /// Resolve `libnvidia-encode` at runtime for the same reason as CUDA — no SDK to link,
1982    /// runs against whatever driver ships — binding `NvEncodeAPICreateInstance` as the sole entry
1983    /// point every later encode call is reached through. `NvEncodeAPIGetMaxSupportedVersion` is kept
1984    /// optional and bound only when present, because very old drivers lack it; negotiation then falls
1985    /// back to probing `create_instance` acceptance directly rather than failing to load.
1986    fn load_nvenc() -> Result<NvencLibrary, String> {
1987        unsafe {
1988            let lib_name = NVENC_DLL_NAME;
1989            let lib = Library::new(lib_name)
1990                .map_err(|e| format!("Could not load NVENC library ({}): {}", lib_name, e))?;
1991
1992            let create_instance = *lib
1993                .get(NV_ENCODE_API_CREATE_INSTANCE_FN_NAME)
1994                .map_err(|e| e.to_string())?;
1995            let get_max_version = lib
1996                .get::<NvEncodeApiGetMaxSupportedVersionFn>(
1997                    NV_ENCODE_API_GET_MAX_SUPPORTED_VERSION_FN_NAME,
1998                )
1999                .map(|s| *s)
2000                .ok();
2001            Ok(NvencLibrary {
2002                create_instance,
2003                get_max_version,
2004                _lib: lib,
2005            })
2006        }
2007    }
2008
2009    /// Turn a `CUresult` into the driver's own error name via `cuGetErrorName` so a failure
2010    /// logs something diagnosable (e.g. `CUDA_ERROR_OUT_OF_MEMORY`) instead of a bare integer,
2011    /// falling back to the numeric code only when the name is unavailable.
2012    unsafe fn get_error_string(cuda: &CudaFunctions, err: CUresult) -> String {
2013        let mut p_str: *const c_char = ptr::null();
2014        if (cuda.cuGetErrorName)(err, &mut p_str) == CUresult::CUDA_SUCCESS && !p_str.is_null() {
2015            CStr::from_ptr(p_str).to_string_lossy().into_owned()
2016        } else {
2017            format!("Unknown CUDA Error ({})", err.0)
2018        }
2019    }
2020
2021    /// Log the CUDA devices CUDA can enumerate — a debug aid when session init fails to find
2022    /// or bind the expected GPU.
2023    unsafe fn probe_devices(cuda: &CudaFunctions) {
2024        let mut count = 0;
2025        if (cuda.cuDeviceGetCount)(&mut count) != CUresult::CUDA_SUCCESS {
2026            return;
2027        }
2028        crate::log::debug!("[NVENC] Found {} CUDA devices:", count);
2029        for i in 0..count {
2030            let mut dev = 0;
2031            (cuda.cuDeviceGet)(&mut dev, i);
2032            crate::log::debug!(
2033                "[NVENC]   Device {}: {}",
2034                i,
2035                Self::device_name_of(cuda, dev)
2036            );
2037        }
2038    }
2039
2040    /// The name CUDA gives a device, or a placeholder when it will not say.
2041    unsafe fn device_name_of(cuda: &CudaFunctions, dev: CUdevice) -> String {
2042        let mut name_buf = [0 as c_char; 256];
2043        if (cuda.cuDeviceGetName)(name_buf.as_mut_ptr(), 256, dev) != CUresult::CUDA_SUCCESS {
2044            return format!("CUDA device {dev}");
2045        }
2046        CStr::from_ptr(name_buf.as_ptr())
2047            .to_string_lossy()
2048            .into_owned()
2049    }
2050
2051    /// The PCI bus ID of the GPU behind `/dev/dri/renderD<128+index>`, read from the sysfs
2052    /// device symlink, so CUDA can bind to the same physical GPU the capture render node lives on.
2053    fn get_pci_bus_id(render_index: i32) -> Option<String> {
2054        let path = format!("/sys/class/drm/renderD{}/device", 128 + render_index);
2055        if let Ok(target) = std::fs::read_link(&path)
2056            && let Some(name) = target.file_name()
2057            && let Some(name_str) = name.to_str()
2058        {
2059            return Some(name_str.to_string());
2060        }
2061        None
2062    }
2063
2064    /// Build a live NVENC session for the settings' codec: bind CUDA to the target GPU, open and
2065    /// configure the encoder, and allocate its input and output buffers.
2066    ///
2067    /// The sequence:
2068    ///
2069    /// 1. **Load and negotiate**: dlopen EGL / CUDA / NVENC, then `nvenc_negotiate` resolves the API
2070    ///    version against the driver (set-once) before any struct is version-tagged. The multi-GPU
2071    ///    `GET_ATTACHED_IDS` ioctl filter is installed after the NVIDIA libraries are loaded — so
2072    ///    their GOTs can be patched — and before `cuInit` enumerates devices (a no-op unless a host
2073    ///    GPU is hidden from this container). The three library `Arc`s are leaked once per process so
2074    ///    the resolved function pointers stay valid for the program's life.
2075    /// 2. **Bind the device**: `cuInit`, then bind by the render node's PCI bus ID
2076    ///    (`encode_node_index`, with auto `<0` meaning device 0), falling back to CUDA device 0, and
2077    ///    retain the device's primary CUDA context — shared and refcounted across every session on
2078    ///    that device rather than a fresh 100-300 MiB context each — pushing it current.
2079    /// 3. **Allocate input**: a pitched ARGB device buffer (`cuMemAllocPitch`, 16-byte element
2080    ///    alignment) that the chroma convert, or hardware CSC, turns into YUV.
2081    /// 4. **Open the session and query caps**: create the function-list instance, open the session
2082    ///    with the negotiated `apiVersion`, refuse a codec the device lists no engine for, and
2083    ///    query `nvEncGetEncodeCaps` so init degrades rather than fails — a 4:4:4 request on a GPU
2084    ///    or codec without it drops to 4:2:0, and a capture beyond the encoder's max dimensions
2085    ///    returns `Err` so the caller falls back to software. Then pull a preset config (P3,
2086    ///    ultra-low-latency); a failed preset lookup logs the driver's error string and proceeds
2087    ///    with the zeroed default rather than aborting.
2088    /// 5. **Configure the stream** (mutating the returned preset config, whose `version` word is
2089    ///    re-stamped while its embedded `rcParams` keeps the version the preset fill set): the
2090    ///    codec's 4:2:0 or 4:4:4 profile; CBR (two-pass quarter-resolution for tighter per-frame
2091    ///    rate adherence, VBV sizing, optional min/max QP clamps in the codec's quantizer domain)
2092    ///    or ConstQP; infinite GOP (`gopLength` / `idrPeriod` = `0xFFFFFFFF`); `zeroReorderDelay`
2093    ///    and, for H.264, a bitstream-restriction VUI (`max_num_reorder_frames=0`) so no-reorder
2094    ///    decoders don't buffer; an explicit level from `nvenc_level` pinned from frame 1 so the
2095    ///    level never bumps mid-stream; BT.709 primaries and transfer for the sRGB source, at
2096    ///    limited range, with the matrix whichever conversion the session uses produces;
2097    ///    a decoded picture buffer of as many frames as the level admits, so a frame a client
2098    ///    lost can be left out of the predictions (`invalidate_reference`) with earlier frames
2099    ///    still there to predict from;
2100    ///    repeated parameter sets on every key frame; H.264 CABAC; no AUD; one AV1 tile; strict
2101    ///    GOP target; and lookahead disabled for real-time latency.
2102    /// 6. **Initialize with resize headroom**: `maxEncodeWidth` / `maxEncodeHeight` are raised to
2103    ///    at least `HEADROOM_WIDTH` x `HEADROOM_HEIGHT` so `reconfigure_resolution` can grow in
2104    ///    place, but never past the driver's reported maximum; this costs a few hundred MiB of
2105    ///    device memory, so a failed init reopens the session and retries at the exact size
2106    ///    (in-place resize then falls back to a rebuild).
2107    /// 7. **Register, map, and buffer**: register and map the packed input surface (as `ARGB`;
2108    ///    `set_input_format` re-registers it for an RGBA source), and create a
2109    ///    4-deep ring of bitstream output buffers.
2110    ///
2111    /// Every failure after the CUDA allocation unwinds the resources created so far — buffers,
2112    /// session, context — before returning `Err`. EGL is only needed by the zero-copy dmabuf path,
2113    /// so callers on the host-ARGB path pass a null `egl_display`. The retained
2114    /// `init_params.encodeConfig` raw pointer is nulled before the struct is returned (it points at
2115    /// a local `config` about to move); the reconfigure paths repoint it at `self.encode_config`
2116    /// when they resubmit.
2117    pub fn new(settings: &RustCaptureSettings, egl_display: *const c_void) -> Result<Self, String> {
2118        Self::new_tuned(settings, egl_display, NvencTuning::default())
2119    }
2120
2121    /// `new` with the preset, rate-control passes, and adaptive quantization named, for the
2122    /// tuning bench; production sessions take `NvencTuning::default`.
2123    pub(crate) fn new_tuned(
2124        settings: &RustCaptureSettings,
2125        egl_display: *const c_void,
2126        tuning: NvencTuning,
2127    ) -> Result<Self, String> {
2128        let codec = settings.codec;
2129        let codec_guid =
2130            codec_guid(codec).ok_or_else(|| format!("NVENC has no {} encoder", codec.display()))?;
2131        crate::log::debug!("[NVENC] Initializing {}...", codec.display());
2132
2133        let egl = if egl_display.is_null() {
2134            None
2135        } else {
2136            Some(Arc::new(Self::load_egl()?))
2137        };
2138        let cuda = Arc::new(Self::load_cuda()?);
2139        let nvenc_lib = Arc::new(Self::load_nvenc()?);
2140        nvenc_negotiate(&nvenc_lib);
2141
2142        crate::nvgpufilter::install();
2143
2144        static LEAK_ONCE: std::sync::Once = std::sync::Once::new();
2145        LEAK_ONCE.call_once(|| {
2146            std::mem::forget(egl.clone());
2147            std::mem::forget(cuda.clone());
2148            std::mem::forget(nvenc_lib.clone());
2149        });
2150
2151        unsafe {
2152            let res = (cuda.cuInit)(0);
2153            if res != CUresult::CUDA_SUCCESS {
2154                return Err(format!(
2155                    "Init CUDA failed: {}",
2156                    Self::get_error_string(&cuda, res)
2157                ));
2158            }
2159
2160            Self::probe_devices(&cuda);
2161
2162            let mut cu_device: CUdevice = 0;
2163            let mut device_found = false;
2164
2165            if let Some(pci_bus_id) = Self::get_pci_bus_id(settings.encode_node_index.max(0)) {
2166                let c_pci_bus_id = CString::new(pci_bus_id.clone()).unwrap();
2167                if (cuda.cuDeviceGetByPCIBusId)(&mut cu_device, c_pci_bus_id.as_ptr())
2168                    == CUresult::CUDA_SUCCESS
2169                {
2170                    crate::log::debug!(
2171                        "[NVENC] Bound to CUDA device via PCI Bus ID: {}",
2172                        pci_bus_id
2173                    );
2174                    device_found = true;
2175                }
2176            }
2177
2178            if !device_found {
2179                let res = (cuda.cuDeviceGet)(&mut cu_device, 0);
2180                if res != CUresult::CUDA_SUCCESS {
2181                    return Err("Failed to get default CUDA device".into());
2182                }
2183            }
2184            let device_name = Self::device_name_of(&cuda, cu_device);
2185
2186            // One primary context per device, shared and refcounted across every session on that
2187            // device, rather than a fresh 100-300 MiB context each: a second display or a rebuild
2188            // retains the same context instead of allocating another. Retain does not make it
2189            // current, so it is pushed here to run the allocations below and left current for the
2190            // encode paths — matching the current-context state the removed cuCtxCreate produced.
2191            let mut cu_context: CUcontext = ptr::null_mut();
2192            let res = (cuda.cuDevicePrimaryCtxRetain)(&mut cu_context, cu_device);
2193            if res != CUresult::CUDA_SUCCESS {
2194                return Err("Failed to retain the device's primary CUDA context".into());
2195            }
2196            if (cuda.cuCtxPushCurrent_v2)(cu_context) != CUresult::CUDA_SUCCESS {
2197                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2198                return Err("Failed to make the primary CUDA context current".into());
2199            }
2200
2201            let width = settings.width as u32;
2202            let height = settings.height as u32;
2203            let mut input_device_ptr: CUdeviceptr = 0;
2204            let mut input_pitch: usize = 0;
2205
2206            let res = (cuda.cuMemAllocPitch_v2)(
2207                &mut input_device_ptr,
2208                &mut input_pitch,
2209                (width * 4) as usize,
2210                height as usize,
2211                16,
2212            );
2213            if res != CUresult::CUDA_SUCCESS {
2214                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2215                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2216                return Err("Failed to allocate ARGB input buffer on GPU".into());
2217            }
2218
2219            let mut function_list = NV_ENCODE_API_FUNCTION_LIST {
2220                version: sv(NvStruct::FunctionList),
2221                ..Default::default()
2222            };
2223            if (nvenc_lib.create_instance)(&mut function_list) != NVENCSTATUS::NV_ENC_SUCCESS {
2224                (cuda.cuMemFree_v2)(input_device_ptr);
2225                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2226                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2227                return Err("NvEncodeAPICreateInstance failed".into());
2228            }
2229
2230            let mut session_params = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
2231                version: sv(NvStruct::OpenSessionExParams),
2232                deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
2233                device: cu_context as *mut c_void,
2234                apiVersion: neg_api(),
2235                ..Default::default()
2236            };
2237
2238            let mut encoder_session: *mut c_void = ptr::null_mut();
2239            let open_fn = function_list.nvEncOpenEncodeSessionEx.unwrap();
2240            let status = open_fn(&mut session_params, &mut encoder_session);
2241            if status != NVENCSTATUS::NV_ENC_SUCCESS {
2242                (cuda.cuMemFree_v2)(input_device_ptr);
2243                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2244                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2245                return Err(session_refusal(status));
2246            }
2247
2248            // The device's engine list decides whether the codec exists here at all (AV1
2249            // arrived with Ada), and a refusal has to name that rather than fail an init.
2250            if !Self::device_encodes(&function_list, encoder_session, &codec_guid) {
2251                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2252                (cuda.cuMemFree_v2)(input_device_ptr);
2253                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2254                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2255                return Err(format!(
2256                    "this GPU's NVENC has no {} engine",
2257                    codec.display()
2258                ));
2259            }
2260
2261            // Query caps so init degrades instead of failing opaquely: a 4:4:4 request on a GPU
2262            // or codec without it drops to 4:2:0, and a capture beyond the encoder's max
2263            // dimensions declines NVENC so the caller falls back to software.
2264            let caps_444 = if codec.fullcolor() {
2265                query_cap(
2266                    &function_list,
2267                    encoder_session,
2268                    codec_guid,
2269                    NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE,
2270                )
2271            } else {
2272                Some(0)
2273            };
2274            let caps_wmax = query_cap(
2275                &function_list,
2276                encoder_session,
2277                codec_guid,
2278                NV_ENC_CAPS::NV_ENC_CAPS_WIDTH_MAX,
2279            );
2280            let caps_hmax = query_cap(
2281                &function_list,
2282                encoder_session,
2283                codec_guid,
2284                NV_ENC_CAPS::NV_ENC_CAPS_HEIGHT_MAX,
2285            );
2286            let caps = decide_caps(
2287                settings.video_fullcolor,
2288                width as i32,
2289                height as i32,
2290                caps_444,
2291                caps_wmax,
2292                caps_hmax,
2293            );
2294            if let Some((mw, mh)) = caps.too_large {
2295                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2296                (cuda.cuMemFree_v2)(input_device_ptr);
2297                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2298                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2299                return Err(format!(
2300                    "NVENC maximum encode size {mw}x{mh} exceeded by {width}x{height}; using software"
2301                ));
2302            }
2303            if caps.downgraded_color {
2304                eprintln!(
2305                    "[NVENC] {} 4:4:4 (YUV444) encoding unsupported on this GPU; encoding 4:2:0.",
2306                    codec.display()
2307                );
2308            }
2309            if tuning.temporal_aq
2310                && query_cap(
2311                    &function_list,
2312                    encoder_session,
2313                    codec_guid,
2314                    NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_TEMPORAL_AQ,
2315                ) == Some(0)
2316            {
2317                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2318                (cuda.cuMemFree_v2)(input_device_ptr);
2319                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2320                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2321                return Err(format!(
2322                    "{} temporal AQ is not supported by this GPU",
2323                    codec.display()
2324                ));
2325            }
2326
2327            let is_444 = caps.fullcolor;
2328            let bit_depth = if settings.video_bit_depth >= 10
2329                && codes_ten_bit(&function_list, encoder_session, codec, codec_guid)
2330            {
2331                10
2332            } else {
2333                8
2334            };
2335            let invalidation = query_cap(
2336                &function_list,
2337                encoder_session,
2338                codec_guid,
2339                NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_REF_PIC_INVALIDATION,
2340            ) == Some(1);
2341
2342            let mut config = NV_ENC_CONFIG {
2343                version: sv(NvStruct::Config),
2344                ..Default::default()
2345            };
2346            let mut preset_config = NV_ENC_PRESET_CONFIG {
2347                version: sv(NvStruct::PresetConfig),
2348                presetCfg: config,
2349                ..Default::default()
2350            };
2351
2352            let get_preset_ex = function_list.nvEncGetEncodePresetConfigEx.unwrap();
2353            let preset_status = get_preset_ex(
2354                encoder_session,
2355                codec_guid,
2356                tuning.preset,
2357                NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
2358                &mut preset_config,
2359            );
2360            if preset_status != NVENCSTATUS::NV_ENC_SUCCESS {
2361                eprintln!(
2362                    "[NVENC] nvEncGetEncodePresetConfigEx failed ({preset_status:?}): {}",
2363                    last_error(&function_list, encoder_session)
2364                );
2365            }
2366
2367            config = preset_config.presetCfg;
2368            config.version = sv(NvStruct::Config);
2369            config.profileGUID = profile_guid(codec, is_444, bit_depth);
2370            if settings.video_cbr_mode {
2371                let bps = cbr_bps(settings);
2372                log_held_rate(settings, bps);
2373                config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
2374                config.rcParams.multiPass = tuning.multipass;
2375                set_cbr_rate(&mut config.rcParams, bps, cbr_vbv(settings, bps));
2376                let lo = codec.hardware_quantizer_bound(Hardware::Nvenc, settings.video_min_qp);
2377                if lo > 0 {
2378                    config.rcParams.set_enableMinQP(1);
2379                    config.rcParams.minQP.qpInterP = lo;
2380                    config.rcParams.minQP.qpInterB = lo;
2381                    config.rcParams.minQP.qpIntra = lo;
2382                }
2383                let hi = codec.hardware_quantizer_bound(Hardware::Nvenc, settings.video_max_qp);
2384                if hi > 0 {
2385                    config.rcParams.set_enableMaxQP(1);
2386                    config.rcParams.maxQP.qpInterP = hi;
2387                    config.rcParams.maxQP.qpInterB = hi;
2388                    config.rcParams.maxQP.qpIntra = hi;
2389                }
2390            } else {
2391                let q = codec.hardware_quantizer(Hardware::Nvenc, settings.video_crf);
2392                config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
2393                config.rcParams.constQP.qpInterP = q;
2394                config.rcParams.constQP.qpInterB = q;
2395                config.rcParams.constQP.qpIntra = q;
2396            }
2397            config.rcParams.set_enableAQ(tuning.spatial_aq as u32);
2398            config
2399                .rcParams
2400                .set_enableTemporalAQ(tuning.temporal_aq as u32);
2401            config.frameIntervalP = 1;
2402            config.gopLength = 0xFFFFFFFF;
2403            config.rcParams.set_zeroReorderDelay(1);
2404            config.rcParams.set_strictGOPTarget(1);
2405            config.rcParams.set_enableLookahead(0);
2406            config.rcParams.lookaheadDepth = 0;
2407            let rate = FrameRate::of(settings.target_fps);
2408            let level_at = |weighted| {
2409                nvenc_level(
2410                    codec,
2411                    width,
2412                    height,
2413                    rate.ceil(),
2414                    level_rate(codec, config.rcParams.maxBitRate as u64, weighted),
2415                    tuning.hevc_high_tier,
2416                )
2417            };
2418            let level = level_at(NVENC_AV1_WEIGHTED.load(Ordering::Relaxed));
2419            let weighted_level = level_at(true);
2420            let dpb = match codec {
2421                Codec::H265 => h265_dpb_frames(level, width, height),
2422                Codec::Av1 => AV1_REFERENCES,
2423                _ => h264_dpb_frames(level, width, height),
2424            };
2425            let dpb = if codec == Codec::Av1 {
2426                dpb
2427            } else {
2428                dpb.min(crate::encoders::reference_frames(settings))
2429            };
2430            let ltr = query_cap(
2431                &function_list,
2432                encoder_session,
2433                codec_guid,
2434                NV_ENC_CAPS::NV_ENC_CAPS_NUM_MAX_LTR_FRAMES,
2435            );
2436            let anchors = anchor_count(
2437                codec,
2438                nvenc_cur_ver().0,
2439                invalidation,
2440                ltr,
2441                dpb,
2442                settings.acknowledge_references,
2443            );
2444            Self::configure_codec(
2445                &mut config,
2446                codec,
2447                is_444,
2448                bit_depth,
2449                level,
2450                dpb,
2451                anchors,
2452                &tuning,
2453            );
2454            #[cfg(test)]
2455            if let Some(refs) = tuning.ref_l0 {
2456                match codec {
2457                    Codec::H265 => config.encodeCodecConfig.hevcConfig.numRefL0 = refs,
2458                    Codec::Av1 => config.encodeCodecConfig.av1Config.numFwdRefs = refs,
2459                    _ => config.encodeCodecConfig.h264Config.numRefL0 = refs,
2460                }
2461            }
2462            let engines = query_cap(
2463                &function_list,
2464                encoder_session,
2465                codec_guid,
2466                NV_ENC_CAPS::NV_ENC_CAPS_NUM_ENCODER_ENGINES,
2467            );
2468            let split = split_mode(codec, width, height, engines, nvenc_cur_ver());
2469            #[cfg(test)]
2470            let split = tuning.split.unwrap_or(split);
2471
2472            let mut init_params = NV_ENC_INITIALIZE_PARAMS {
2473                version: sv(NvStruct::InitializeParams),
2474                encodeGUID: codec_guid,
2475                presetGUID: tuning.preset,
2476                tuningInfo: NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
2477                encodeWidth: width,
2478                encodeHeight: height,
2479                darWidth: width,
2480                darHeight: height,
2481                frameRateNum: rate.num,
2482                frameRateDen: rate.den,
2483                enablePTD: 1,
2484                encodeConfig: &mut config,
2485                maxEncodeWidth: nvenc_headroom(width, HEADROOM_WIDTH, caps_wmax),
2486                maxEncodeHeight: nvenc_headroom(height, HEADROOM_HEIGHT, caps_hmax),
2487                ..Default::default()
2488            };
2489            init_params.set_splitEncodeMode(split as u32);
2490
2491            let init_fn = function_list.nvEncInitializeEncoder.unwrap();
2492            let mut headroom_status =
2493                init_fn(encoder_session, &mut Negotiated::new(init_params).value);
2494            if headroom_status != NVENCSTATUS::NV_ENC_SUCCESS && weighted_level != level {
2495                // A driver holding AV1 to `NVENC_AV1_RATE` refuses the Annex A level as invalid,
2496                // and a refused encoder stays refused: the weighted level opens a session of its
2497                // own.
2498                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2499                encoder_session = ptr::null_mut();
2500                if open_fn(&mut session_params, &mut encoder_session) != NVENCSTATUS::NV_ENC_SUCCESS
2501                {
2502                    (cuda.cuMemFree_v2)(input_device_ptr);
2503                    (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2504                    (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2505                    return Err("Failed to reopen the AV1 session at the weighted level".into());
2506                }
2507                config.encodeCodecConfig.av1Config.level = weighted_level;
2508                init_params.encodeConfig = &mut config;
2509                headroom_status = init_fn(encoder_session, &mut Negotiated::new(init_params).value);
2510                if headroom_status == NVENCSTATUS::NV_ENC_SUCCESS {
2511                    NVENC_AV1_WEIGHTED.store(true, Ordering::Relaxed);
2512                    eprintln!(
2513                        "[NVENC] The driver refused AV1 level {level} at {} kbps; declaring {weighted_level}",
2514                        config.rcParams.maxBitRate / 1000
2515                    );
2516                }
2517            }
2518            if headroom_status != NVENCSTATUS::NV_ENC_SUCCESS {
2519                eprintln!(
2520                    "[NVENC] Init with {}x{} resize headroom failed ({headroom_status:?}): {}",
2521                    init_params.maxEncodeWidth,
2522                    init_params.maxEncodeHeight,
2523                    last_error(&function_list, encoder_session)
2524                );
2525                // An encoder the driver refused to initialize stays refused, so the retry at the
2526                // exact capture size needs a session of its own rather than this one.
2527                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2528                encoder_session = ptr::null_mut();
2529                let reopened = open_fn(&mut session_params, &mut encoder_session)
2530                    == NVENCSTATUS::NV_ENC_SUCCESS;
2531                init_params.maxEncodeWidth = width;
2532                init_params.maxEncodeHeight = height;
2533                init_params.encodeConfig = &mut config;
2534                let exact_status = if reopened {
2535                    init_fn(encoder_session, &mut Negotiated::new(init_params).value)
2536                } else {
2537                    NVENCSTATUS::NV_ENC_ERR_NO_ENCODE_DEVICE
2538                };
2539                if exact_status != NVENCSTATUS::NV_ENC_SUCCESS {
2540                    let detail = if reopened {
2541                        let d = last_error(&function_list, encoder_session);
2542                        (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2543                        d
2544                    } else {
2545                        "could not reopen the session".to_string()
2546                    };
2547                    (cuda.cuMemFree_v2)(input_device_ptr);
2548                    (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2549                    (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2550                    return Err(format!(
2551                        "Failed to initialize {} encoder at {width}x{height} ({exact_status:?}): {detail}",
2552                        codec.display()
2553                    ));
2554                }
2555                eprintln!("[NVENC] Running without resize headroom.");
2556            }
2557
2558            init_params.encodeConfig = ptr::null_mut();
2559
2560            let mut reg_res = NV_ENC_REGISTER_RESOURCE {
2561                version: sv(NvStruct::RegisterResource),
2562                resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
2563                width,
2564                height,
2565                resourceToRegister: input_device_ptr as *mut c_void,
2566                pitch: input_pitch as u32,
2567                bufferFormat: NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB,
2568                bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
2569                ..Default::default()
2570            };
2571
2572            let register_fn = function_list.nvEncRegisterResource.unwrap();
2573            if register_fn(encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
2574                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2575                (cuda.cuMemFree_v2)(input_device_ptr);
2576                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2577                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2578                return Err("Failed to register input buffer".into());
2579            }
2580
2581            let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
2582                version: sv(NvStruct::MapInputResource),
2583                registeredResource: reg_res.registeredResource,
2584                ..Default::default()
2585            };
2586            let map_fn = function_list.nvEncMapInputResource.unwrap();
2587            if map_fn(encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
2588                (function_list.nvEncUnregisterResource.unwrap())(
2589                    encoder_session,
2590                    reg_res.registeredResource,
2591                );
2592                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2593                (cuda.cuMemFree_v2)(input_device_ptr);
2594                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2595                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2596                return Err("Failed to map input buffer".into());
2597            }
2598
2599            let mut bitstream_buffers = Vec::new();
2600            let create_bs_fn = function_list.nvEncCreateBitstreamBuffer.unwrap();
2601            for _ in 0..BITSTREAM_BUFFERS {
2602                let mut bitstream_params = NV_ENC_CREATE_BITSTREAM_BUFFER {
2603                    version: sv(NvStruct::CreateBitstreamBuffer),
2604                    ..Default::default()
2605                };
2606                if create_bs_fn(encoder_session, &mut bitstream_params)
2607                    != NVENCSTATUS::NV_ENC_SUCCESS
2608                {
2609                    for &bs in &bitstream_buffers {
2610                        (function_list.nvEncDestroyBitstreamBuffer.unwrap())(encoder_session, bs);
2611                    }
2612                    (function_list.nvEncUnmapInputResource.unwrap())(
2613                        encoder_session,
2614                        map_params.mappedResource,
2615                    );
2616                    (function_list.nvEncUnregisterResource.unwrap())(
2617                        encoder_session,
2618                        reg_res.registeredResource,
2619                    );
2620                    (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2621                    (cuda.cuMemFree_v2)(input_device_ptr);
2622                    (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2623                    (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2624                    return Err("Failed to create bitstream buffer".into());
2625                }
2626                bitstream_buffers.push(bitstream_params.bitstreamBuffer);
2627            }
2628
2629            // 8-bit 4:4:4 subsamples nothing and needs no kernel; a driver that refuses the
2630            // 4:2:0 one keeps NVENC's own conversion, which follows the declared matrix either
2631            // way. A 10-bit session was initialized for the 10-bit surface only the kernel
2632            // writes.
2633            let layout = ConvertLayout::of(is_444, bit_depth);
2634            #[cfg(test)]
2635            let layout = layout.filter(|_| !tuning.hardware_csc);
2636            let csc = layout.and_then(|layout| {
2637                ChromaConvert::new(
2638                    &cuda,
2639                    &function_list,
2640                    encoder_session,
2641                    width,
2642                    height,
2643                    layout,
2644                )
2645            });
2646            if bit_depth > 8 && csc.is_none() {
2647                for buffer in &bitstream_buffers {
2648                    (function_list.nvEncDestroyBitstreamBuffer.unwrap())(encoder_session, *buffer);
2649                }
2650                (function_list.nvEncUnmapInputResource.unwrap())(
2651                    encoder_session,
2652                    map_params.mappedResource,
2653                );
2654                (function_list.nvEncUnregisterResource.unwrap())(
2655                    encoder_session,
2656                    reg_res.registeredResource,
2657                );
2658                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2659                (cuda.cuMemFree_v2)(input_device_ptr);
2660                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2661                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2662                return Err("the driver took no 10-bit convert kernel".into());
2663            }
2664            keep_primary_context(cu_device);
2665            crate::log::debug!(
2666                "[NVENC] {} initialized (4:4:4 mode: {}, chroma convert: {}).",
2667                codec.display(),
2668                is_444,
2669                if csc.is_some() { "kernel" } else { "hardware" }
2670            );
2671
2672            Ok(Self {
2673                encoder_session,
2674                cuda_context: cu_context,
2675                cuda_device: cu_device,
2676                device_name,
2677                egl_display: egl_display as EGLDisplay,
2678                codec,
2679                fullcolor: is_444,
2680                bit_depth,
2681                width,
2682                height,
2683                current_qp: codec.hardware_quantizer(Hardware::Nvenc, settings.video_crf),
2684                held_qp: None,
2685                held_band: None,
2686                hold_refused: false,
2687                held_bytes: 0,
2688                qp_map: Vec::new(),
2689                last_quality: None,
2690                last_bytes: None,
2691                encode_config: config,
2692                init_params,
2693                input_device_ptr,
2694                input_pitch,
2695                input_format: NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB,
2696                registered_input_resource: reg_res.registeredResource,
2697                mapped_input_buffer: map_params.mappedResource,
2698                bitstream_buffers,
2699                current_buffer_idx: 0,
2700                dmabuf_cache: HashMap::new(),
2701                external_input: None,
2702                pinned_hosts: HashMap::new(),
2703                cuda,
2704                egl,
2705                _nvenc_lib: nvenc_lib,
2706                nvenc_funcs: function_list,
2707                omit_stripe_headers: settings.omit_stripe_headers,
2708                node_index: settings.encode_node_index.max(0),
2709                pin_uploads: std::env::var("PIXELFLUX_NVENC_PIN").as_deref() != Ok("0"),
2710                direct_dmabuf: std::env::var("PIXELFLUX_NVENC_DIRECT").as_deref() != Ok("0"),
2711                csc,
2712                dpb,
2713                engines,
2714                references: invalidation.then(|| {
2715                    if anchors > 0 {
2716                        let mut w =
2717                            ReferenceWindow::with_anchors(window_frames(codec, dpb, true), anchors);
2718                        if settings.acknowledge_references {
2719                            w.set_acknowledged();
2720                        }
2721                        if codec == Codec::Av1 {
2722                            w.lend_free_anchors();
2723                        }
2724                        w
2725                    } else {
2726                        ReferenceWindow::new(dpb)
2727                    }
2728                }),
2729                last_reference: Reference::Untracked,
2730                frame_num: WideFrameNum::default(),
2731                blit_semaphore: ptr::null_mut(),
2732                band_hash: None,
2733                upload_hash: None,
2734                upload_words: None,
2735                upload_hashes: None,
2736            })
2737        }
2738    }
2739
2740    /// Whether the open session's device lists an encode engine for `codec`.
2741    unsafe fn device_encodes(
2742        funcs: &NV_ENCODE_API_FUNCTION_LIST,
2743        session: *mut c_void,
2744        codec: &GUID,
2745    ) -> bool {
2746        let (Some(count_fn), Some(list_fn)) =
2747            (funcs.nvEncGetEncodeGUIDCount, funcs.nvEncGetEncodeGUIDs)
2748        else {
2749            return true;
2750        };
2751        let mut count = 0u32;
2752        if count_fn(session, &mut count) != NVENCSTATUS::NV_ENC_SUCCESS || count == 0 {
2753            return false;
2754        }
2755        let mut guids = vec![GUID::default(); count as usize];
2756        let mut listed = 0u32;
2757        if list_fn(session, guids.as_mut_ptr(), count, &mut listed) != NVENCSTATUS::NV_ENC_SUCCESS {
2758            return false;
2759        }
2760        guids
2761            .iter()
2762            .take(listed as usize)
2763            .any(|g| guid_eq(g, codec))
2764    }
2765
2766    /// Program the codec-specific arm of `config`: `level`, an infinite IDR period, the chroma
2767    /// format, 8-bit input and output, parameter sets repeated on every key frame, and the
2768    /// color description every session converts with.
2769    ///
2770    /// The whole description is BT.709 at limited range. Primaries and transfer describe the
2771    /// source, sRGB desktop pixels, which shares both with BT.709; the matrix is what
2772    /// `ChromaConvert` produces, and NVENC's own conversion — the fallback, and the 4:4:4
2773    /// sessions — follows the matrix declared here at the limited range it emits, which
2774    /// `gpu_hardware_conversion_matches_the_declared_matrix` holds it to. H.264 additionally
2775    /// restricts reordering in its VUI so no-reorder decoders don't buffer, and codes CABAC.
2776    /// H.264 and HEVC frames carry `SLICES_PER_FRAME` slices; AV1 asks for one tile, since tiles
2777    /// cost bitrate and buy no quality, and tier 0, the only tier NVENC takes for it. The driver
2778    /// honors a 1x1 request below 1986 pixels of picture height; above that it forces a second
2779    /// tile row whatever is asked for, in the low-latency presets this one is among but not from
2780    /// P5 up, so a 4K AV1 session codes two tile rows.
2781    ///
2782    /// HEVC declares the tier `h265_tier` names for its level, High: NVENC validates a CBR target
2783    /// against the MaxBR of the pinned level, and the Main-tier ceiling of the 5.x levels
2784    /// (40 Mbit/s at 5.1) is one a 4K desktop session reaches, where a Main-tier open is refused
2785    /// and a live rate change past it is declined. The tier is a signaled cap, not a coding
2786    /// tool; it does change the codec string a client derives from the SPS (`H153` for `L153`).
2787    #[allow(clippy::too_many_arguments)]
2788    fn configure_codec(
2789        config: &mut NV_ENC_CONFIG,
2790        codec: Codec,
2791        fullcolor: bool,
2792        bit_depth: u32,
2793        level: u32,
2794        dpb: u32,
2795        anchors: usize,
2796        tuning: &NvencTuning,
2797    ) {
2798        let depth = if bit_depth > 8 {
2799            NV_ENC_BIT_DEPTH::NV_ENC_BIT_DEPTH_10
2800        } else {
2801            NV_ENC_BIT_DEPTH::NV_ENC_BIT_DEPTH_8
2802        };
2803        let primaries = NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709;
2804        let transfer = NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709;
2805        let matrix = NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709;
2806        let vui = |vui: &mut NV_ENC_CONFIG_H264_VUI_PARAMETERS| {
2807            vui.videoSignalTypePresentFlag = 1;
2808            vui.videoFormat = NV_ENC_VUI_VIDEO_FORMAT::NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED;
2809            vui.colourDescriptionPresentFlag = 1;
2810            vui.colourPrimaries = primaries;
2811            vui.transferCharacteristics = transfer;
2812            vui.colourMatrix = matrix;
2813            vui.videoFullRangeFlag = 0;
2814        };
2815        unsafe {
2816            match codec {
2817                Codec::H265 => {
2818                    let c = &mut config.encodeCodecConfig.hevcConfig;
2819                    c.level = level;
2820                    c.maxNumRefFramesInDPB = dpb;
2821                    if anchors > 0 {
2822                        c.set_enableLTR(1);
2823                        c.ltrTrustMode = 0;
2824                        c.ltrNumFrames = anchors as u32;
2825                    }
2826                    c.tier = if tuning.hevc_high_tier {
2827                        h265_tier(level)
2828                    } else {
2829                        0
2830                    };
2831                    c.sliceMode = SLICE_MODE_COUNT;
2832                    c.sliceModeData = tuning.slices;
2833                    c.idrPeriod = 0xFFFFFFFF;
2834                    c.set_chromaFormatIDC(if fullcolor { 3 } else { 1 });
2835                    c.inputBitDepth = depth;
2836                    c.outputBitDepth = depth;
2837                    c.set_repeatSPSPPS(1);
2838                    c.set_outputAUD(0);
2839                    vui(&mut c.hevcVUIParameters);
2840                }
2841                Codec::Av1 => {
2842                    let c = &mut config.encodeCodecConfig.av1Config;
2843                    c.level = level;
2844                    c.tier = 0;
2845                    c.idrPeriod = 0xFFFFFFFF;
2846                    c.set_chromaFormatIDC(1);
2847                    c.inputBitDepth = depth;
2848                    c.outputBitDepth = depth;
2849                    c.maxNumRefFramesInDPB = dpb;
2850                    if anchors > 0 {
2851                        c.set_enableLTR(1);
2852                        c.ltrNumFrames = anchors as u32;
2853                    }
2854                    c.set_repeatSeqHdr(1);
2855                    c.set_outputAnnexBFormat(0);
2856                    c.set_enableBitstreamPadding(0);
2857                    c.numTileColumns = 1;
2858                    c.numTileRows = 1;
2859                    c.colorPrimaries = primaries;
2860                    c.transferCharacteristics = transfer;
2861                    c.matrixCoefficients = matrix;
2862                    c.colorRange = 0;
2863                }
2864                _ => {
2865                    let c = &mut config.encodeCodecConfig.h264Config;
2866                    c.level = level;
2867                    c.maxNumRefFrames = dpb;
2868                    if anchors > 0 {
2869                        c.set_enableLTR(1);
2870                        c.ltrTrustMode = 0;
2871                        c.ltrNumFrames = anchors as u32;
2872                    }
2873                    c.sliceMode = SLICE_MODE_COUNT;
2874                    c.sliceModeData = tuning.slices;
2875                    c.idrPeriod = 0xFFFFFFFF;
2876                    c.chromaFormatIDC = if fullcolor { 3 } else { 1 };
2877                    c.set_repeatSPSPPS(1);
2878                    c.entropyCodingMode =
2879                        NV_ENC_H264_ENTROPY_CODING_MODE::NV_ENC_H264_ENTROPY_CODING_MODE_CABAC;
2880                    c.set_outputAUD(0);
2881                    c.h264VUIParameters.bitstreamRestrictionFlag = 1;
2882                    vui(&mut c.h264VUIParameters);
2883                }
2884            }
2885        }
2886    }
2887
2888    /// The level the live config declares, read from its codec arm.
2889    fn declared_level(&self) -> u32 {
2890        let config = &self.encode_config.encodeCodecConfig;
2891        unsafe {
2892            match self.codec {
2893                Codec::H265 => config.hevcConfig.level,
2894                Codec::Av1 => config.av1Config.level,
2895                _ => config.h264Config.level,
2896            }
2897        }
2898    }
2899
2900    /// The level a `width` x `height` stream at `fps` takes at the live config's peak bitrate
2901    /// and HEVC tier, and the driver's AV1 share (`NVENC_AV1_WEIGHTED`).
2902    fn level_for(&self, width: u32, height: u32, fps: u32) -> u32 {
2903        let high_tier = unsafe { self.encode_config.encodeCodecConfig.hevcConfig.tier == 1 };
2904        nvenc_level(
2905            self.codec,
2906            width,
2907            height,
2908            fps,
2909            level_rate(
2910                self.codec,
2911                self.encode_config.rcParams.maxBitRate as u64,
2912                NVENC_AV1_WEIGHTED.load(Ordering::Relaxed),
2913            ),
2914            high_tier,
2915        )
2916    }
2917
2918    /// Write the level for a new geometry or frame rate into the live config's codec arm.
2919    fn set_level(&mut self, width: u32, height: u32, fps: u32) {
2920        let level = self.level_for(width, height, fps);
2921        match self.codec {
2922            Codec::H265 => self.encode_config.encodeCodecConfig.hevcConfig.level = level,
2923            Codec::Av1 => self.encode_config.encodeCodecConfig.av1Config.level = level,
2924            _ => self.encode_config.encodeCodecConfig.h264Config.level = level,
2925        }
2926    }
2927
2928    /// The decoded picture buffer, in frames, the level a `width` x `height` stream at `fps`
2929    /// takes admits for this codec.
2930    fn dpb_frames_at(&self, width: u32, height: u32, fps: u32) -> u32 {
2931        let level = self.level_for(width, height, fps);
2932        match self.codec {
2933            Codec::H265 => h265_dpb_frames(level, width, height),
2934            Codec::Av1 => AV1_REFERENCES,
2935            _ => h264_dpb_frames(level, width, height),
2936        }
2937    }
2938
2939    /// The frame the last encoded frame predicted from.
2940    pub fn last_reference(&self) -> Reference {
2941        self.last_reference
2942    }
2943
2944    /// Leave frame `frame_id` and every frame after it out of the predictions. False when the
2945    /// device cannot or refuses, and the caller codes a key frame instead.
2946    pub fn invalidate_reference(&mut self, frame_id: u16) -> bool {
2947        let Some(references) = &mut self.references else {
2948            return false;
2949        };
2950        match references.invalidate(frame_id) {
2951            Invalidation::Forget(pts) => unsafe {
2952                (self.nvenc_funcs.nvEncInvalidateRefFrames.unwrap())(self.encoder_session, pts)
2953                    == NVENCSTATUS::NV_ENC_SUCCESS
2954            },
2955            Invalidation::KeyFrame | Invalidation::Ignored => true,
2956        }
2957    }
2958
2959    /// Every consumer holds frame `frame_id`, or where not `held` was sent it
2960    /// (`ReferenceWindow::acknowledge`).
2961    pub fn acknowledge_reference(&mut self, frame_id: u16, held: bool) {
2962        if let Some(references) = &mut self.references {
2963            references.acknowledge(frame_id, held);
2964        }
2965    }
2966
2967    /// Apply what the consumers say of a frame (`FrameEncoder::take_report`); false where a key
2968    /// frame is coded instead.
2969    pub fn take_report(&mut self, report: ReferenceReport) -> bool {
2970        match report {
2971            ReferenceReport::Lost(frame_id) => self.invalidate_reference(frame_id),
2972            ReferenceReport::Held(frame_id) => {
2973                self.acknowledge_reference(frame_id, true);
2974                true
2975            }
2976            ReferenceReport::Sent(frame_id) => {
2977                self.acknowledge_reference(frame_id, false);
2978                true
2979            }
2980        }
2981    }
2982
2983    /// Leave the frame just encoded out of the predictions as a frame the client lost is, for a
2984    /// frame coded again in its place (`ReferenceWindow::retract`); false where the window or
2985    /// the device cannot.
2986    fn retract_last(&mut self) -> bool {
2987        let (funcs, session) = (&self.nvenc_funcs, self.encoder_session);
2988        self.references.as_mut().is_some_and(|r| {
2989            r.retract(|pts| unsafe {
2990                (funcs.nvEncInvalidateRefFrames.unwrap())(session, pts)
2991                    == NVENCSTATUS::NV_ENC_SUCCESS
2992            })
2993        })
2994    }
2995
2996    /// How the session splits a frame across the device's encode engines (`split_mode`), for the
2997    /// line that says which device encodes.
2998    pub fn split_summary(&self) -> String {
2999        let forced = [
3000            NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_AUTO_FORCED_MODE,
3001            NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_THREE_FORCED_MODE,
3002        ]
3003        .map(|m| m as u32)
3004        .contains(&self.init_params.splitEncodeMode());
3005        match self.engines {
3006            Some(n) if n > 1 && forced => format!("split across {n} engines"),
3007            Some(n) if n > 1 && self.codec != Codec::H264 => {
3008                format!("split as the driver picks, {n} engines")
3009            }
3010            Some(n) => format!("{n} engine{}", if n == 1 { "" } else { "s" }),
3011            None => "engines unreported".into(),
3012        }
3013    }
3014
3015    /// The codec the session emits.
3016    /// The name CUDA gives the GPU this session encodes on.
3017    pub fn device_name(&self) -> &str {
3018        &self.device_name
3019    }
3020
3021    pub fn codec(&self) -> Codec {
3022        self.codec
3023    }
3024
3025    /// Every NVENC session converts to and declares limited range.
3026    pub fn is_full_range(&self) -> bool {
3027        false
3028    }
3029
3030    /// Whether the session negotiated 4:4:4 chroma.
3031    pub fn is_fullcolor(&self) -> bool {
3032        self.fullcolor
3033    }
3034
3035    /// The bits per sample the session codes.
3036    pub fn bit_depth(&self) -> u32 {
3037        self.bit_depth
3038    }
3039
3040    /// Follow a capture restart on the live session, folding in the current rate / QP / fps,
3041    /// without tearing it down.
3042    ///
3043    /// The NVENC session, CUDA context, and bitstream buffers survive, so a restart costs a few
3044    /// milliseconds instead of a full rebuild. Flow:
3045    ///
3046    /// 1. **Reject the unchangeable**: a different encode device or codec, a chroma-format flip
3047    ///    (4:4:4), an RC-mode flip, or dimensions of zero or beyond the init-time `maxEncode`
3048    ///    headroom all return `Err` so the caller rebuilds.
3049    /// 2. **Keep the stream at unchanged dimensions**: the reference chain, the input surface, and
3050    ///    the dmabuf imports stay as they are, so the restart costs no IDR and no reset. Only the
3051    ///    pinned hosts are dropped -- the restart recreates the source buffers, often at the same
3052    ///    addresses -- and the rate, frame rate, and wire framing the restart carries are folded
3053    ///    in, as `reconfigure_rate` does. Returns `Ok(false)`.
3054    /// 3. **Release geometry-dependent state** under the pushed CUDA context: unmap / unregister /
3055    ///    free the packed input surface, every cached dmabuf import (with the NVENC registration a
3056    ///    direct import holds), and every pinned host. The dmabuf imports are re-created lazily by
3057    ///    the encode path; pinned hosts are dropped because the source shm segments are recreated
3058    ///    on resize and may reuse the same base addresses.
3059    /// 4. **Reconfigure the session**: update the level for the new size, the decoded picture
3060    ///    buffer where that level admits fewer frames (the driver takes a smaller one at the
3061    ///    forced IDR and refuses to raise it again), the CBR bitrate + VBV or the ConstQP, and the
3062    ///    new dimensions / DAR / frame rate, then `NvEncReconfigureEncoder` with `resetEncoder`
3063    ///    and `forceIDR` so the stream restarts cleanly at the new size. Driver rejection returns
3064    ///    `Err`.
3065    /// 5. **Reallocate the packed input** at the new size and register + map it as init does, in
3066    ///    the byte order the session was last fed.
3067    ///
3068    /// On a resize the next encoded frame is a reset-RC IDR and `Ok(true)` is returned.
3069    pub fn reconfigure_resolution(
3070        &mut self,
3071        settings: &RustCaptureSettings,
3072    ) -> Result<bool, String> {
3073        let new_w = settings.width as u32;
3074        let new_h = settings.height as u32;
3075        let is_cbr = self.encode_config.rcParams.rateControlMode
3076            == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
3077        if settings.encode_node_index.max(0) != self.node_index {
3078            return Err("encode device changed".into());
3079        }
3080        if settings.codec != self.codec {
3081            return Err("codec changed".into());
3082        }
3083        if (settings.video_fullcolor && self.codec.fullcolor()) != self.fullcolor {
3084            return Err("chroma format changed".into());
3085        }
3086        if (settings.video_bit_depth >= 10) != (self.bit_depth > 8)
3087            && matches!(self.codec, Codec::H265 | Codec::Av1)
3088        {
3089            return Err("bit depth changed".into());
3090        }
3091        if settings.video_cbr_mode != is_cbr {
3092            return Err("rate-control mode changed".into());
3093        }
3094        if new_w == 0
3095            || new_h == 0
3096            || new_w > self.init_params.maxEncodeWidth
3097            || new_h > self.init_params.maxEncodeHeight
3098        {
3099            return Err(format!(
3100                "{}x{} outside reconfigure headroom {}x{}",
3101                new_w, new_h, self.init_params.maxEncodeWidth, self.init_params.maxEncodeHeight
3102            ));
3103        }
3104        if (new_w, new_h) == (self.width, self.height) {
3105            self.release_pinned_hosts();
3106            self.reconfigure_rate(settings);
3107            self.omit_stripe_headers = settings.omit_stripe_headers;
3108            return Ok(false);
3109        }
3110        let rate = FrameRate::of(settings.target_fps);
3111        let dpb = self.dpb_frames_at(new_w, new_h, rate.ceil()).min(self.dpb);
3112
3113        unsafe {
3114            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
3115            self.unmap_external_input();
3116            if let Some(csc) = self.csc.take() {
3117                csc.release(&self.cuda, &self.nvenc_funcs, self.encoder_session);
3118            }
3119            if !self.mapped_input_buffer.is_null() {
3120                (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
3121                    self.encoder_session,
3122                    self.mapped_input_buffer,
3123                );
3124                self.mapped_input_buffer = ptr::null_mut();
3125            }
3126            if !self.registered_input_resource.is_null() {
3127                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3128                    self.encoder_session,
3129                    self.registered_input_resource,
3130                );
3131                self.registered_input_resource = ptr::null_mut();
3132            }
3133            if self.input_device_ptr != 0 {
3134                (self.cuda.cuMemFree_v2)(self.input_device_ptr);
3135                self.input_device_ptr = 0;
3136            }
3137            let imports: Vec<CachedDmaBuf> = self.dmabuf_cache.drain().map(|(_, c)| c).collect();
3138            for cache in imports {
3139                self.release_dmabuf_import(cache);
3140            }
3141            for (base, len) in self.pinned_hosts.drain() {
3142                if len > 0 {
3143                    (self.cuda.cuMemHostUnregister)(base as *mut c_void);
3144                }
3145            }
3146
3147            if is_cbr {
3148                let bps = cbr_bps(settings);
3149                set_cbr_rate(
3150                    &mut self.encode_config.rcParams,
3151                    bps,
3152                    cbr_vbv(settings, bps),
3153                );
3154            } else {
3155                let qp = self
3156                    .codec
3157                    .hardware_quantizer(Hardware::Nvenc, settings.video_crf);
3158                self.encode_config.rcParams.constQP.qpInterP = qp;
3159                self.encode_config.rcParams.constQP.qpInterB = qp;
3160                self.encode_config.rcParams.constQP.qpIntra = qp;
3161                self.current_qp = qp;
3162            }
3163            self.set_level(new_w, new_h, rate.ceil());
3164            match self.codec {
3165                Codec::H265 => {
3166                    self.encode_config
3167                        .encodeCodecConfig
3168                        .hevcConfig
3169                        .maxNumRefFramesInDPB = dpb
3170                }
3171                Codec::Av1 => {
3172                    self.encode_config
3173                        .encodeCodecConfig
3174                        .av1Config
3175                        .maxNumRefFramesInDPB = dpb
3176                }
3177                _ => {
3178                    self.encode_config
3179                        .encodeCodecConfig
3180                        .h264Config
3181                        .maxNumRefFrames = dpb
3182                }
3183            }
3184            self.init_params.encodeWidth = new_w;
3185            self.init_params.encodeHeight = new_h;
3186            self.init_params.darWidth = new_w;
3187            self.init_params.darHeight = new_h;
3188            self.init_params.frameRateNum = rate.num;
3189            self.init_params.frameRateDen = rate.den;
3190            self.init_params.set_splitEncodeMode(split_mode(
3191                self.codec,
3192                new_w,
3193                new_h,
3194                self.engines,
3195                nvenc_cur_ver(),
3196            ) as u32);
3197            if self.reconfigure(true, true) != NVENCSTATUS::NV_ENC_SUCCESS {
3198                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3199                return Err("NvEncReconfigureEncoder rejected the resolution change".into());
3200            }
3201            self.width = new_w;
3202            self.height = new_h;
3203            self.dpb = dpb;
3204            if let Some(references) = &mut self.references {
3205                references.set_capacity(window_frames(self.codec, dpb, references.anchored()));
3206                references.reset();
3207            }
3208
3209            let mut input_device_ptr: CUdeviceptr = 0;
3210            let mut input_pitch: usize = 0;
3211            let res = (self.cuda.cuMemAllocPitch_v2)(
3212                &mut input_device_ptr,
3213                &mut input_pitch,
3214                (new_w * 4) as usize,
3215                new_h as usize,
3216                16,
3217            );
3218            if res != CUresult::CUDA_SUCCESS {
3219                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3220                return Err("Failed to allocate ARGB input buffer on GPU".into());
3221            }
3222            let mut reg_res = NV_ENC_REGISTER_RESOURCE {
3223                version: sv(NvStruct::RegisterResource),
3224                resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
3225                width: new_w,
3226                height: new_h,
3227                resourceToRegister: input_device_ptr as *mut c_void,
3228                pitch: input_pitch as u32,
3229                bufferFormat: self.input_format,
3230                bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
3231                ..Default::default()
3232            };
3233            let register_fn = self.nvenc_funcs.nvEncRegisterResource.unwrap();
3234            if register_fn(self.encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
3235                (self.cuda.cuMemFree_v2)(input_device_ptr);
3236                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3237                return Err("Failed to register input buffer".into());
3238            }
3239            let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
3240                version: sv(NvStruct::MapInputResource),
3241                registeredResource: reg_res.registeredResource,
3242                ..Default::default()
3243            };
3244            let map_fn = self.nvenc_funcs.nvEncMapInputResource.unwrap();
3245            if map_fn(self.encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
3246                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3247                    self.encoder_session,
3248                    reg_res.registeredResource,
3249                );
3250                (self.cuda.cuMemFree_v2)(input_device_ptr);
3251                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3252                return Err("Failed to map input buffer".into());
3253            }
3254            self.input_device_ptr = input_device_ptr;
3255            self.input_pitch = input_pitch;
3256            self.registered_input_resource = reg_res.registeredResource;
3257            self.mapped_input_buffer = map_params.mappedResource;
3258            if let Some(layout) = ConvertLayout::of(self.fullcolor, self.bit_depth) {
3259                self.csc = ChromaConvert::new(
3260                    &self.cuda,
3261                    &self.nvenc_funcs,
3262                    self.encoder_session,
3263                    new_w,
3264                    new_h,
3265                    layout,
3266                );
3267                if self.bit_depth > 8 && self.csc.is_none() {
3268                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3269                    return Err("the driver took no 10-bit convert kernel".into());
3270                }
3271            }
3272            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3273        }
3274        self.omit_stripe_headers = settings.omit_stripe_headers;
3275        Ok(true)
3276    }
3277
3278    /// Page-lock one host upload source's base address once, under the already-current CUDA
3279    /// context, so the copy is a direct pinned DMA instead of a pageable copy staged through a driver
3280    /// bounce buffer. A `0`-length entry records a failed registration so the address is never
3281    /// re-probed; the persistent, bounded shm / reused planar sources make this a one-time cost.
3282    unsafe fn pin_host_source(&mut self, base: usize, len: usize) {
3283        if let std::collections::hash_map::Entry::Vacant(e) = self.pinned_hosts.entry(base) {
3284            let st = (self.cuda.cuMemHostRegister_v2)(base as *mut c_void, len, 0);
3285            e.insert(if st == CUresult::CUDA_SUCCESS { len } else { 0 });
3286        }
3287    }
3288
3289    /// Drop every page-locked host registration, under the pushed CUDA context.
3290    ///
3291    /// Called when the capture's shm segments are recreated at unchanged dimensions: the new
3292    /// segments often reuse the old base addresses, so a stale registration would alias fresh memory.
3293    /// Subsequent uploads re-pin lazily. A `0`-length entry marks a registration that failed and so
3294    /// is not unregistered.
3295    pub fn release_pinned_hosts(&mut self) {
3296        if self.pinned_hosts.is_empty() {
3297            return;
3298        }
3299        unsafe {
3300            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
3301            for (base, len) in self.pinned_hosts.drain() {
3302                if len > 0 {
3303                    (self.cuda.cuMemHostUnregister)(base as *mut c_void);
3304                }
3305            }
3306            let _ = (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3307        }
3308    }
3309
3310    /// Tear down one cached dmabuf import under the already-current CUDA context, inner handle
3311    /// first: the NVENC mapping and registration a direct import holds, then the CUDA graphics
3312    /// resource, then the EGLImage it was built from. The encode that last read the import has
3313    /// completed (`submit_frame` waits for the bitstream), so nothing is still in flight on it.
3314    unsafe fn release_dmabuf_import(&self, cache: CachedDmaBuf) {
3315        if cache.tex != 0 {
3316            (self.cuda.cuTexObjectDestroy)(cache.tex);
3317        }
3318        if let DmaBufInput::Direct {
3319            registered, mapped, ..
3320        } = cache.input
3321        {
3322            (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(self.encoder_session, mapped);
3323            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(self.encoder_session, registered);
3324        }
3325        (self.cuda.cuGraphicsUnregisterResource)(cache.cuda_resource);
3326        if let Some(egl) = &self.egl {
3327            (egl.eglDestroyImageKHR)(self.egl_display, cache.egl_image);
3328        }
3329    }
3330
3331    /// Register the packed input surface with NVENC in the byte order `format` names, when it is
3332    /// not already: the surface memory is unchanged, only its registration (and mapping) is
3333    /// replaced, so a session fed first from one source order and then the other keeps one surface.
3334    /// Runs under the already-current CUDA context; a failed re-registration leaves the surface
3335    /// unregistered and returns `Err`, so the caller's encode fails visibly instead of encoding
3336    /// swapped channels.
3337    unsafe fn set_input_format(&mut self, format: NV_ENC_BUFFER_FORMAT) -> Result<(), String> {
3338        if self.input_format == format && !self.registered_input_resource.is_null() {
3339            return Ok(());
3340        }
3341        if !self.mapped_input_buffer.is_null() {
3342            (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
3343                self.encoder_session,
3344                self.mapped_input_buffer,
3345            );
3346            self.mapped_input_buffer = ptr::null_mut();
3347        }
3348        if !self.registered_input_resource.is_null() {
3349            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3350                self.encoder_session,
3351                self.registered_input_resource,
3352            );
3353            self.registered_input_resource = ptr::null_mut();
3354        }
3355        let mut reg_res = NV_ENC_REGISTER_RESOURCE {
3356            version: sv(NvStruct::RegisterResource),
3357            resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
3358            width: self.width,
3359            height: self.height,
3360            resourceToRegister: self.input_device_ptr as *mut c_void,
3361            pitch: self.input_pitch as u32,
3362            bufferFormat: format,
3363            bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
3364            ..Default::default()
3365        };
3366        if (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res)
3367            != NVENCSTATUS::NV_ENC_SUCCESS
3368        {
3369            return Err(format!("Failed to register input buffer as {format:?}"));
3370        }
3371        let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
3372            version: sv(NvStruct::MapInputResource),
3373            registeredResource: reg_res.registeredResource,
3374            ..Default::default()
3375        };
3376        if (self.nvenc_funcs.nvEncMapInputResource.unwrap())(self.encoder_session, &mut map_params)
3377            != NVENCSTATUS::NV_ENC_SUCCESS
3378        {
3379            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3380                self.encoder_session,
3381                reg_res.registeredResource,
3382            );
3383            return Err(format!("Failed to map input buffer as {format:?}"));
3384        }
3385        self.registered_input_resource = reg_res.registeredResource;
3386        self.mapped_input_buffer = map_params.mappedResource;
3387        self.input_format = format;
3388        Ok(())
3389    }
3390
3391    /// Hand the live config to `nvEncReconfigureEncoder`, resetting the encoder and forcing an IDR
3392    /// as asked.
3393    unsafe fn reconfigure(&mut self, reset: bool, force_idr: bool) -> NVENCSTATUS {
3394        self.init_params.encodeConfig = &mut self.encode_config;
3395        let mut params = reconfigure_params(self.init_params, nvenc_cur_ver(), reset, force_idr);
3396        (self.nvenc_funcs.nvEncReconfigureEncoder.unwrap())(self.encoder_session, &mut params.value)
3397    }
3398
3399    /// Reconfigure the live session's ConstQP when the quantizer the session quality index
3400    /// `crf` selects differs from the current one, returning whether a reconfigure actually
3401    /// happened.
3402    ///
3403    /// A no-op in CBR mode (bitrate-controlled, so QP-based paint-over does not apply) and when the
3404    /// QP is unchanged. When it does apply, the three `constQP` fields are updated and the session is
3405    /// reconfigured **without** a forced IDR: a lower-QP P-frame refines the static image against the
3406    /// existing reference chain (paint-over) with no intra-frame bitrate spike, so the GOP continues
3407    /// seamlessly across the reconfigure.
3408    unsafe fn reconfigure_if_needed(&mut self, crf: u32) -> bool {
3409        if self.encode_config.rcParams.rateControlMode
3410            == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
3411        {
3412            return false;
3413        }
3414        let target_qp = self.codec.hardware_quantizer(Hardware::Nvenc, crf as i32);
3415        if self.current_qp != target_qp {
3416            self.encode_config.rcParams.constQP.qpInterP = target_qp;
3417            self.encode_config.rcParams.constQP.qpInterB = target_qp;
3418            self.encode_config.rcParams.constQP.qpIntra = target_qp;
3419            let status = self.reconfigure(false, false);
3420            if status == NVENCSTATUS::NV_ENC_SUCCESS {
3421                self.current_qp = target_qp;
3422                return true;
3423            }
3424            eprintln!(
3425                "[NVENC] Quantizer reconfigure refused ({status:?}): {}",
3426                last_error(&self.nvenc_funcs, self.encoder_session)
3427            );
3428        }
3429        false
3430    }
3431
3432    /// The quality index the rate control last coded a frame at, where the driver reports its
3433    /// quantizer; a held frame (`hold_quantizer`) is not the rate control's and leaves it.
3434    pub fn last_quality(&self) -> Option<u32> {
3435        self.last_quality
3436    }
3437
3438    /// The bytes of the last frame the rate control coded; a held frame leaves it.
3439    pub fn last_size(&self) -> Option<usize> {
3440        self.last_bytes
3441    }
3442
3443    /// Encode the next frame at the constant quantizer the quality index `crf` selects, whatever
3444    /// the rate control, and leave the session's own rate control and quantizer as they were for
3445    /// the frame after: the cleanup of a still screen. A held key frame of a constant-rate session
3446    /// that comes out past `HELD_KEY_BUDGET_S` of the target is coded again, as a key frame, at the
3447    /// coarser quantizer `held_key_retry` picks, and so is a held frame of the whole picture past
3448    /// `HELD_REFRESH_LIMIT_S`, predicted from the frame before the first attempt, which the device
3449    /// leaves out as it does a frame the client lost (`retract_last`). `band`, the share of the
3450    /// picture from and to in raster order, confines `crf` to the blocks it covers (`band_size`),
3451    /// the rest of the frame held at the coarsest quantizer, or for AV1 at the coarsest the delta
3452    /// map's 128 steps reach above the band: a still region is left as it is at any quantizer, and
3453    /// a change the frame carries before its damage is known (X11 under Turbo hashes a frame beside
3454    /// its encode) costs what the rate control's own frame would.
3455    pub fn hold_quantizer(&mut self, crf: u32, band: Option<(f64, f64)>) {
3456        self.held_qp = Some(crf);
3457        self.held_band = band.filter(|_| self.band_size().is_some());
3458    }
3459
3460    /// The bytes of the last held frame (0 before one), where the session holds a band: H.264,
3461    /// HEVC and AV1, whose QP delta map is one entry a macroblock, a 32x32 coding tree block, or a
3462    /// 64x64 superblock.
3463    pub fn band_size(&self) -> Option<usize> {
3464        matches!(self.codec, Codec::H264 | Codec::H265 | Codec::Av1).then_some(self.held_bytes)
3465    }
3466
3467    /// Put the session on constant quantizer `q` for the picture about to be submitted, `mapped`
3468    /// with the picture's QP delta map on top (`fill_band_map`), answering the rate control to
3469    /// restore once it is encoded; `None` where the session is already there or the driver
3470    /// refused, which leaves the picture to the rate control.
3471    ///
3472    /// The rate-control mode is one of the parameters `nvEncReconfigureEncoder` takes without a
3473    /// reset, so a constant-rate session keeps its HRD state across the held picture instead of
3474    /// starting over after it.
3475    unsafe fn hold_rate(&mut self, q: u32, mapped: bool) -> Option<NV_ENC_RC_PARAMS> {
3476        let saved = self.encode_config.rcParams;
3477        let constant = saved.rateControlMode == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
3478        if constant && !mapped && saved.constQP.qpIntra == q && saved.constQP.qpInterP == q {
3479            return None;
3480        }
3481        let rc = &mut self.encode_config.rcParams;
3482        if mapped {
3483            rc.qpMapMode = NV_ENC_QP_MAP_MODE::NV_ENC_QP_MAP_DELTA;
3484        }
3485        rc.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
3486        rc.constQP.qpInterP = q;
3487        rc.constQP.qpInterB = q;
3488        rc.constQP.qpIntra = q;
3489        let status = self.reconfigure(false, false);
3490        if status != NVENCSTATUS::NV_ENC_SUCCESS {
3491            self.encode_config.rcParams = saved;
3492            if !std::mem::replace(&mut self.hold_refused, true) {
3493                eprintln!(
3494                    "[NVENC] Holding a frame at quantizer {q} refused ({status:?}): {}",
3495                    last_error(&self.nvenc_funcs, self.encoder_session)
3496                );
3497            }
3498            return None;
3499        }
3500        Some(saved)
3501    }
3502
3503    /// Apply a runtime rate-control / frame-rate change to the live session, and report whether
3504    /// the session carries it afterwards.
3505    ///
3506    /// In CBR mode the target bitrate, max bitrate, VBV, and its initial delay are updated (the VBV
3507    /// is ignored outside CBR); the target fps is updated in either mode. The session is
3508    /// reconfigured only when one of these actually changed — no RC reset, and a forced IDR only
3509    /// where a target past the declared level's bitrate ceiling, or a frame rate past its
3510    /// macroblock rate, raises the level, which the decoder learns from the sequence header a key
3511    /// frame carries; a level a lower target or frame rate no longer needs stays, since a level
3512    /// only ever has to be high enough and the driver refuses one below the decoded picture
3513    /// buffer the session declared — so calling it every frame is cheap. A reconfigure the driver
3514    /// refuses leaves the session encoding at its previous rate, logged with the driver's reason.
3515    pub fn reconfigure_rate(&mut self, settings: &RustCaptureSettings) -> bool {
3516        unsafe {
3517            let mut changed = false;
3518            let mut level_raised = false;
3519            if self.encode_config.rcParams.rateControlMode
3520                == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
3521            {
3522                let bps = cbr_bps(settings);
3523                let vbv = cbr_vbv(settings, bps);
3524                let rc = &mut self.encode_config.rcParams;
3525                if rc.averageBitRate != bps || rc.maxBitRate != bps || rc.vbvBufferSize != vbv {
3526                    log_held_rate(settings, bps);
3527                    set_cbr_rate(rc, bps, vbv);
3528                    changed = true;
3529                    let fps = self
3530                        .init_params
3531                        .frameRateNum
3532                        .div_ceil(self.init_params.frameRateDen.max(1));
3533                    let (w, h) = (self.init_params.encodeWidth, self.init_params.encodeHeight);
3534                    if self.level_for(w, h, fps) > self.declared_level() {
3535                        self.set_level(w, h, fps);
3536                        level_raised = true;
3537                    }
3538                }
3539            }
3540            let rate = FrameRate::of(settings.target_fps);
3541            if (self.init_params.frameRateNum, self.init_params.frameRateDen)
3542                != (rate.num, rate.den)
3543            {
3544                self.init_params.frameRateNum = rate.num;
3545                self.init_params.frameRateDen = rate.den;
3546                let (w, h, fps) = (
3547                    self.init_params.encodeWidth,
3548                    self.init_params.encodeHeight,
3549                    rate.ceil(),
3550                );
3551                if self.level_for(w, h, fps) > self.declared_level() {
3552                    self.set_level(w, h, fps);
3553                    level_raised = true;
3554                }
3555                changed = true;
3556            }
3557            if !changed {
3558                return true;
3559            }
3560            let mut status = self.reconfigure(false, level_raised);
3561            if status != NVENCSTATUS::NV_ENC_SUCCESS
3562                && self.codec == Codec::Av1
3563                && !NVENC_AV1_WEIGHTED.swap(true, Ordering::Relaxed)
3564            {
3565                let (w, h) = (self.init_params.encodeWidth, self.init_params.encodeHeight);
3566                let fps = self
3567                    .init_params
3568                    .frameRateNum
3569                    .div_ceil(self.init_params.frameRateDen.max(1));
3570                let refused = self.declared_level();
3571                if self.level_for(w, h, fps) > refused {
3572                    self.set_level(w, h, fps);
3573                    level_raised = true;
3574                    status = self.reconfigure(false, true);
3575                }
3576                if status == NVENCSTATUS::NV_ENC_SUCCESS {
3577                    eprintln!(
3578                        "[NVENC] The driver refused AV1 level {refused} at {} kbps; declaring {}",
3579                        self.encode_config.rcParams.maxBitRate / 1000,
3580                        self.declared_level()
3581                    );
3582                } else {
3583                    NVENC_AV1_WEIGHTED.store(false, Ordering::Relaxed);
3584                }
3585            }
3586            if status != NVENCSTATUS::NV_ENC_SUCCESS {
3587                eprintln!(
3588                    "[NVENC] Rate reconfigure refused ({status:?}): {}",
3589                    last_error(&self.nvenc_funcs, self.encoder_session)
3590                );
3591                return false;
3592            }
3593            if level_raised && let Some(references) = &mut self.references {
3594                references.reset();
3595            }
3596            true
3597        }
3598    }
3599
3600    /// Encode one mapped input picture and return its bitstream bytes behind the wire header.
3601    ///
3602    /// The shared tail of all three encode paths:
3603    ///
3604    /// 1. **Pick an output buffer** from the ring (`current_buffer_idx` advances modulo the ring
3605    ///    length) and submit the picture with `nvEncEncodePicture`; `force_idr` sets the force-IDR
3606    ///    pic flag.
3607    /// 2. **Lock the bitstream** (`nvEncLockBitstream`, blocking) to read the encoded bytes. The
3608    ///    lock has to block: on Linux a `doNotWait` lock answers an unfinished encode with
3609    ///    `NV_ENC_SUCCESS` and an empty bitstream, not `NV_ENC_ERR_LOCK_BUSY`, so polling it would
3610    ///    hand out empty frames.
3611    /// 3. **Frame the output**: unless `omit_stripe_headers` is set, prepend the wire header with
3612    ///    the frame kind derived from the *actual* encoded `pictureType` (IDR = key, I = intra,
3613    ///    else delta) rather than the `force_idr` request.
3614    /// 4. **Emit**: append the encoded bytes, unlock the bitstream, and return the framed buffer.
3615    unsafe fn submit_frame(
3616        &mut self,
3617        mapped_buffer: NV_ENC_INPUT_PTR,
3618        buffer_format: NV_ENC_BUFFER_FORMAT,
3619        frame_number: u64,
3620        force_idr: bool,
3621    ) -> Result<Vec<u8>, String> {
3622        let output_bitstream = self.bitstream_buffers[self.current_buffer_idx];
3623        self.current_buffer_idx = (self.current_buffer_idx + 1) % self.bitstream_buffers.len();
3624
3625        let mut force_idr =
3626            force_idr || self.references.as_ref().is_some_and(|r| !r.has_reference());
3627        if !force_idr
3628            && let Some(references) = &mut self.references
3629            && references.plan_anchor(false).is_some()
3630        {
3631            match references.settle() {
3632                Some(Invalidation::Forget(pts)) => {
3633                    force_idr = (self.nvenc_funcs.nvEncInvalidateRefFrames.unwrap())(
3634                        self.encoder_session,
3635                        pts,
3636                    ) != NVENCSTATUS::NV_ENC_SUCCESS;
3637                }
3638                Some(Invalidation::KeyFrame) => force_idr = true,
3639                _ => {}
3640            }
3641        }
3642        if !force_idr && let Some(references) = &mut self.references {
3643            match references.forget_stale() {
3644                Some(Invalidation::Forget(pts)) => {
3645                    force_idr = (self.nvenc_funcs.nvEncInvalidateRefFrames.unwrap())(
3646                        self.encoder_session,
3647                        pts,
3648                    ) != NVENCSTATUS::NV_ENC_SUCCESS;
3649                }
3650                Some(Invalidation::KeyFrame) => force_idr = true,
3651                _ => {}
3652            }
3653        }
3654        let anchor = self
3655            .references
3656            .as_ref()
3657            .and_then(|r| r.plan_anchor(force_idr));
3658        let named = self
3659            .references
3660            .as_ref()
3661            .filter(|_| !force_idr)
3662            .and_then(ReferenceWindow::predicting_anchor);
3663        let mut pic_params = NV_ENC_PIC_PARAMS {
3664            version: sv(NvStruct::PicParams),
3665            inputWidth: self.width,
3666            inputHeight: self.height,
3667            inputBuffer: mapped_buffer,
3668            outputBitstream: output_bitstream,
3669            bufferFmt: buffer_format,
3670            pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
3671            encodePicFlags: if force_idr {
3672                NV_ENC_PIC_FLAGS::NV_ENC_PIC_FLAG_FORCEIDR as u32
3673            } else {
3674                0
3675            },
3676            inputTimeStamp: self
3677                .references
3678                .as_ref()
3679                .map_or(0, ReferenceWindow::next_pts),
3680            ..Default::default()
3681        };
3682
3683        macro_rules! long_term {
3684            ($p:expr) => {{
3685                let p = $p;
3686                if let Some(slot) = named {
3687                    p.set_ltrUseFrames(1);
3688                    p.ltrUseFrameBitmap = 1 << slot;
3689                }
3690                if let Some(slot) = anchor {
3691                    p.set_ltrMarkFrame(1);
3692                    p.ltrMarkFrameIdx = slot as u32;
3693                }
3694            }};
3695        }
3696        match self.codec {
3697            Codec::Av1 => long_term!(&mut pic_params.codecPicParams.av1PicParams),
3698            Codec::H265 => long_term!(&mut pic_params.codecPicParams.hevcPicParams),
3699            _ => long_term!(&mut pic_params.codecPicParams.h264PicParams),
3700        }
3701        let held_qp = self.held_qp.take();
3702        let band = self.held_band.take();
3703        let held = held_qp.and_then(|crf| {
3704            let q = self.codec.hardware_quantizer(Hardware::Nvenc, crf as i32);
3705            let rest = self
3706                .codec
3707                .quantizer_max()
3708                .min(q + i8::MIN.unsigned_abs() as u32);
3709            match band {
3710                Some(share) if rest > q => {
3711                    fill_band_map(
3712                        &mut self.qp_map,
3713                        self.codec,
3714                        self.width,
3715                        self.height,
3716                        share,
3717                        q as i32 - rest as i32,
3718                    );
3719                    let held = self.hold_rate(rest, true);
3720                    if held.is_some() {
3721                        pic_params.qpDeltaMap = self.qp_map.as_mut_ptr();
3722                        pic_params.qpDeltaMapSize = self.qp_map.len() as u32;
3723                    }
3724                    held
3725                }
3726                _ => self.hold_rate(q, false),
3727            }
3728        });
3729        let mut result = self.encode_picture(
3730            &mut pic_params,
3731            output_bitstream,
3732            frame_number,
3733            held_qp.is_some(),
3734            anchor,
3735        );
3736        let retry = match (held_qp, held, &result) {
3737            (Some(crf), Some(rc), Ok(coded))
3738                if rc.rateControlMode == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
3739                    && (force_idr
3740                        || band.is_none()
3741                            && coded.len() as f64
3742                                > rc.averageBitRate as f64 / 8.0 * super::HELD_REFRESH_LIMIT_S) =>
3743            {
3744                let cap = (rc.averageBitRate as f64 / 8.0 * super::HELD_KEY_BUDGET_S) as usize;
3745                super::held_key_retry(crf, coded.len(), cap)
3746            }
3747            _ => None,
3748        };
3749        if let Some(coarser) = retry
3750            && self
3751                .hold_rate(
3752                    self.codec
3753                        .hardware_quantizer(Hardware::Nvenc, coarser as i32),
3754                    false,
3755                )
3756                .is_some()
3757            && (force_idr || self.retract_last())
3758        {
3759            pic_params.inputTimeStamp = self
3760                .references
3761                .as_ref()
3762                .map_or(0, ReferenceWindow::next_pts);
3763            result = self.encode_picture(
3764                &mut pic_params,
3765                output_bitstream,
3766                frame_number,
3767                true,
3768                anchor,
3769            );
3770        }
3771        if let Some(rc) = held {
3772            self.encode_config.rcParams = rc;
3773            let status = self.reconfigure(false, false);
3774            if status != NVENCSTATUS::NV_ENC_SUCCESS {
3775                eprintln!(
3776                    "[NVENC] Restoring the rate control after a held frame refused ({status:?}): {}",
3777                    last_error(&self.nvenc_funcs, self.encoder_session)
3778                );
3779            }
3780        }
3781        result
3782    }
3783
3784    /// Submit one picture and read its bitstream back behind the wire header: steps 1 to 4 of
3785    /// `submit_frame` past the choice of output buffer. A `held` picture leaves `last_quality` to
3786    /// the rate control's own frames.
3787    unsafe fn encode_picture(
3788        &mut self,
3789        pic_params: &mut NV_ENC_PIC_PARAMS,
3790        output_bitstream: NV_ENC_OUTPUT_PTR,
3791        frame_number: u64,
3792        held: bool,
3793        anchor: Option<u8>,
3794    ) -> Result<Vec<u8>, String> {
3795        let encode_fn = self.nvenc_funcs.nvEncEncodePicture.unwrap();
3796        let res = encode_fn(self.encoder_session, pic_params);
3797        if res != NVENCSTATUS::NV_ENC_SUCCESS {
3798            return Err(format!("Encode Picture failed: {:?}", res));
3799        }
3800        // A host upload's band hash asked for (`hash_next_upload`), queued once its encode is, so
3801        // it runs on the CUDA cores beside the encode engine and holds nothing up.
3802        if let Some(rows) = self.upload_hash.take() {
3803            let (src, pitch, width, height) = (
3804                self.input_device_ptr,
3805                self.input_pitch,
3806                self.width,
3807                self.height,
3808            );
3809            let cuda = &self.cuda;
3810            self.upload_words = self
3811                .band_hash
3812                .get_or_insert_with(|| BandHash::new(cuda))
3813                .as_mut()
3814                .and_then(|h| h.launch(cuda, src, pitch, width, height, rows));
3815        }
3816
3817        let mut negotiated = Negotiated::new(NV_ENC_LOCK_BITSTREAM {
3818            version: sv(NvStruct::LockBitstream),
3819            outputBitstream: output_bitstream,
3820            ..Default::default()
3821        });
3822        let lock_params = &mut negotiated.value;
3823        lock_params.set_doNotWait(0);
3824        let lock_fn = self.nvenc_funcs.nvEncLockBitstream.unwrap();
3825        let status = lock_fn(self.encoder_session, lock_params);
3826        if status != NVENCSTATUS::NV_ENC_SUCCESS {
3827            return Err(format!("Lock Bitstream failed: {status:?}"));
3828        }
3829
3830        let data_ptr = lock_params.bitstreamBufferPtr as *const u8;
3831        let data_size = lock_params.bitstreamSizeInBytes as usize;
3832        let header_sz = if self.omit_stripe_headers {
3833            0
3834        } else {
3835            VIDEO_HEADER_LEN
3836        };
3837        let mut output = Vec::with_capacity(header_sz + data_size);
3838        if !held {
3839            self.last_quality = match lock_params.frameAvgQP {
3840                0 => None,
3841                q => Some(self.codec.hardware_quality_index(Hardware::Nvenc, q)),
3842            };
3843            self.last_bytes = Some(data_size);
3844        } else {
3845            self.held_bytes = data_size;
3846        }
3847        let frame_type = match lock_params.pictureType {
3848            NV_ENC_PIC_TYPE::NV_ENC_PIC_TYPE_IDR => FRAME_KEY,
3849            NV_ENC_PIC_TYPE::NV_ENC_PIC_TYPE_I => FRAME_INTRA,
3850            _ => FRAME_DELTA,
3851        };
3852        let mut wire_type = frame_type;
3853        if let Some(references) = &mut self.references {
3854            let shared = references.acknowledging() && references.predicts_from_shared();
3855            self.last_reference =
3856                references.record_marked(frame_number as u16, frame_type != FRAME_DELTA, anchor);
3857            if anchor.is_some() && frame_type == FRAME_DELTA && shared {
3858                wire_type |= FRAME_ANCHOR;
3859            }
3860        }
3861
3862        if !self.omit_stripe_headers {
3863            push_video_header(
3864                &mut output,
3865                self.codec,
3866                wire_type,
3867                frame_number as u16,
3868                0,
3869                self.width as u16,
3870                self.height as u16,
3871                self.last_reference,
3872            );
3873        }
3874
3875        if data_size > 0 && !data_ptr.is_null() {
3876            let slice = std::slice::from_raw_parts(data_ptr, data_size);
3877            if self.codec == Codec::H264 && self.references.is_some() {
3878                let mut widened = true;
3879                for nal in crate::encoders::codec::annexb_nals(slice) {
3880                    widened &= self.frame_num.push_unit(&[0, 0, 0, 1], nal, &mut output);
3881                }
3882                if !widened && let Some(references) = &mut self.references {
3883                    // Its slices no longer match the set they went out under: the next frame
3884                    // is a key frame.
3885                    references.reset();
3886                }
3887            } else {
3888                output.extend_from_slice(slice);
3889            }
3890        }
3891        if frame_type == FRAME_KEY
3892            && self.codec == Codec::H264
3893            && let Some(references) = &mut self.references
3894        {
3895            references.set_frame_num_range(h264_frame_num_range(&output[header_sz..]).unwrap_or(0));
3896        }
3897
3898        (self.nvenc_funcs.nvEncUnlockBitstream.unwrap())(self.encoder_session, output_bitstream);
3899        Ok(output)
3900    }
3901
3902    /// Encode a dmabuf frame zero-copy, by importing it through EGL into CUDA and, where the
3903    /// driver allows, handing the mapped plane to NVENC as its input.
3904    ///
3905    /// Applies any pending ConstQP change, then works under the pushed CUDA context:
3906    ///
3907    /// 1. **Import once, cache by fd with an identity check**: the cache is keyed by the dmabuf fd
3908    ///    but each entry stores the buffer's `DmaBufIdentity`; an entry whose identity no longer
3909    ///    matches (a recycled fd) is released first. On a miss, build an `EGLImageKHR` from the
3910    ///    dmabuf's fd / offset / pitch / modifier, register it as a CUDA graphics resource, map it to
3911    ///    a `CUeglFrame`, and settle how it feeds the encoder: a first plane that `direct_plane`
3912    ///    accepts (and `direct_dmabuf` on) is registered with NVENC in place — a pitch-linear plane
3913    ///    as a CUDA device pointer at its own pitch, a four-channel 8-bit CUDA array as a CUDA
3914    ///    array — in the byte order the dmabuf fourcc names, and mapped once (`DmaBufInput::Direct`);
3915    ///    any other plane, or a registration the driver refuses, takes `DmaBufInput::Copy`. The
3916    ///    result is memoized so a recurring capture buffer pays the import cost only once. Each
3917    ///    failure destroys what it created and pops the context.
3918    /// 2. **Feed the encoder**: a direct import is submitted as it is — no copy at all. A copy
3919    ///    import is copied with `cuMemcpy2DAsync` on the default stream — the array plane or the
3920    ///    pitch-linear plane, per `frame_type` — into the packed input surface, re-registered in the
3921    ///    dmabuf's byte order when it differs; NVENC processes its input on that same stream, so the
3922    ///    copy is ordered before the encode without a host wait.
3923    /// 3. **Submit** via `submit_frame`, then pop the context.
3924    ///
3925    /// The dmabuf fd is read out before the context is pushed so an early `?` return cannot leave the
3926    /// CUDA context stack imbalanced.
3927    pub fn encode(
3928        &mut self,
3929        dmabuf: &Dmabuf,
3930        frame_number: u64,
3931        crf: u32,
3932        force_idr: bool,
3933    ) -> Result<Vec<u8>, String> {
3934        self.encode_frame(dmabuf, frame_number, crf, force_idr, false)
3935    }
3936
3937    /// `encode`, for a buffer the X server is still blitting into: the frame's work is queued
3938    /// behind a wait on the semaphore the server signals once the blit has landed
3939    /// (`blit_semaphore_fd`), so it runs on the GPU right behind the blit instead of after the
3940    /// CPU has learnt of it. NVENC reads its input on the default stream, as the copy and the
3941    /// convert do, so one wait there orders all of it.
3942    pub fn encode_after_blit(
3943        &mut self,
3944        dmabuf: &Dmabuf,
3945        frame_number: u64,
3946        crf: u32,
3947        force_idr: bool,
3948    ) -> Result<Vec<u8>, String> {
3949        self.encode_frame(dmabuf, frame_number, crf, force_idr, true)
3950    }
3951
3952    /// A new semaphore for the X server to signal after a blit and `encode_after_blit` to wait
3953    /// on: made on this session's GPU, imported into its context in place of any earlier one,
3954    /// and returned as the fd the server imports.
3955    pub fn blit_semaphore_fd(&mut self) -> Result<std::os::fd::OwnedFd, String> {
3956        use std::os::fd::IntoRawFd;
3957        unsafe {
3958            let (Some(import), Some(_), Some(destroy)) = (
3959                self.cuda.cuImportExternalSemaphore,
3960                self.cuda.cuWaitExternalSemaphoresAsync,
3961                self.cuda.cuDestroyExternalSemaphore,
3962            ) else {
3963                return Err("the CUDA driver has no external semaphores".into());
3964            };
3965            let mut uuid = CUuuid { bytes: [0; 16] };
3966            if (self.cuda.cuDeviceGetUuid)(&mut uuid, self.cuda_device) != CUresult::CUDA_SUCCESS {
3967                return Err("the CUDA device has no UUID to find its Vulkan device by".into());
3968            }
3969            let (cuda_fd, server_fd) =
3970                super::blit_semaphore::opaque_fd_pair(uuid.bytes.map(|b| b.to_ne_bytes()[0]))?;
3971            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
3972            if !self.blit_semaphore.is_null() {
3973                destroy(self.blit_semaphore);
3974                self.blit_semaphore = ptr::null_mut();
3975            }
3976            let mut desc = CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC {
3977                type_: CUexternalSemaphoreHandleType::CU_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD,
3978                ..Default::default()
3979            };
3980            desc.handle.fd = cuda_fd.as_raw_fd();
3981            let mut semaphore: CUexternalSemaphore = ptr::null_mut();
3982            let r = import(&mut semaphore, &desc);
3983            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3984            if r != CUresult::CUDA_SUCCESS {
3985                return Err(format!(
3986                    "CUDA refused the semaphore ({})",
3987                    Self::get_error_string(&self.cuda, r)
3988                ));
3989            }
3990            // A successful import owns its fd.
3991            let _ = cuda_fd.into_raw_fd();
3992            self.blit_semaphore = semaphore;
3993            Ok(server_fd)
3994        }
3995    }
3996
3997    fn encode_frame(
3998        &mut self,
3999        dmabuf: &Dmabuf,
4000        frame_number: u64,
4001        crf: u32,
4002        force_idr: bool,
4003        after_blit: bool,
4004    ) -> Result<Vec<u8>, String> {
4005        unsafe {
4006            self.reconfigure_if_needed(crf);
4007            let fd = dmabuf.handles().next().ok_or("No handles")?.as_raw_fd();
4008            let fmt = dmabuf.format();
4009            let modifier: u64 = fmt.modifier.into();
4010            let identity = DmaBufIdentity::probe(fd, modifier, self.width, self.height);
4011            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4012
4013            // First, before anything can fail: each signal the server was asked for has exactly
4014            // one wait, or the binary semaphore would be signalled again while still signalled.
4015            if after_blit {
4016                let wait = self.cuda.cuWaitExternalSemaphoresAsync;
4017                let queued = match wait {
4018                    Some(wait) if !self.blit_semaphore.is_null() => {
4019                        let params = CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS::default();
4020                        wait(&self.blit_semaphore, &params, 1, ptr::null_mut())
4021                    }
4022                    _ => CUresult::CUDA_ERROR_NOT_INITIALIZED,
4023                };
4024                if queued != CUresult::CUDA_SUCCESS {
4025                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4026                    return Err(format!(
4027                        "the wait for the server's blit could not be queued ({})",
4028                        Self::get_error_string(&self.cuda, queued)
4029                    ));
4030                }
4031            }
4032
4033            // A raw fd number is not an identity: the host recycles fd numbers across slot
4034            // renegotiations, so an entry whose stored identity no longer matches is torn down and
4035            // re-imported rather than returning a stale EGLImage for a buffer the fd no longer names.
4036            if self
4037                .dmabuf_cache
4038                .get(&fd)
4039                .is_some_and(|c| c.identity != identity)
4040                && let Some(stale) = self.dmabuf_cache.remove(&fd)
4041            {
4042                self.release_dmabuf_import(stale);
4043            }
4044
4045            if !self.dmabuf_cache.contains_key(&fd) {
4046                let Some(egl) = self.egl.clone() else {
4047                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4048                    return Err("a session opened without an EGL display imports no dmabuf".into());
4049                };
4050                let stride = dmabuf.strides().next().unwrap_or(0) as i32;
4051                let offset = dmabuf.offsets().next().unwrap_or(0) as i32;
4052
4053                let attribs = [
4054                    EGL_WIDTH,
4055                    self.width as i32,
4056                    EGL_HEIGHT,
4057                    self.height as i32,
4058                    EGL_LINUX_DRM_FOURCC_EXT,
4059                    egl_import_fourcc(fmt.code) as i32,
4060                    EGL_DMA_BUF_PLANE0_FD_EXT,
4061                    fd,
4062                    EGL_DMA_BUF_PLANE0_OFFSET_EXT,
4063                    offset,
4064                    EGL_DMA_BUF_PLANE0_PITCH_EXT,
4065                    stride,
4066                    EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
4067                    (modifier & 0xFFFFFFFF) as i32,
4068                    EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
4069                    (modifier >> 32) as i32,
4070                    EGL_NONE,
4071                ];
4072
4073                let egl_image = (egl.eglCreateImageKHR)(
4074                    self.egl_display,
4075                    ptr::null_mut(),
4076                    EGL_LINUX_DMA_BUF_EXT,
4077                    ptr::null_mut(),
4078                    attribs.as_ptr(),
4079                );
4080                if egl_image == EGL_NO_IMAGE_KHR {
4081                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4082                    return Err("Failed to create EGLImage".into());
4083                }
4084
4085                let mut cuda_resource: CUgraphicsResource = ptr::null_mut();
4086                if (self.cuda.cuGraphicsEGLRegisterImage)(&mut cuda_resource, egl_image, 1)
4087                    != CUresult::CUDA_SUCCESS
4088                {
4089                    (egl.eglDestroyImageKHR)(self.egl_display, egl_image);
4090                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4091                    return Err("Failed to register EGLImage".into());
4092                }
4093
4094                let mut egl_frame: CUeglFrame = std::mem::zeroed();
4095                if (self.cuda.cuGraphicsResourceGetMappedEglFrame)(
4096                    &mut egl_frame,
4097                    cuda_resource,
4098                    0,
4099                    0,
4100                ) != CUresult::CUDA_SUCCESS
4101                {
4102                    (self.cuda.cuGraphicsUnregisterResource)(cuda_resource);
4103                    (egl.eglDestroyImageKHR)(self.egl_display, egl_image);
4104                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4105                    return Err("Failed to map EGL frame".into());
4106                }
4107
4108                let input = match (
4109                    self.direct_dmabuf,
4110                    direct_plane(&egl_frame, self.width, self.height),
4111                    fourcc_nvenc_format(fmt.code),
4112                ) {
4113                    (true, Some(plane), Some(format)) => {
4114                        self.register_direct_input(&egl_frame, plane, format)
4115                    }
4116                    _ => DmaBufInput::Copy,
4117                };
4118                crate::log::debug!(
4119                    "[NVENC] dmabuf imported as a {} frame ({} planes, {}x{}, pitch {}, {} channels of element format {}): {}.",
4120                    match egl_frame.frame_type {
4121                        CU_EGL_FRAME_TYPE_PITCH => "pitch-linear",
4122                        CU_EGL_FRAME_TYPE_ARRAY => "CUDA-array",
4123                        _ => "unknown-kind",
4124                    },
4125                    egl_frame.plane_count,
4126                    egl_frame.width,
4127                    egl_frame.height,
4128                    egl_frame.pitch,
4129                    egl_frame.num_channels,
4130                    egl_frame.cu_format,
4131                    match input {
4132                        DmaBufInput::Direct { .. } => "encoding in place",
4133                        DmaBufInput::Copy => "copying per frame",
4134                    }
4135                );
4136
4137                // An array-typed import is read through a texture, so the convert never needs
4138                // it copied into linear memory.
4139                let tex = if self.csc.is_some() && egl_frame.frame_type == CU_EGL_FRAME_TYPE_ARRAY {
4140                    ChromaConvert::texture_for(&self.cuda, egl_frame.frame.p_array[0])
4141                } else {
4142                    0
4143                };
4144                self.dmabuf_cache.insert(
4145                    fd,
4146                    CachedDmaBuf {
4147                        identity,
4148                        egl_image,
4149                        cuda_resource,
4150                        egl_frame,
4151                        input,
4152                        tex,
4153                    },
4154                );
4155            }
4156
4157            let (egl_frame, input, tex) = {
4158                let cached = self.dmabuf_cache.get(&fd).unwrap();
4159                (cached.egl_frame, cached.input, cached.tex)
4160            };
4161            let (mapped, format) = match input {
4162                DmaBufInput::Direct { mapped, format, .. } => (mapped, format),
4163                DmaBufInput::Copy => {
4164                    let mut copy_params = CUDA_MEMCPY2D {
4165                        srcMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
4166                        srcHost: ptr::null(),
4167                        srcDevice: 0,
4168                        srcArray: ptr::null_mut(),
4169                        srcPitch: 0,
4170                        dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
4171                        dstHost: ptr::null_mut(),
4172                        dstDevice: self.input_device_ptr,
4173                        dstArray: ptr::null_mut(),
4174                        dstPitch: self.input_pitch,
4175                        WidthInBytes: (self.width * 4) as usize,
4176                        Height: self.height as usize,
4177                        ..Default::default()
4178                    };
4179                    if egl_frame.frame_type == CU_EGL_FRAME_TYPE_ARRAY {
4180                        copy_params.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_ARRAY;
4181                        copy_params.srcArray = egl_frame.frame.p_array[0];
4182                    } else {
4183                        copy_params.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_DEVICE;
4184                        copy_params.srcDevice = egl_frame.frame.p_pitch[0] as CUdeviceptr;
4185                        copy_params.srcPitch = egl_frame.pitch as usize;
4186                    }
4187                    // A fourcc without a packed NVENC equivalent keeps the surface's current
4188                    // registration; the copy still lands the bytes, as it always has.
4189                    if let Some(format) = fourcc_nvenc_format(fmt.code)
4190                        && let Err(e) = self.set_input_format(format)
4191                    {
4192                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4193                        return Err(e);
4194                    }
4195                    if (self.cuda.cuMemcpy2DAsync_v2)(&copy_params, ptr::null_mut())
4196                        != CUresult::CUDA_SUCCESS
4197                    {
4198                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4199                        return Err("Sanitization copy failed".into());
4200                    }
4201                    (self.mapped_input_buffer, self.input_format)
4202                }
4203            };
4204
4205            // The convert reads the frame where it already is: the import itself when the driver
4206            // mapped it pitch-linear or handed back an array a texture covers, the session's own
4207            // surface when the frame was copied into it. No case adds a copy of its own.
4208            let swap = format == NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR;
4209            let converted = match input {
4210                DmaBufInput::Copy => {
4211                    self.convert_packed(self.input_device_ptr, self.input_pitch, swap)
4212                }
4213                DmaBufInput::Direct { .. } if egl_frame.frame_type == CU_EGL_FRAME_TYPE_PITCH => {
4214                    self.convert_packed(
4215                        egl_frame.frame.p_pitch[0] as CUdeviceptr,
4216                        egl_frame.pitch as usize,
4217                        swap,
4218                    )
4219                }
4220                DmaBufInput::Direct { .. } => self.convert_texture(tex, swap),
4221            };
4222            let (mapped, format) = match converted {
4223                Ok(Some(converted)) => converted,
4224                Ok(None) => (mapped, format),
4225                Err(e) => {
4226                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4227                    return Err(e);
4228                }
4229            };
4230
4231            let result = self.submit_frame(mapped, format, frame_number, force_idr);
4232            if result.is_err() {
4233                (self.cuda.cuStreamSynchronize)(ptr::null_mut());
4234            }
4235            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4236            result
4237        }
4238    }
4239
4240    /// Register the first plane of a mapped dmabuf frame with NVENC in place — as a pitch-linear
4241    /// CUDA device pointer or as a CUDA array, per `plane` — and map it as an input, under the
4242    /// already-current CUDA context. Either step failing falls back to `DmaBufInput::Copy` — the
4243    /// per-frame copy then serves that import for as long as it is cached, so a driver that declines
4244    /// the direct path costs one failed registration, not a failed frame.
4245    unsafe fn register_direct_input(
4246        &mut self,
4247        frame: &CUeglFrame,
4248        plane: DirectPlane,
4249        format: NV_ENC_BUFFER_FORMAT,
4250    ) -> DmaBufInput {
4251        let (resource_type, resource, pitch) = match plane {
4252            DirectPlane::Pitch(pitch) => (
4253                NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
4254                frame.frame.p_pitch[0],
4255                pitch,
4256            ),
4257            DirectPlane::Array(row_bytes) => (
4258                NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDAARRAY,
4259                frame.frame.p_array[0] as *mut c_void,
4260                row_bytes,
4261            ),
4262        };
4263        let mut reg_res = NV_ENC_REGISTER_RESOURCE {
4264            version: sv(NvStruct::RegisterResource),
4265            resourceType: resource_type,
4266            width: self.width,
4267            height: self.height,
4268            resourceToRegister: resource,
4269            pitch,
4270            bufferFormat: format,
4271            bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
4272            ..Default::default()
4273        };
4274        let st =
4275            (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res);
4276        if st != NVENCSTATUS::NV_ENC_SUCCESS {
4277            eprintln!(
4278                "[NVENC] dmabuf plane registration ({plane:?}) refused ({st:?}); copying per frame."
4279            );
4280            return DmaBufInput::Copy;
4281        }
4282        let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
4283            version: sv(NvStruct::MapInputResource),
4284            registeredResource: reg_res.registeredResource,
4285            ..Default::default()
4286        };
4287        let st = (self.nvenc_funcs.nvEncMapInputResource.unwrap())(
4288            self.encoder_session,
4289            &mut map_params,
4290        );
4291        if st != NVENCSTATUS::NV_ENC_SUCCESS {
4292            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
4293                self.encoder_session,
4294                reg_res.registeredResource,
4295            );
4296            eprintln!("[NVENC] dmabuf plane mapping refused ({st:?}); copying per frame.");
4297            return DmaBufInput::Copy;
4298        }
4299        DmaBufInput::Direct {
4300            registered: reg_res.registeredResource,
4301            mapped: map_params.mappedResource,
4302            format,
4303        }
4304    }
4305
4306    /// Encode a host BGRA frame (B,G,R,A in memory, NVENC's word-ordered `ARGB` — the layout an
4307    /// XShm grab or the pixman framebuffer yields) through `encode_cpu_packed`.
4308    pub fn encode_cpu_argb(
4309        &mut self,
4310        argb: &[u8],
4311        src_stride: usize,
4312        frame_number: u64,
4313        crf: u32,
4314        force_idr: bool,
4315    ) -> Result<Vec<u8>, String> {
4316        self.encode_cpu_packed(argb, src_stride, false, frame_number, crf, force_idr)
4317    }
4318
4319    /// Encode a host packed-pixel frame by uploading it straight into the packed input surface,
4320    /// with no CPU-side color conversion: the surface is either the chroma convert's source or,
4321    /// where the kernel did not load, NVENC's own conversion's; a host prepass would cost this
4322    /// path its copy-free property.
4323    ///
4324    /// `rgba_input` names the byte order — `false` for B,G,R,A (X11 XShm, the pixman framebuffer),
4325    /// `true` for R,G,B,A (a GLES readback) — and the input surface is registered with NVENC in
4326    /// that order (`ARGB` / `ABGR`, re-registered in place when it changes), so both arrive at the
4327    /// hardware CSC untouched. `src_stride` is the source row stride in bytes (`>= width*4`).
4328    /// Steps, under the pushed CUDA context after any pending QP change:
4329    ///
4330    /// 1. **Bounds-check** the source against `stride × (rows-1) + width*4`, erroring rather than
4331    ///    reading past a short buffer.
4332    /// 2. **Pin the source once**: unless pinning was disabled at init (`PIXELFLUX_NVENC_PIN=0`, read
4333    ///    once into `pin_uploads`), page-lock each distinct source base address via `pin_host_source`
4334    ///    so the upload is a direct DMA from the caller's buffer instead of a pageable copy staged
4335    ///    through a driver bounce buffer. The persistent, bounded shm / pool sources make this a
4336    ///    one-time bounded cost.
4337    /// 3. **Upload and submit**: `cuMemcpy2DAsync` the rows into the input surface on the default
4338    ///    stream honoring `src_stride`, then `submit_frame`. NVENC processes its input on that same
4339    ///    stream, so the upload is ordered before the encode without a host wait, and the blocking
4340    ///    bitstream lock inside `submit_frame` (or the stream sync on its error path) guarantees the
4341    ///    upload has finished reading `pixels` by the time this returns — the caller may reuse the
4342    ///    buffer immediately.
4343    pub fn encode_cpu_packed(
4344        &mut self,
4345        pixels: &[u8],
4346        src_stride: usize,
4347        rgba_input: bool,
4348        frame_number: u64,
4349        crf: u32,
4350        force_idr: bool,
4351    ) -> Result<Vec<u8>, String> {
4352        unsafe {
4353            self.reconfigure_if_needed(crf);
4354            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4355
4356            let width_bytes = (self.width * 4) as usize;
4357            let rows = self.height as usize;
4358            let needed = if rows == 0 {
4359                0
4360            } else {
4361                src_stride * (rows - 1) + width_bytes
4362            };
4363            if src_stride < width_bytes || pixels.len() < needed {
4364                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4365                return Err(format!(
4366                    "packed buffer too small: len={} need>={} (stride={}, {}x{})",
4367                    pixels.len(),
4368                    needed,
4369                    src_stride,
4370                    self.width,
4371                    self.height
4372                ));
4373            }
4374
4375            let format = if rgba_input {
4376                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
4377            } else {
4378                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
4379            };
4380            // With the convert in place the packed surface is the kernel's source and needs no
4381            // registration of its own; NVENC is handed the NV12 the kernel writes.
4382            if self.csc.is_none()
4383                && let Err(e) = self.set_input_format(format)
4384            {
4385                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4386                return Err(e);
4387            }
4388
4389            if self.pin_uploads {
4390                self.pin_host_source(pixels.as_ptr() as usize, pixels.len());
4391            }
4392
4393            let copy = CUDA_MEMCPY2D {
4394                srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
4395                srcHost: pixels.as_ptr() as *const c_void,
4396                srcPitch: src_stride,
4397                dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
4398                dstDevice: self.input_device_ptr,
4399                dstPitch: self.input_pitch,
4400                WidthInBytes: width_bytes,
4401                Height: rows,
4402                ..Default::default()
4403            };
4404            if (self.cuda.cuMemcpy2DAsync_v2)(&copy, ptr::null_mut()) != CUresult::CUDA_SUCCESS {
4405                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4406                return Err("packed host->device upload failed".into());
4407            }
4408
4409            let (mapped, submitted) =
4410                match self.convert_packed(self.input_device_ptr, self.input_pitch, rgba_input) {
4411                    Ok(Some(converted)) => converted,
4412                    Ok(None) => (self.mapped_input_buffer, self.input_format),
4413                    Err(e) => {
4414                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4415                        return Err(e);
4416                    }
4417                };
4418            let result = self.submit_frame(mapped, submitted, frame_number, force_idr);
4419            if result.is_err() {
4420                (self.cuda.cuStreamSynchronize)(ptr::null_mut());
4421            }
4422            self.upload_hash = None;
4423            if let Some(words) = self.upload_words.take()
4424                && let Some(Some(hash)) = self.band_hash.as_mut()
4425            {
4426                self.upload_hashes = hash.read(&self.cuda, words);
4427            }
4428            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4429            result
4430        }
4431    }
4432
4433    /// Run the chroma convert over an array-typed import through `tex`, answering the input
4434    /// NVENC should be handed. `None` where the session has no convert, or where the driver gave
4435    /// no texture for the import and NVENC's own conversion stands in, which a 10-bit session,
4436    /// initialized for the convert's surface, has none of.
4437    unsafe fn convert_texture(
4438        &self,
4439        tex: CUtexObject,
4440        rgba_input: bool,
4441    ) -> Result<Option<(NV_ENC_INPUT_PTR, NV_ENC_BUFFER_FORMAT)>, String> {
4442        match self.csc.as_ref() {
4443            Some(csc) if tex != 0 => {
4444                csc.run_texture(&self.cuda, tex, self.width, self.height, rgba_input)?;
4445                Ok(Some(csc.input()))
4446            }
4447            _ if self.bit_depth > 8 => {
4448                Err("the driver gave no texture for the import a 10-bit session converts".into())
4449            }
4450            _ => Ok(None),
4451        }
4452    }
4453
4454    /// Run the chroma convert over a packed surface, answering the input NVENC should be
4455    /// handed, or `None` where the session has no convert and encodes the packed surface itself.
4456    /// The caller holds the CUDA context current.
4457    unsafe fn convert_packed(
4458        &self,
4459        src: CUdeviceptr,
4460        src_pitch: usize,
4461        rgba_input: bool,
4462    ) -> Result<Option<(NV_ENC_INPUT_PTR, NV_ENC_BUFFER_FORMAT)>, String> {
4463        match self.csc.as_ref() {
4464            Some(csc) => {
4465                csc.run(
4466                    &self.cuda,
4467                    src,
4468                    src_pitch,
4469                    self.width,
4470                    self.height,
4471                    rgba_input,
4472                )?;
4473                Ok(Some(csc.input()))
4474            }
4475            None => Ok(None),
4476        }
4477    }
4478
4479    /// The geometry the session is currently initialized for.
4480    pub fn width(&self) -> u32 {
4481        self.width
4482    }
4483
4484    /// The geometry the session is currently initialized for.
4485    pub fn height(&self) -> u32 {
4486        self.height
4487    }
4488
4489    /// Make this session's CUDA context current on the calling thread, so a capture source can
4490    /// allocate its frames in the very context the encoder reads them from — the whole basis of
4491    /// a zero-copy hand-over. Paired with [`NvencEncoder::pop_context`].
4492    pub(crate) fn push_context(&self) -> bool {
4493        unsafe { (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context) == CUresult::CUDA_SUCCESS }
4494    }
4495
4496    /// Give back the context [`NvencEncoder::push_context`] made current.
4497    pub(crate) fn pop_context(&self) {
4498        unsafe {
4499            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4500        }
4501    }
4502
4503    /// Drop the registration of an externally-owned input surface, under the pushed CUDA context.
4504    ///
4505    /// The caller owns the memory behind it, so this must run before that owner releases it: a
4506    /// registration outliving its buffer leaves the driver holding a mapping of freed video
4507    /// memory.
4508    pub fn release_external_input(&mut self) {
4509        if self.external_input.is_none() {
4510            return;
4511        }
4512        unsafe {
4513            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4514            self.unmap_external_input();
4515            let _ = (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4516        }
4517    }
4518
4519    /// Unmap and unregister the external input, with the CUDA context already current.
4520    unsafe fn unmap_external_input(&mut self) {
4521        if let Some(ext) = self.external_input.take() {
4522            (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(self.encoder_session, ext.mapped);
4523            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
4524                self.encoder_session,
4525                ext.registered,
4526            );
4527        }
4528    }
4529
4530    /// The mapped NVENC input for an externally-owned device pointer, registering it in place the
4531    /// first time and reusing that registration for every later frame from the same buffer.
4532    ///
4533    /// A capture source hands back one buffer for as long as its geometry holds, so the register
4534    /// and map cost is paid once per session rather than per frame; a pointer, pitch, or geometry
4535    /// that changes releases the old registration and builds a new one.
4536    unsafe fn register_external_input(
4537        &mut self,
4538        device_ptr: CUdeviceptr,
4539        pitch: usize,
4540        format: NV_ENC_BUFFER_FORMAT,
4541    ) -> Result<NV_ENC_INPUT_PTR, String> {
4542        if let Some(ext) = self.external_input
4543            && ext.device_ptr == device_ptr
4544            && ext.pitch == pitch
4545            && ext.format == format
4546            && ext.width == self.width
4547            && ext.height == self.height
4548        {
4549            return Ok(ext.mapped);
4550        }
4551        self.unmap_external_input();
4552        let mut reg_res = NV_ENC_REGISTER_RESOURCE {
4553            version: sv(NvStruct::RegisterResource),
4554            resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
4555            width: self.width,
4556            height: self.height,
4557            resourceToRegister: device_ptr as *mut c_void,
4558            pitch: pitch as u32,
4559            bufferFormat: format,
4560            bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
4561            ..Default::default()
4562        };
4563        let st =
4564            (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res);
4565        if st != NVENCSTATUS::NV_ENC_SUCCESS {
4566            return Err(format!(
4567                "failed to register the captured frame as an NVENC input ({st:?})"
4568            ));
4569        }
4570        let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
4571            version: sv(NvStruct::MapInputResource),
4572            registeredResource: reg_res.registeredResource,
4573            ..Default::default()
4574        };
4575        let st = (self.nvenc_funcs.nvEncMapInputResource.unwrap())(
4576            self.encoder_session,
4577            &mut map_params,
4578        );
4579        if st != NVENCSTATUS::NV_ENC_SUCCESS {
4580            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
4581                self.encoder_session,
4582                reg_res.registeredResource,
4583            );
4584            return Err(format!(
4585                "failed to map the captured frame as an NVENC input ({st:?})"
4586            ));
4587        }
4588        self.external_input = Some(ExternalInput {
4589            device_ptr,
4590            pitch,
4591            width: self.width,
4592            height: self.height,
4593            format,
4594            registered: reg_res.registeredResource,
4595            mapped: map_params.mappedResource,
4596        });
4597        Ok(map_params.mappedResource)
4598    }
4599
4600    /// Encode a frame that already lives in video memory, reading it exactly where it was
4601    /// produced.
4602    ///
4603    /// This is the zero-copy hand-over: `device_ptr` addresses packed pixels in this session's own
4604    /// CUDA context — the X11 NvFBC capture buffer the NVIDIA driver composited the screen into —
4605    /// so no upload or `cuMemcpy` stands between the screen and the bitstream. The chroma convert
4606    /// reads that buffer where it lies, as it does for every other input, and a session without
4607    /// one hands the packed surface to NVENC's own conversion instead, registered in place.
4608    /// `rgba` names the byte order (`false` for the B,G,R,A the driver's native format delivers),
4609    /// and `pitch` is the buffer's row stride in bytes.
4610    ///
4611    /// The caller must keep the buffer alive and unmodified until this returns; the blocking
4612    /// bitstream lock inside means the encoder has finished reading by then, so the next capture
4613    /// may overwrite it.
4614    pub fn encode_cuda_pitch(
4615        &mut self,
4616        device_ptr: CUdeviceptr,
4617        pitch: usize,
4618        rgba: bool,
4619        frame_number: u64,
4620        crf: u32,
4621        force_idr: bool,
4622    ) -> Result<Vec<u8>, String> {
4623        unsafe {
4624            self.reconfigure_if_needed(crf);
4625            // The pitch has to cover the session's rows whichever conversion reads them: the
4626            // kernel indexes by it, and NVENC's own registration requires the 4-byte alignment.
4627            if pitch < self.width as usize * 4 || !pitch.is_multiple_of(4) {
4628                return Err(format!(
4629                    "external input pitch {pitch} does not cover {}x{} at 4-byte alignment",
4630                    self.width, self.height
4631                ));
4632            }
4633            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4634            let format = if rgba {
4635                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
4636            } else {
4637                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
4638            };
4639            let submitted = match self.convert_packed(device_ptr, pitch, rgba) {
4640                Ok(Some(converted)) => Ok(converted),
4641                Ok(None) => self
4642                    .register_external_input(device_ptr, pitch, format)
4643                    .map(|m| (m, format)),
4644                Err(e) => Err(e),
4645            };
4646            let (mapped, submitted) = match submitted {
4647                Ok(pair) => pair,
4648                Err(e) => {
4649                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4650                    return Err(e);
4651                }
4652            };
4653            let result = self.submit_frame(mapped, submitted, frame_number, force_idr);
4654            if result.is_err() {
4655                (self.cuda.cuStreamSynchronize)(ptr::null_mut());
4656            }
4657            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4658            result
4659        }
4660    }
4661
4662    /// Hash the next host frame's bands of `rows` rows on the device as it encodes, for
4663    /// `take_upload_hashes` to read after: the change detection a Turbo frame's paint-over
4664    /// reads, for the cost of a kernel beside the encode instead of a CPU pass over the frame.
4665    /// False where the driver refuses the kernel, which leaves the hash to the caller.
4666    pub fn hash_next_upload(&mut self, rows: u32) -> bool {
4667        self.upload_hashes = None;
4668        self.upload_words = None;
4669        let available = unsafe {
4670            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4671            let cuda = &self.cuda;
4672            let loaded = self
4673                .band_hash
4674                .get_or_insert_with(|| BandHash::new(cuda))
4675                .is_some();
4676            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4677            loaded
4678        };
4679        self.upload_hash = available.then_some(rows);
4680        available
4681    }
4682
4683    /// The band hashes of the host frame last coded where `hash_next_upload` asked for them.
4684    pub fn take_upload_hashes(&mut self) -> Option<Vec<u64>> {
4685        self.upload_hashes.take()
4686    }
4687
4688    /// The hash of each band of `rows` rows of the `width`x`height` packed frame at
4689    /// `device_ptr` (`pitch` bytes a row, in this session's context), computed where the frame
4690    /// lies: the change detection of a capture that reports none, NvFBC's. None where the driver
4691    /// refuses the kernel.
4692    pub fn band_hashes(
4693        &mut self,
4694        device_ptr: CUdeviceptr,
4695        pitch: usize,
4696        width: u32,
4697        height: u32,
4698        rows: u32,
4699    ) -> Option<Vec<u64>> {
4700        if pitch < width as usize * 4 {
4701            return None;
4702        }
4703        unsafe {
4704            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4705            let cuda = &self.cuda;
4706            let hashes = self
4707                .band_hash
4708                .get_or_insert_with(|| BandHash::new(cuda))
4709                .as_mut()
4710                .and_then(|h| h.run(cuda, device_ptr, pitch, width, height, rows));
4711            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4712            hashes
4713        }
4714    }
4715}
4716
4717#[cfg(test)]
4718mod tests {
4719    use super::*;
4720
4721    /// Every codec's arm declares the BT.709 matrix the session converts with, in both chroma
4722    /// formats and with no device involved. A stream whose pixels and signal disagree shifts
4723    /// every saturated color on the client, and only a GPU would otherwise report it.
4724    #[test]
4725    fn every_codec_declares_the_conversion_matrix() {
4726        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
4727            for fullcolor in [false, true] {
4728                let mut config = NV_ENC_CONFIG {
4729                    version: sv(NvStruct::Config),
4730                    ..Default::default()
4731                };
4732                NvencEncoder::configure_codec(
4733                    &mut config,
4734                    codec,
4735                    fullcolor,
4736                    8,
4737                    nvenc_level(codec, 1280, 720, 60, 0, true),
4738                    1,
4739                    0,
4740                    &NvencTuning::default(),
4741                );
4742                let got = unsafe {
4743                    match codec {
4744                        Codec::H265 => {
4745                            config
4746                                .encodeCodecConfig
4747                                .hevcConfig
4748                                .hevcVUIParameters
4749                                .colourMatrix
4750                        }
4751                        Codec::Av1 => config.encodeCodecConfig.av1Config.matrixCoefficients,
4752                        _ => {
4753                            config
4754                                .encodeCodecConfig
4755                                .h264Config
4756                                .h264VUIParameters
4757                                .colourMatrix
4758                        }
4759                    }
4760                };
4761                assert_eq!(
4762                    got as u32,
4763                    NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709 as u32,
4764                    "{codec:?} 4:4:4={fullcolor}"
4765                );
4766            }
4767        }
4768    }
4769
4770    #[test]
4771    fn anchors_require_the_negotiated_api_and_device_capacity() {
4772        for acked in [false, true] {
4773            assert_eq!(anchor_count(Codec::Av1, 12, true, Some(6), 4, acked), 0);
4774            assert_eq!(anchor_count(Codec::Av1, 13, false, Some(6), 4, acked), 0);
4775            assert_eq!(anchor_count(Codec::Av1, 13, true, None, 4, acked), 0);
4776            assert_eq!(anchor_count(Codec::Av1, 13, true, Some(0), 4, acked), 0);
4777            assert_eq!(anchor_count(Codec::Av1, 13, true, Some(6), 2, acked), 0);
4778            assert_eq!(anchor_count(Codec::Av1, 13, true, Some(1), 3, acked), 1);
4779            for api in [11, 12, 13] {
4780                assert_eq!(
4781                    anchor_count(Codec::H265, api, true, Some(8), 4, acked),
4782                    ANCHORS
4783                );
4784                for codec in [Codec::H264, Codec::H265] {
4785                    assert_eq!(anchor_count(codec, api, false, Some(8), 4, acked), 0);
4786                    assert_eq!(anchor_count(codec, api, true, Some(0), 4, acked), 0);
4787                    assert_eq!(anchor_count(codec, api, true, Some(8), 2, acked), 0);
4788                }
4789            }
4790        }
4791        assert_eq!(anchor_count(Codec::Av1, 13, true, Some(6), 4, false), 1);
4792        assert_eq!(
4793            anchor_count(Codec::Av1, 13, true, Some(6), 4, true),
4794            ANCHORS
4795        );
4796        for api in [11, 12, 13] {
4797            for acked in [false, true] {
4798                assert_eq!(anchor_count(Codec::H264, api, true, Some(8), 4, acked), 1);
4799            }
4800        }
4801        // No room for two: the one an unacknowledged session keeps.
4802        assert_eq!(anchor_count(Codec::Av1, 13, true, Some(8), 3, true), 1);
4803        assert_eq!(anchor_count(Codec::Av1, 13, true, Some(1), 4, true), 1);
4804    }
4805
4806    /// AV1's persistent reference must be enabled in its own codec arm; merely keeping an
4807    /// anchor in ReferenceWindow would report a dependency the device does not retain.
4808    #[test]
4809    fn av1_anchor_configuration_matches_the_reference_window() {
4810        for anchors in [0, 1] {
4811            let mut config = NV_ENC_CONFIG::default();
4812            NvencEncoder::configure_codec(
4813                &mut config,
4814                Codec::Av1,
4815                false,
4816                8,
4817                13,
4818                AV1_REFERENCES,
4819                anchors,
4820                &NvencTuning::default(),
4821            );
4822            let av1 = unsafe { config.encodeCodecConfig.av1Config };
4823            assert_eq!(av1.enableLTR(), anchors as u32);
4824            assert_eq!(av1.ltrNumFrames, anchors as u32);
4825            assert_eq!(av1.maxNumRefFramesInDPB, AV1_REFERENCES);
4826        }
4827    }
4828
4829    /// A band's share of the picture lands on the blocks it covers, rounded outward, so the bands
4830    /// of a sweep leave no block between them, and nowhere else.
4831    #[test]
4832    fn a_band_covers_its_share_of_the_blocks() {
4833        let mut map = Vec::new();
4834        fill_band_map(&mut map, Codec::H264, 1920, 1080, (0.0, 1.0 / 64.0), -33);
4835        assert_eq!(map.len(), 120 * 68);
4836        assert_eq!(map.iter().filter(|&&d| d == -33).count(), 128);
4837        assert!(map[..128].iter().all(|&d| d == -33) && map[128..].iter().all(|&d| d == 0));
4838        fill_band_map(&mut map, Codec::H265, 1920, 1080, (0.5, 0.75), -80);
4839        assert_eq!(map.len(), 60 * 34);
4840        assert!(map[..1020].iter().all(|&d| d == 0) && map[1530..].iter().all(|&d| d == 0));
4841        assert!(
4842            map[1020..1530].iter().all(|&d| d == -51),
4843            "the delta is bounded to the quantizer range"
4844        );
4845        fill_band_map(&mut map, Codec::Av1, 1920, 1080, (0.0, 1.0), -211);
4846        assert_eq!(map.len(), 30 * 17);
4847        assert!(
4848            map.iter().all(|&d| d == -128),
4849            "an AV1 delta is bounded to what the map holds"
4850        );
4851        let covered = |a: f64, b: f64| {
4852            let mut m = Vec::new();
4853            fill_band_map(&mut m, Codec::H264, 1280, 720, (a, b), -1);
4854            m.iter().map(|&d| d != 0).collect::<Vec<bool>>()
4855        };
4856        let (x, y) = (covered(0.0, 0.3), covered(0.3, 1.0));
4857        assert!(
4858            x.iter().zip(&y).all(|(a, b)| *a || *b),
4859            "adjacent bands leave no block out"
4860        );
4861    }
4862}
4863
4864#[cfg(test)]
4865mod gpu_tests {
4866    use super::*;
4867    use crate::encoders::reference::REFERENCE_FRAMES;
4868
4869    /// Test helper: H.264 full-frame capture settings at `w×h`, `fps`, CRF 25.
4870    fn settings(w: i32, h: i32, fps: f64) -> RustCaptureSettings {
4871        RustCaptureSettings {
4872            width: w,
4873            height: h,
4874            codec: Codec::H264,
4875            target_fps: fps,
4876            video_crf: 25,
4877            ..Default::default()
4878        }
4879    }
4880
4881    /// Test helper: a session for checks that hand it a fresh buffer every frame, which it pins
4882    /// none of: a registration outlives the buffer it page-locked, and a later buffer at an
4883    /// overlapping address then fails to upload, or uploads the pages of the freed one, where a
4884    /// capture's persistent shm is released on every reshape.
4885    fn host_session(s: &RustCaptureSettings) -> Result<NvencEncoder, String> {
4886        NvencEncoder::new(s, ptr::null()).map(|mut enc| {
4887            enc.pin_uploads = false;
4888            enc
4889        })
4890    }
4891
4892    /// Test helper: a `w×h` BGRA frame filled with a hashed gradient (offset by `seed`) so
4893    /// the content has structure and encodes are non-trivial.
4894    fn frame(w: usize, h: usize, seed: u8) -> Vec<u8> {
4895        let mut f = vec![0u8; w * h * 4];
4896        for (i, px) in f.as_chunks_mut::<4>().0.iter_mut().enumerate() {
4897            let v = ((i as u32).wrapping_mul(2654435761) >> 24) as u8;
4898            px[0] = v.wrapping_add(seed);
4899            px[1] = v ^ seed;
4900            px[2] = seed;
4901            px[3] = 255;
4902        }
4903        f
4904    }
4905
4906    /// Test helper: hand the session's `encode_config` to `nvEncReconfigureEncoder` as it stands,
4907    /// for checks that vary one rate-control field, with the driver's answer.
4908    fn reconfigure_raw(enc: &mut NvencEncoder) -> (NVENCSTATUS, String) {
4909        unsafe {
4910            let status = enc.reconfigure(false, false);
4911            let detail = if status == NVENCSTATUS::NV_ENC_SUCCESS {
4912                "accepted".to_string()
4913            } else {
4914                last_error(&enc.nvenc_funcs, enc.encoder_session)
4915            };
4916            (status, detail)
4917        }
4918    }
4919
4920    /// Test helper: how far apart, in mean luma, the last picture of `frames` decodes with and
4921    /// without the frames at the `lost` indices.
4922    fn apart_without(codec: Codec, frames: &[Vec<u8>], lost: std::ops::Range<usize>) -> f64 {
4923        use crate::webcam::decode::{Decoder as _, VideoDecoder};
4924        let (mut whole, mut lossy) = (
4925            VideoDecoder::new(codec).unwrap(),
4926            VideoDecoder::new(codec).unwrap(),
4927        );
4928        for (i, f) in frames.iter().enumerate() {
4929            assert!(whole.decode(f).expect("decode"), "{codec:?} frame {i}");
4930            if !lost.contains(&i) {
4931                assert!(
4932                    lossy.decode(f).expect("decode past the loss"),
4933                    "{codec:?} frame {i}"
4934                );
4935            }
4936        }
4937        luma_apart(&whole.frame().unwrap(), &lossy.frame().unwrap())
4938    }
4939
4940    /// Test helper: the mean luma distance between two decoded pictures.
4941    fn luma_apart(
4942        a: &crate::webcam::convert::I420View<'_>,
4943        b: &crate::webcam::convert::I420View<'_>,
4944    ) -> f64 {
4945        a.y.chunks(a.y_stride)
4946            .zip(b.y.chunks(b.y_stride))
4947            .take(a.height)
4948            .flat_map(|(ra, rb)| {
4949                ra[..a.width]
4950                    .iter()
4951                    .zip(&rb[..a.width])
4952                    .map(|(&x, &y)| (x as f64 - y as f64).abs())
4953            })
4954            .sum::<f64>()
4955            / (a.width * a.height) as f64
4956    }
4957
4958    /// Test helper: the mean luma distance between the last picture decoded from every frame and
4959    /// the last one decoded from the frames at the `keep` indices alone; None where those left the
4960    /// last one undecodable.
4961    fn apart_keeping(codec: Codec, frames: &[Vec<u8>], keep: &[usize]) -> Option<f64> {
4962        use crate::webcam::decode::{Decoder as _, VideoDecoder};
4963        let (mut whole, mut part) = (
4964            VideoDecoder::new(codec).unwrap(),
4965            VideoDecoder::new(codec).unwrap(),
4966        );
4967        let mut got = false;
4968        for (i, f) in frames.iter().enumerate() {
4969            let _ = whole.decode(f);
4970            if keep.contains(&i) {
4971                got = part.decode(f).unwrap_or(false) && i == frames.len() - 1;
4972            }
4973        }
4974        if !got {
4975            return None;
4976        }
4977        Some(luma_apart(&whole.frame()?, &part.frame()?))
4978    }
4979
4980    /// Test helper: the gradient frame with a 256x256 block of another gradient moved `step`
4981    /// blocks along its top rows, the frames of a steady desktop-like sequence.
4982    fn moving_frame(w: usize, h: usize, step: usize) -> Vec<u8> {
4983        let mut f = frame(w, h, 10);
4984        let block = frame(256, 256, 200);
4985        let x0 = (step * 64) % (w - 256);
4986        for row in 0..256.min(h) {
4987            let dst = (row * w + x0) * 4;
4988            f[dst..dst + 256 * 4].copy_from_slice(&block[row * 256 * 4..(row + 1) * 256 * 4]);
4989        }
4990        f
4991    }
4992
4993    /// Test helper: a NAL unit with its emulation-prevention bytes removed.
4994    fn rbsp(nal: &[u8]) -> Vec<u8> {
4995        let mut out = Vec::with_capacity(nal.len());
4996        let mut zeros = 0usize;
4997        for &b in nal {
4998            if zeros >= 2 && b == 3 {
4999                zeros = 0;
5000                continue;
5001            }
5002            zeros = if b == 0 { zeros + 1 } else { 0 };
5003            out.push(b);
5004        }
5005        out
5006    }
5007
5008    /// Test helper: luma PSNR of one decoded access unit against the BGRA frame it encodes, the
5009    /// source taken through the limited-range BT.709 luma the session converts with.
5010    fn luma_psnr(
5011        dec: &mut crate::webcam::decode::VideoDecoder,
5012        pkt: &[u8],
5013        src: &[u8],
5014        w: usize,
5015        h: usize,
5016    ) -> f64 {
5017        use crate::webcam::decode::Decoder;
5018        assert!(
5019            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
5020            "no picture"
5021        );
5022        let v = dec.frame().expect("decoded frame");
5023        let mut se = 0f64;
5024        for y in 0..h {
5025            for x in 0..w {
5026                let p = &src[(y * w + x) * 4..(y * w + x) * 4 + 3];
5027                let luma = crate::encoders::chroma_siting::ycbcr(
5028                    [p[2], p[1], p[0]].map(f64::from),
5029                    crate::encoders::chroma_siting::BT709,
5030                )[0];
5031                let d = v.y[y * v.y_stride + x] as f64 - luma;
5032                se += d * d;
5033            }
5034        }
5035        let mse = se / (w * h) as f64;
5036        if mse == 0.0 {
5037            99.0
5038        } else {
5039            10.0 * (255.0f64 * 255.0 / mse).log10()
5040        }
5041    }
5042
5043    /// Test helper: one CBR bench row. Encodes `seq` on `enc`, its first frame as the warm-up key
5044    /// frame, and prints the achieved rate at `fps`, the smallest and largest frame, and the luma
5045    /// PSNR of every decoded frame against its source.
5046    fn cbr_row(
5047        label: &str,
5048        enc: &mut NvencEncoder,
5049        codec: Codec,
5050        seq: &[&Vec<u8>],
5051        w: usize,
5052        h: usize,
5053        fps: usize,
5054    ) {
5055        use crate::webcam::decode::VideoDecoder;
5056        let n = seq.len() - 1;
5057        let first = enc
5058            .encode_cpu_packed(seq[0], w * 4, false, 0, 25, true)
5059            .expect("warm-up");
5060        let mut pkts: Vec<Vec<u8>> = Vec::with_capacity(n);
5061        per_frame(label, n, |i| {
5062            pkts.push(
5063                enc.encode_cpu_packed(seq[1 + i], w * 4, false, 1 + i as u64, 25, false)
5064                    .expect("encode"),
5065            );
5066        });
5067        let sizes: Vec<usize> = pkts
5068            .iter()
5069            .map(|p| p.len().saturating_sub(VIDEO_HEADER_LEN))
5070            .collect();
5071        let bytes: usize = sizes.iter().sum();
5072        let mut dec = VideoDecoder::new(codec).expect("decoder");
5073        luma_psnr(&mut dec, &first, seq[0], w, h);
5074        let psnr: Vec<f64> = pkts
5075            .iter()
5076            .enumerate()
5077            .map(|(i, p)| luma_psnr(&mut dec, p, seq[1 + i], w, h))
5078            .collect();
5079        println!(
5080            "    {} kbps, frames {}..{} kbit, luma PSNR {:.1} dB mean, {:.1} dB worst",
5081            bytes * 8 * fps / n / 1000,
5082            sizes.iter().min().unwrap() * 8 / 1000,
5083            sizes.iter().max().unwrap() * 8 / 1000,
5084            psnr.iter().sum::<f64>() / n as f64,
5085            psnr.iter().cloned().fold(f64::INFINITY, f64::min),
5086        );
5087    }
5088
5089    /// Test helper: read the big-endian width/height (bytes 6-9) from the wire header.
5090    fn wire_dims(pkt: &[u8]) -> (u16, u16) {
5091        (
5092            u16::from_be_bytes([pkt[6], pkt[7]]),
5093            u16::from_be_bytes([pkt[8], pkt[9]]),
5094        )
5095    }
5096
5097    /// End-to-end in-place resize on a real GPU: encode 720p, grow to 1080p and verify the
5098    /// first post-resize frame is an IDR (steady frames are P), shrink to 480p, exercise the
5099    /// rejection cases (beyond headroom, chroma flip, RC-mode flip) and confirm the session still
5100    /// encodes afterward, and optionally dump a decodable stream. Ignored by default; run with
5101    /// `cargo test gpu_ -- --ignored --nocapture --test-threads=1` — building NVENC sessions
5102    /// concurrently races in the driver and intermittently faults, so the GPU set runs serially.
5103    #[test]
5104    #[ignore]
5105    fn gpu_resolution_reconfigure_roundtrip() {
5106        let mut s = settings(1280, 720, 60.0);
5107        let t0 = std::time::Instant::now();
5108        let mut enc = host_session(&s).expect("NVENC init");
5109        let init_ms = t0.elapsed().as_secs_f64() * 1000.0;
5110
5111        let mut stream: Vec<u8> = Vec::new();
5112        let f720 = frame(1280, 720, 10);
5113        for i in 0..5u64 {
5114            let pkt = enc
5115                .encode_cpu_argb(&f720, 1280 * 4, i, 25, i == 0)
5116                .expect("encode 720p");
5117            assert_eq!(wire_dims(&pkt), (1280, 720));
5118            stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5119        }
5120
5121        assert!(
5122            !enc.reconfigure_resolution(&s)
5123                .expect("same-size reconfigure"),
5124            "unchanged dimensions must not reset the session"
5125        );
5126        let pkt = enc
5127            .encode_cpu_argb(&f720, 1280 * 4, 5, 25, false)
5128            .expect("encode after same-size reconfigure");
5129        assert_eq!(
5130            pkt[1] & 0x0f,
5131            FRAME_DELTA,
5132            "the stream continues without an IDR at unchanged dimensions"
5133        );
5134        assert!(
5135            pkt.len() > VIDEO_HEADER_LEN,
5136            "a locked bitstream carries the encoded picture"
5137        );
5138        stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5139
5140        s.width = 1920;
5141        s.height = 1080;
5142        let t1 = std::time::Instant::now();
5143        assert!(enc.reconfigure_resolution(&s).expect("grow reconfigure"));
5144        let grow_ms = t1.elapsed().as_secs_f64() * 1000.0;
5145        let f1080 = frame(1920, 1080, 40);
5146        let pkt = enc
5147            .encode_cpu_argb(&f1080, 1920 * 4, 6, 25, false)
5148            .expect("encode 1080p");
5149        assert_eq!(pkt[0], 0x04);
5150        assert_eq!(
5151            pkt[1] & 0x0f,
5152            FRAME_KEY,
5153            "first frame after a resize must be an IDR"
5154        );
5155        assert_eq!(wire_dims(&pkt), (1920, 1080));
5156        assert!(
5157            pkt.len() > VIDEO_HEADER_LEN,
5158            "a locked bitstream carries the encoded picture"
5159        );
5160        stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5161        for i in 7..10u64 {
5162            let pkt = enc
5163                .encode_cpu_argb(&f1080, 1920 * 4, i, 25, false)
5164                .expect("encode 1080p");
5165            assert_eq!(
5166                pkt[1] & 0x0f,
5167                FRAME_DELTA,
5168                "steady frames after the IDR are P frames"
5169            );
5170            stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5171        }
5172
5173        s.width = 640;
5174        s.height = 480;
5175        let t2 = std::time::Instant::now();
5176        assert!(enc.reconfigure_resolution(&s).expect("shrink reconfigure"));
5177        let shrink_ms = t2.elapsed().as_secs_f64() * 1000.0;
5178        let f480 = frame(640, 480, 70);
5179        let pkt = enc
5180            .encode_cpu_argb(&f480, 640 * 4, 10, 25, false)
5181            .expect("encode 480p");
5182        assert_eq!(pkt[1] & 0x0f, FRAME_KEY);
5183        assert_eq!(wire_dims(&pkt), (640, 480));
5184        stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5185
5186        s.width = 4100;
5187        s.height = 2400;
5188        assert!(enc.reconfigure_resolution(&s).is_err(), "beyond headroom");
5189        s.width = 640;
5190        s.height = 480;
5191        s.video_fullcolor = true;
5192        assert!(enc.reconfigure_resolution(&s).is_err(), "chroma flip");
5193        s.video_fullcolor = false;
5194        s.video_cbr_mode = true;
5195        assert!(enc.reconfigure_resolution(&s).is_err(), "RC mode flip");
5196        s.video_cbr_mode = false;
5197        let pkt = enc
5198            .encode_cpu_argb(&f480, 640 * 4, 11, 25, false)
5199            .expect("session survives rejected reconfigures");
5200        stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5201
5202        println!(
5203            "init={init_ms:.1}ms grow(720p->1080p)={grow_ms:.1}ms shrink(1080p->480p)={shrink_ms:.1}ms"
5204        );
5205        if let Ok(path) = std::env::var("NVENC_TEST_DUMP") {
5206            std::fs::write(&path, &stream).unwrap();
5207            println!("wrote {} bytes to {path}", stream.len());
5208        }
5209    }
5210
5211    /// On a real GPU: every codec the device lists an engine for comes up on host frames
5212    /// without loading EGL, frames it emits carry the codec's wire id and a kind the
5213    /// bitstream agrees with, decode back
5214    /// to the painted picture, and a key frame forced mid-stream starts a fresh decoder on
5215    /// its own; a codec the device lacks (AV1 before Ada) is refused with a message naming
5216    /// it. HEVC 4:4:4 is taken where the device carries it, AV1 never quietly. Ignored by
5217    /// default.
5218    #[test]
5219    #[ignore]
5220    fn gpu_codec_sessions_encode_and_decode() {
5221        use crate::encoders::codec::{
5222            FRAME_DELTA, FRAME_KEY, av1_is_key, h264_frame_type, h265_frame_type, parse_video_type,
5223        };
5224        use crate::webcam::decode::{Decoder, VideoDecoder};
5225        let (w, h) = (1280usize, 720usize);
5226        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5227            let mut s = settings(w as i32, h as i32, 60.0);
5228            s.codec = codec;
5229            let mut enc = match host_session(&s) {
5230                Ok(enc) => enc,
5231                Err(e) => {
5232                    println!("{codec:?}: {e}");
5233                    assert!(
5234                        e.contains("engine"),
5235                        "a missing codec must be refused as such: {e}"
5236                    );
5237                    continue;
5238                }
5239            };
5240            assert_eq!(enc.codec(), codec);
5241            assert!(enc.egl.is_none(), "a session on host frames loads no EGL");
5242            let mut dec = VideoDecoder::new(codec).expect("decoder");
5243            for i in 0..6u64 {
5244                let src = frame(w, h, 10 + i as u8);
5245                let pkt = enc
5246                    .encode_cpu_argb(&src, w * 4, i, 25, i == 0)
5247                    .expect("encode");
5248                let (wire_codec, kind) = parse_video_type(pkt[1]).expect("video type byte");
5249                assert_eq!(wire_codec, codec);
5250                assert_eq!(
5251                    kind,
5252                    if i == 0 { FRAME_KEY } else { FRAME_DELTA },
5253                    "{codec:?} frame {i}"
5254                );
5255                let payload = &pkt[VIDEO_HEADER_LEN..];
5256                if codec != Codec::Av1 {
5257                    assert!(
5258                        payload.starts_with(&[0, 0, 0, 1]),
5259                        "{codec:?} frame {i}: the bitstream starts right after the header"
5260                    );
5261                }
5262                let read = match codec {
5263                    Codec::H264 => h264_frame_type(payload),
5264                    Codec::H265 => h265_frame_type(payload),
5265                    _ => {
5266                        if av1_is_key(payload) {
5267                            FRAME_KEY
5268                        } else {
5269                            FRAME_DELTA
5270                        }
5271                    }
5272                };
5273                assert_eq!(
5274                    read, kind,
5275                    "{codec:?} frame {i}: the bitstream disagrees with pictureType"
5276                );
5277                assert!(
5278                    dec.decode(payload).expect("decode"),
5279                    "{codec:?} frame {i} decoded nothing"
5280                );
5281                let f = dec.frame().unwrap();
5282                assert_eq!((f.width, f.height), (w, h));
5283            }
5284            let key = enc
5285                .encode_cpu_argb(&frame(w, h, 99), w * 4, 6, 25, true)
5286                .expect("forced key");
5287            assert_eq!(parse_video_type(key[1]), Some((codec, FRAME_KEY)));
5288            let mut fresh = VideoDecoder::new(codec).expect("decoder");
5289            assert!(
5290                fresh.decode(&key[VIDEO_HEADER_LEN..]).expect("decode"),
5291                "{codec:?}: a forced key frame must decode alone"
5292            );
5293
5294            let mut full = s.clone();
5295            full.video_fullcolor = true;
5296            let enc = host_session(&full).expect("4:4:4 request");
5297            if codec.fullcolor() {
5298                println!("{codec:?} 4:4:4: {}", enc.is_fullcolor());
5299            } else {
5300                assert!(!enc.is_fullcolor(), "{codec:?} never carries 4:4:4");
5301            }
5302        }
5303    }
5304
5305    /// On a real GPU: a 10-bit request is coded at 10 bits in each format the probe lists them
5306    /// for and at 8 in every other, through the convert that writes the 10-bit surface, and an
5307    /// in-place resize keeps it. The decoders here take 8-bit streams alone, so a 10-bit key
5308    /// frame is one they refuse for its depth. Ignored by default.
5309    #[test]
5310    #[ignore]
5311    fn gpu_ten_bit_follows_the_device() {
5312        use crate::webcam::decode::{Decoder, VideoDecoder};
5313        let (w, h) = (1280usize, 720usize);
5314        for (codec, formats) in probe_codecs(0).expect("probe") {
5315            for fullcolor in [false, codec.fullcolor()] {
5316                let mut s = settings(w as i32, h as i32, 60.0);
5317                s.codec = codec;
5318                s.video_fullcolor = fullcolor;
5319                s.video_bit_depth = 10;
5320                let mut enc = host_session(&s).expect("session");
5321                let carried = formats.ten_bit[(fullcolor && formats.fullcolor) as usize];
5322                println!("{codec:?} 4:4:4 {fullcolor}: {}-bit", enc.bit_depth());
5323                assert_eq!(enc.bit_depth(), if carried { 10 } else { 8 }, "{codec:?}");
5324                assert_eq!(
5325                    enc.csc.as_ref().map(|c| c.layout),
5326                    ConvertLayout::of(enc.is_fullcolor(), enc.bit_depth()),
5327                    "{codec:?}"
5328                );
5329                let key = enc
5330                    .encode_cpu_argb(&frame(w, h, 1), w * 4, 0, 25, true)
5331                    .expect("key frame");
5332                let decoded = VideoDecoder::new(codec)
5333                    .expect("decoder")
5334                    .decode(&key[VIDEO_HEADER_LEN..]);
5335                if carried {
5336                    let refusal = format!("{:?}", decoded.expect_err("an 8-bit decoder"));
5337                    assert!(refusal.contains("bit depth"), "{codec:?}: {refusal}");
5338                } else if !enc.is_fullcolor() {
5339                    assert!(decoded.expect("decode"), "{codec:?}");
5340                }
5341                s.width = 1920;
5342                s.height = 1080;
5343                enc.reconfigure_resolution(&s).expect("resize");
5344                assert_eq!(enc.bit_depth(), if carried { 10 } else { 8 });
5345                enc.encode_cpu_argb(&frame(1920, 1080, 2), 1920 * 4, 1, 25, false)
5346                    .expect("a frame after the resize");
5347                s.video_bit_depth = 8;
5348                assert_eq!(
5349                    enc.reconfigure_resolution(&s).is_err(),
5350                    carried,
5351                    "{codec:?}: a depth change rebuilds the session"
5352                );
5353            }
5354        }
5355    }
5356
5357    /// A frame a client lost is left out of the device's predictions: the next frame predicts
5358    /// from the newest frame before it and names it, a decoder that never saw the lost frames
5359    /// decodes it as one that saw everything does, the stream declares the decoded picture
5360    /// buffer the level admits, and an in-place resize redeclares it: a loss as deep as the new
5361    /// buffer's recent frames reach is still predicted past, a deeper one from the key frame the
5362    /// resize forced where an anchor is kept, and losing the last reference costs a key frame. A
5363    /// device that cannot invalidate a reference tracks none and says so. Ignored by default.
5364    #[test]
5365    #[ignore]
5366    fn gpu_predicts_past_a_lost_frame() {
5367        use crate::encoders::reference::Reference;
5368        use crate::encoders::sps::h264_max_num_ref_frames;
5369        let (w, h) = (1280usize, 720usize);
5370        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5371            let mut s = settings(w as i32, h as i32, 60.0);
5372            s.codec = codec;
5373            s.omit_stripe_headers = true;
5374            let mut enc = match host_session(&s) {
5375                Ok(enc) => enc,
5376                Err(e) => {
5377                    println!("{codec:?}: {e}");
5378                    continue;
5379                }
5380            };
5381            let encode = |enc: &mut NvencEncoder, i: usize, w: usize, h: usize| {
5382                let out = enc
5383                    .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
5384                    .expect("encode");
5385                (out, enc.last_reference())
5386            };
5387            let (first, reference) = encode(&mut enc, 0, w, h);
5388            if reference == Reference::Untracked {
5389                println!(
5390                    "{codec:?}: this device cannot invalidate a reference, so nothing is tracked"
5391                );
5392                assert!(!enc.invalidate_reference(0));
5393                continue;
5394            }
5395            assert_eq!(reference, Reference::None);
5396            if codec == Codec::H264 {
5397                assert_eq!(
5398                    h264_max_num_ref_frames(&first),
5399                    Some(REFERENCE_FRAMES),
5400                    "the SPS declares the DPB"
5401                );
5402            }
5403            let mut frames = vec![first];
5404            for i in 1..8 {
5405                let (out, reference) = encode(&mut enc, i, w, h);
5406                assert_eq!(
5407                    reference,
5408                    Reference::Frame(i as u16 - 1),
5409                    "{codec:?} frame {i}"
5410                );
5411                frames.push(out);
5412            }
5413            assert!(
5414                enc.invalidate_reference(5),
5415                "{codec:?}: the device refused the invalidation"
5416            );
5417            let (out, reference) = encode(&mut enc, 8, w, h);
5418            // AV1's four-slot DPB leaves three recent pictures beside its one anchor.
5419            // The oldest recent picture (4) is already gone, so it predicts from anchor 0.
5420            let before_loss = if codec == Codec::Av1
5421                && enc
5422                    .references
5423                    .as_ref()
5424                    .is_some_and(ReferenceWindow::anchored)
5425            {
5426                0
5427            } else {
5428                4
5429            };
5430            assert_eq!(reference, Reference::Frame(before_loss), "{codec:?}");
5431            frames.push(out);
5432            let (out, reference) = encode(&mut enc, 9, w, h);
5433            assert_eq!(reference, Reference::Frame(8), "{codec:?}");
5434            frames.push(out);
5435            let off = apart_without(codec, &frames, 5..8);
5436            println!("{codec:?}: frame 9 without frames 5-7 is {off:.3} off the complete decode");
5437            assert!(
5438                off < 0.5,
5439                "{codec:?}: the decoder that lost frames 5-7 shows frame 9 {off:.2} off the one that saw them"
5440            );
5441            // A grow to a level admitting fewer frames declares the smaller buffer at the IDR the
5442            // resize forces.
5443            s.width = 1920;
5444            s.height = 1080;
5445            assert!(
5446                enc.reconfigure_resolution(&s).expect("in-place grow"),
5447                "{codec:?}"
5448            );
5449            let dpb = enc.dpb as usize;
5450            let anchored = enc.references.as_ref().unwrap().anchored();
5451            let (out, reference) = encode(&mut enc, 10, 1920, 1080);
5452            assert_eq!(reference, Reference::None);
5453            if codec == Codec::H264 {
5454                assert_eq!(
5455                    h264_max_num_ref_frames(&out),
5456                    Some(4),
5457                    "1080p at level 4.2 admits four"
5458                );
5459            }
5460            let mut frames = vec![out];
5461            for i in 11..=10 + dpb {
5462                let (out, reference) = encode(&mut enc, i, 1920, 1080);
5463                assert_eq!(
5464                    reference,
5465                    Reference::Frame(i as u16 - 1),
5466                    "{codec:?} frame {i}"
5467                );
5468                frames.push(out);
5469            }
5470            assert!(
5471                enc.invalidate_reference(12),
5472                "{codec:?}: the device refused the invalidation"
5473            );
5474            let (out, reference) = encode(&mut enc, 11 + dpb, 1920, 1080);
5475            assert_eq!(
5476                reference,
5477                Reference::Frame(if anchored { 10 } else { 11 }),
5478                "{codec:?}: use the newest reference still held before the loss"
5479            );
5480            frames.push(out);
5481            let (out, reference) = encode(&mut enc, 12 + dpb, 1920, 1080);
5482            assert_eq!(reference, Reference::Frame(11 + dpb as u16), "{codec:?}");
5483            frames.push(out);
5484            let off = apart_without(codec, &frames, 2..dpb + 1);
5485            println!(
5486                "{codec:?}: at 1080p, frame {} without frames 12-{} is {off:.3} off the complete decode",
5487                12 + dpb,
5488                10 + dpb
5489            );
5490            assert!(
5491                off < 0.5,
5492                "{codec:?}: the decoder that lost frames 12-{} shows {off:.2} off the one that saw them",
5493                10 + dpb
5494            );
5495            assert!(enc.invalidate_reference(11));
5496            assert_eq!(
5497                encode(&mut enc, 13 + dpb, 1920, 1080).1,
5498                if anchored {
5499                    Reference::Frame(10)
5500                } else {
5501                    Reference::None
5502                },
5503                "{codec:?}: losing frame 11 requires the older anchor or a key frame"
5504            );
5505            assert!(enc.invalidate_reference(10));
5506            assert_eq!(
5507                encode(&mut enc, 14 + dpb, 1920, 1080).1,
5508                if anchored {
5509                    Reference::None
5510                } else {
5511                    Reference::Frame(13 + dpb as u16)
5512                },
5513                "{codec:?}: losing the anchor requires a key frame; a report before a new key frame is ignored"
5514            );
5515        }
5516    }
5517
5518    /// A report for a frame an earlier invalidation already left out keeps the chain predicted
5519    /// since: lose 80 and 81, recover from 79, and the report for 81 that arrives after 98 lets 99
5520    /// predict from 98 rather than cost a key frame; a loss in the new chain still invalidates
5521    /// it. The stream without the lost frames decodes as the complete one does. Ignored by
5522    /// default.
5523    #[test]
5524    #[ignore]
5525    fn gpu_a_covered_loss_keeps_the_recovered_chain() {
5526        use crate::encoders::reference::Reference;
5527        let (w, h) = (1920usize, 1080usize);
5528        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5529            let mut s = settings(w as i32, h as i32, 60.0);
5530            s.codec = codec;
5531            s.omit_stripe_headers = true;
5532            let mut enc = match host_session(&s) {
5533                Ok(enc) => enc,
5534                Err(e) => {
5535                    println!("{codec:?}: {e}");
5536                    continue;
5537                }
5538            };
5539            let mut frames = Vec::new();
5540            for i in 0..=107usize {
5541                let (lost, expect) = match i {
5542                    82 => (Some(80), Some(79)),
5543                    99 => (Some(81), Some(98)),
5544                    107 => (Some(106), Some(105)),
5545                    _ => (None, None),
5546                };
5547                if let Some(lost) = lost {
5548                    assert!(enc.invalidate_reference(lost), "{codec:?}: lost {lost}");
5549                }
5550                frames.push(
5551                    enc.encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
5552                        .expect("encode"),
5553                );
5554                if i == 0 && enc.last_reference() == Reference::Untracked {
5555                    break;
5556                }
5557                if let Some(from) = expect {
5558                    assert_eq!(
5559                        enc.last_reference(),
5560                        Reference::Frame(from),
5561                        "{codec:?} {i}"
5562                    );
5563                }
5564            }
5565            if enc.last_reference() == Reference::Untracked {
5566                println!("{codec:?}: this device cannot invalidate a reference");
5567                continue;
5568            }
5569            let received: Vec<usize> = (0..frames.len())
5570                .filter(|i| !matches!(i, 80 | 81 | 106))
5571                .collect();
5572            let off = apart_keeping(codec, &frames, &received);
5573            assert!(
5574                off.is_some_and(|x| x < 0.5),
5575                "{codec:?}: without frames 80, 81 and 106 the last frame is {off:?} off"
5576            );
5577            println!("{codec:?}: frame 99 predicts from 98 after the covered report, {off:?} off");
5578        }
5579    }
5580
5581    /// A held refresh of the whole picture past `HELD_REFRESH_LIMIT_S` of a constant rate is coded
5582    /// again to fit `HELD_KEY_BUDGET_S`, and one under it stands. The frame coded again predicts
5583    /// from the frame before the first attempt, which the device leaves out as it does a lost
5584    /// frame: a decoder that never sees the attempt decodes what follows as one that saw it does,
5585    /// and a loss reported for the frame sent in its place leaves that frame out. Ignored by
5586    /// default.
5587    #[test]
5588    #[ignore]
5589    fn gpu_a_held_refresh_past_its_limit_is_coded_again() {
5590        use crate::encoders::reference::Reference;
5591        let (w, h) = (1280usize, 720usize);
5592        let still = frame(w, h, 10);
5593        let encode = |enc: &mut NvencEncoder, i: u64, held: Option<u32>| {
5594            if let Some(crf) = held {
5595                enc.hold_quantizer(crf, None);
5596            }
5597            let out = enc
5598                .encode_cpu_argb(&still, w * 4, i, 25, i == 0)
5599                .expect("encode");
5600            (out, enc.last_reference())
5601        };
5602        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5603            let mut s = settings(w as i32, h as i32, 60.0);
5604            s.codec = codec;
5605            s.omit_stripe_headers = true;
5606            s.video_cbr_mode = true;
5607            s.video_bitrate_kbps = 50_000;
5608            let mut enc = match host_session(&s) {
5609                Ok(enc) => enc,
5610                Err(e) => {
5611                    println!("{codec:?}: {e}");
5612                    continue;
5613                }
5614            };
5615            let mut frames: Vec<_> = (0..4).map(|i| encode(&mut enc, i, None).0).collect();
5616            if enc.last_reference() == Reference::Untracked {
5617                println!("{codec:?}: this device cannot invalidate a reference");
5618                continue;
5619            }
5620            // An attempt within the limit of this rate, taken back as the session takes back one
5621            // past it.
5622            let (attempt, _) = encode(&mut enc, 4, Some(20));
5623            assert!(enc.retract_last(), "{codec:?}: the device refused");
5624            frames.push(attempt);
5625            let (out, reference) = encode(&mut enc, 4, Some(40));
5626            assert_eq!(reference, Reference::Frame(3), "{codec:?}");
5627            frames.push(out);
5628            let (out, reference) = encode(&mut enc, 5, None);
5629            assert_eq!(reference, Reference::Frame(4), "{codec:?}");
5630            frames.push(out);
5631            let off = apart_without(codec, &frames, 4..5);
5632            assert!(
5633                off < 0.5,
5634                "{codec:?}: without the attempt the frames after it decode {off:.2} off"
5635            );
5636            assert!(enc.invalidate_reference(4), "{codec:?}");
5637            let (out, reference) = encode(&mut enc, 6, None);
5638            assert!(
5639                matches!(reference, Reference::Frame(id) if id < 4),
5640                "{codec:?}: {reference:?} after losing the frame sent in place of the attempt"
5641            );
5642            frames.push(out);
5643            let lost = apart_without(codec, &frames, 4..7);
5644            assert!(
5645                lost < 0.5,
5646                "{codec:?}: past the loss the frame decodes {lost:.2} off"
5647            );
5648            // Past `HELD_REFRESH_LIMIT_S` of the target the session codes such a frame again on its
5649            // own (AV1 on 595.91.07 caps a held frame at a second of the target itself); under it,
5650            // the frame stands.
5651            for (kbps, first) in [(100, 7u64), (1000, 11)] {
5652                s.video_bitrate_kbps = kbps;
5653                assert!(enc.reconfigure_rate(&s), "{codec:?}");
5654                for i in first..first + 3 {
5655                    encode(&mut enc, i, None);
5656                }
5657                let pts = |enc: &NvencEncoder| enc.references.as_ref().map_or(0, |r| r.next_pts());
5658                let before = pts(&enc);
5659                let (out, reference) = encode(&mut enc, first + 3, Some(20));
5660                assert_eq!(reference, Reference::Frame(first as u16 + 2), "{codec:?}");
5661                let spent = out.len() as f64 / (kbps as f64 * 1000.0 / 8.0);
5662                let coded = pts(&enc) - before;
5663                println!(
5664                    "{codec:?}: a held frame at {kbps} kbps goes out at {} bytes, {spent:.2} s of the target, encoded {coded} times",
5665                    out.len()
5666                );
5667                assert!(spent <= crate::encoders::HELD_REFRESH_LIMIT_S, "{codec:?}");
5668                if kbps == 1000 {
5669                    assert_eq!(coded, 1, "{codec:?}: a frame under the limit stands");
5670                }
5671            }
5672        }
5673    }
5674
5675    /// Two consumers share a session told which frames both hold: one takes every frame, the other
5676    /// misses stretches, everything but the anchors and, where there are two, then everything,
5677    /// and once it has room reports the frame its missing run starts with. Of two anchors, the
5678    /// newest both hold stays pinned while the schedule marks the other, which predicts from the
5679    /// frame before it (both hold none of the two newest), so the one behind misses the anchors
5680    /// too; the one behind decodes each frame it is sent as the other does, the first after its
5681    /// report predicting from the anchor it holds; and no frame after the first is a key frame.
5682    /// H.264 keeps one anchor, a frame every forty-eight, and marks none while the one behind
5683    /// holds no recent frame, which then predicts past from the one it holds. Ignored by default.
5684    #[test]
5685    #[ignore]
5686    fn gpu_acknowledged_anchors_carry_a_consumer_left_behind() {
5687        use crate::encoders::codec::parse_video_type;
5688        use crate::encoders::reference::Reference;
5689        use crate::webcam::decode::{Decoder as _, VideoDecoder};
5690        let (w, h) = (1280usize, 720usize);
5691        // (frames it has no room for, anchors too, the frame its report goes ahead of, the anchor
5692        // the frame after the report predicts from), for two anchors and for one
5693        let pinned = [(13..=40, false, 42, 12u16), (61..=100, true, 102, 60)];
5694        let alone = [(49..=90, false, 92, 48u16), (97..=150, false, 152, 96)];
5695        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5696            let mut s = settings(w as i32, h as i32, 60.0);
5697            s.codec = codec;
5698            s.acknowledge_references = true;
5699            let mut enc = match host_session(&s) {
5700                Ok(enc) => enc,
5701                Err(e) => {
5702                    println!("{codec:?}: {e}");
5703                    continue;
5704                }
5705            };
5706            let anchors = enc
5707                .references
5708                .as_ref()
5709                .map_or(0, ReferenceWindow::anchor_count);
5710            let (stretches, period, last) = match anchors {
5711                ANCHORS => (&pinned, 12, 120),
5712                1 => (&alone, 48, 160),
5713                _ => {
5714                    println!("{codec:?}: this device or API keeps no long-term reference");
5715                    continue;
5716                }
5717            };
5718            let (mut whole, mut part) = (
5719                VideoDecoder::new(codec).unwrap(),
5720                VideoDecoder::new(codec).unwrap(),
5721            );
5722            let mut holds = Vec::new();
5723            let mut run = None;
5724            let (mut worst, mut sizes) = (0f64, (0usize, 0usize, 0usize, 0usize));
5725            for i in 0..=last {
5726                let stretch = stretches.iter().find(|t| t.0.contains(&i));
5727                if let Some(&(_, _, _, from)) = stretches.iter().find(|t| t.2 == i) {
5728                    let first = run.take().expect("a missing run");
5729                    assert!(enc.take_report(ReferenceReport::Lost(first)), "{codec:?}");
5730                    assert_eq!(
5731                        enc.references
5732                            .as_ref()
5733                            .and_then(ReferenceWindow::newest_valid)
5734                            .map(|f| f.0),
5735                        Some(from),
5736                        "{codec:?}: {first} reported lost before {i}"
5737                    );
5738                }
5739                let out = enc
5740                    .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
5741                    .expect("encode");
5742                let (_, kind) = parse_video_type(out[1]).expect("video type byte");
5743                assert_eq!(
5744                    kind == FRAME_KEY,
5745                    i == 0,
5746                    "{codec:?}: frame {i} is kind {kind}"
5747                );
5748                let reference = enc.last_reference();
5749                let can = match reference {
5750                    Reference::None => true,
5751                    Reference::Frame(r) => holds[r as usize],
5752                    Reference::Untracked => false,
5753                };
5754                let anchor = out[1] & FRAME_ANCHOR != 0;
5755                if anchor {
5756                    assert!(
5757                        can,
5758                        "{codec:?}: anchor {i} predicts from {reference:?}, held by one"
5759                    );
5760                }
5761                assert_eq!(
5762                    anchor,
5763                    i > 0 && i % period == 0 && can,
5764                    "{codec:?}: frame {i} predicting from {reference:?}"
5765                );
5766                let sent = can && stretch.is_none_or(|t| anchor && !t.1);
5767                let bits = &out[VIDEO_HEADER_LEN..];
5768                assert!(whole.decode(bits).unwrap_or(false), "{codec:?}: frame {i}");
5769                if sent {
5770                    assert!(
5771                        part.decode(bits).unwrap_or(false),
5772                        "{codec:?}: frame {i} sent"
5773                    );
5774                    let off = luma_apart(&whole.frame().unwrap(), &part.frame().unwrap());
5775                    assert!(off < 0.5, "{codec:?}: frame {i} sent is {off:.3} off");
5776                    worst = worst.max(off);
5777                    run = None;
5778                    enc.acknowledge_reference(i as u16, true);
5779                } else if can {
5780                    run = Some(i as u16);
5781                }
5782                holds.push(sent);
5783                if i > 0 {
5784                    let bucket = if anchor { &mut sizes.2 } else { &mut sizes.0 };
5785                    *bucket += out.len();
5786                    if anchor { sizes.3 += 1 } else { sizes.1 += 1 }
5787                }
5788            }
5789            println!(
5790                "{codec:?}: worst {worst:.3} apart; {} B a frame, {} B an anchor ({} anchors)",
5791                sizes.0 / sizes.1.max(1),
5792                sizes.2 / sizes.3.max(1),
5793                sizes.3
5794            );
5795        }
5796    }
5797
5798    /// With the second anchor lost, a loss of the two newest frames is predicted past from the
5799    /// newest frame the device still holds, and the window names it: on AV1 one past the recent
5800    /// frames beside two anchors, held in the long-term slot the lost anchor lends. A decoder
5801    /// given the frames up to it, and none after, decodes the next frame as one given every frame
5802    /// does. Ignored by default.
5803    #[test]
5804    #[ignore]
5805    fn gpu_a_lost_anchor_lends_its_slot_to_the_recent_frames() {
5806        let (w, h) = (1280usize, 720usize);
5807        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5808            let mut s = settings(w as i32, h as i32, 60.0);
5809            s.codec = codec;
5810            s.acknowledge_references = true;
5811            s.omit_stripe_headers = true;
5812            let mut enc = match host_session(&s) {
5813                Ok(enc) => enc,
5814                Err(e) => {
5815                    println!("{codec:?}: {e}");
5816                    continue;
5817                }
5818            };
5819            if !enc
5820                .references
5821                .as_ref()
5822                .is_some_and(ReferenceWindow::anchored)
5823            {
5824                println!("{codec:?}: this device or API keeps no long-term reference");
5825                continue;
5826            }
5827            let mut frames = Vec::new();
5828            let mut from = 0;
5829            for i in 0..=22usize {
5830                match i {
5831                    16 => assert!(enc.take_report(ReferenceReport::Lost(12)), "{codec:?}"),
5832                    21 => assert!(enc.take_report(ReferenceReport::Lost(19)), "{codec:?}"),
5833                    _ => {}
5834                }
5835                let out = enc
5836                    .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
5837                    .expect("encode");
5838                if i <= 11 || (16..=17).contains(&i) {
5839                    enc.acknowledge_reference(i as u16, true);
5840                }
5841                frames.push(out);
5842                if i == 21 {
5843                    let Reference::Frame(r) = enc.last_reference() else {
5844                        panic!("{codec:?}: {:?} past the loss", enc.last_reference());
5845                    };
5846                    from = r as usize;
5847                }
5848            }
5849            if codec == Codec::Av1 {
5850                assert_eq!(from, 18, "{codec:?}: past the loss of 19 and 20");
5851            }
5852            let keep: Vec<usize> = (0..=11).chain(16..=from).chain([21, 22]).collect();
5853            let off = apart_keeping(codec, &frames, &keep);
5854            println!("{codec:?}: past the loss from {from}, {off:?} apart");
5855            assert!(
5856                off.is_some_and(|off| off < 0.5),
5857                "{codec:?}: given the frames up to {from}, frame 22 decodes {off:?} apart"
5858            );
5859        }
5860    }
5861
5862    /// A loss deeper than the recent frames is predicted past from the newest anchor before it,
5863    /// the one the device predicts from, and decodes as the complete stream does; one before every
5864    /// anchor costs a key frame. An H.264 session, and an AV1 one on an API with AV1 long-term
5865    /// references, keeps one anchor, a frame every forty-eight, an H.265 one two, a frame every
5866    /// twelve by turns, and on Pascal reaches one recent frame past its buffer. A CBR rate lowered
5867    /// between the anchor and the loss keeps the anchor. Ignored by default.
5868    #[test]
5869    #[ignore]
5870    fn gpu_predicts_past_a_loss_deeper_than_the_recent_frames() {
5871        use crate::encoders::reference::Reference;
5872        // (codec, frames sent after the key frame, first frame lost, the anchor predicted from, an
5873        // older anchor the device must not have taken, CBR lowered from 20 to 8 Mbit/s as the lost
5874        // frame is encoded), at 1080p and, for the frames `split_mode` splits, at 4K
5875        let cases = [
5876            (Codec::H264, 60u16, 50u16, Some(48u16), Some(0u16), false),
5877            (Codec::H264, 60, 57, Some(48), Some(0), false),
5878            (Codec::H264, 80, 64, Some(48), Some(0), false),
5879            (Codec::H264, 80, 51, Some(48), Some(0), false),
5880            (Codec::H264, 80, 64, Some(48), Some(0), true),
5881            (Codec::H264, 60, 40, None, None, false),
5882            (Codec::Av1, 60, 50, Some(48), Some(0), false),
5883            (Codec::Av1, 60, 57, Some(48), Some(0), false),
5884            (Codec::Av1, 80, 64, Some(48), Some(0), false),
5885            (Codec::Av1, 80, 64, Some(48), Some(0), true),
5886            (Codec::Av1, 60, 40, None, None, false),
5887            (Codec::H265, 30, 28, Some(27), Some(26), false),
5888            (Codec::H265, 30, 27, Some(24), Some(12), false),
5889            (Codec::H265, 40, 37, Some(36), Some(24), false),
5890            (Codec::H265, 40, 30, Some(24), Some(12), false),
5891            (Codec::H265, 58, 40, Some(36), Some(24), false),
5892            (Codec::H265, 58, 37, Some(36), Some(24), false),
5893            (Codec::H265, 40, 20, None, None, false),
5894        ];
5895        let split = [
5896            (Codec::Av1, 60u16, 50u16, Some(48u16), Some(0u16), false),
5897            (Codec::H265, 30, 28, Some(27), Some(26), false),
5898            (Codec::H265, 40, 30, Some(24), Some(12), false),
5899        ];
5900        let fmt = |v: Option<f64>| v.map_or("no picture".to_string(), |x| format!("{x:.3}"));
5901        let sized = (cases.into_iter().map(|c| (1920usize, 1080usize, c)))
5902            .chain(split.into_iter().map(|c| (3840, 2160, c)));
5903        for (w, h, (codec, sent, lost, from, older, retarget)) in sized {
5904            let mut s = settings(w as i32, h as i32, 60.0);
5905            s.codec = codec;
5906            s.omit_stripe_headers = true;
5907            if retarget {
5908                s.video_cbr_mode = true;
5909                s.video_bitrate_kbps = 20_000;
5910            }
5911            let mut enc = match host_session(&s) {
5912                Ok(enc) => enc,
5913                Err(e) => {
5914                    println!("{codec:?}: {e}");
5915                    continue;
5916                }
5917            };
5918            if codec == Codec::Av1
5919                && !enc
5920                    .references
5921                    .as_ref()
5922                    .is_some_and(ReferenceWindow::anchored)
5923            {
5924                println!("{codec:?}: this device or API keeps no long-term reference");
5925                continue;
5926            }
5927            let mut frames = Vec::new();
5928            for i in 0..=sent as usize {
5929                if retarget && i == lost as usize {
5930                    s.video_bitrate_kbps = 8000;
5931                    assert!(
5932                        enc.reconfigure_rate(&s),
5933                        "{codec:?}: the rate change was refused"
5934                    );
5935                }
5936                frames.push(
5937                    enc.encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
5938                        .expect("encode"),
5939                );
5940                if i == 0 && enc.last_reference() == Reference::Untracked {
5941                    break;
5942                }
5943            }
5944            if enc.last_reference() == Reference::Untracked {
5945                println!("{codec:?}: this device cannot invalidate a reference");
5946                continue;
5947            }
5948            let normal = frames[sent as usize].len();
5949            assert!(
5950                enc.invalidate_reference(lost),
5951                "{codec:?}: the device refused the invalidation"
5952            );
5953            let next = sent as usize + 1;
5954            frames.push(
5955                enc.encode_cpu_argb(&moving_frame(w, h, next), w * 4, next as u64, 25, false)
5956                    .expect("encode"),
5957            );
5958            let reference = enc.last_reference();
5959            let keeping = |upto: usize| (0..=upto).chain([next]).collect::<Vec<_>>();
5960            let without_lost: Vec<usize> = (0..=next)
5961                .filter(|&i| i < lost as usize || i == next)
5962                .collect();
5963            println!(
5964                "{codec:?} {w}x{h}: frame {lost} of {sent} lost, frame {next} predicts from {reference:?}, {} B against {normal} B",
5965                frames[next].len()
5966            );
5967            // The frame after an anchor is marked predicts from the anchor itself.
5968            if let Some(anchor) = from.filter(|&a| a > 0 && a < sent).map(usize::from) {
5969                let skipped: Vec<usize> = (0..=anchor + 1).filter(|&i| i != anchor).collect();
5970                let off = apart_keeping(codec, &frames[..=anchor + 1], &skipped);
5971                assert!(
5972                    off.is_none_or(|x| x >= 0.5),
5973                    "{codec:?}: frame {} decodes without {anchor} ({})",
5974                    anchor + 1,
5975                    fmt(off)
5976                );
5977            }
5978            match from {
5979                Some(anchor) => {
5980                    assert_eq!(
5981                        reference,
5982                        Reference::Frame(anchor),
5983                        "{codec:?}: lost {lost}"
5984                    );
5985                    let off = apart_keeping(codec, &frames, &without_lost);
5986                    assert!(
5987                        off.is_some_and(|x| x < 0.5),
5988                        "{codec:?}: without {lost}-{sent} the frame is {} off",
5989                        fmt(off)
5990                    );
5991                    let off = apart_keeping(codec, &frames, &keeping(anchor as usize));
5992                    assert!(
5993                        off.is_some_and(|x| x < 0.5),
5994                        "{codec:?}: up to the anchor {anchor} the frame is {} off",
5995                        fmt(off)
5996                    );
5997                    if let Some(older) = older {
5998                        let off = apart_keeping(codec, &frames, &keeping(older as usize));
5999                        let past_buffer = codec == Codec::H265
6000                            && anchor == sent - (enc.dpb - ANCHORS as u32) as u16;
6001                        if past_buffer && off.is_some_and(|x| x < 0.5) {
6002                            println!(
6003                                "{codec:?}: lost {lost}, the device predicts from before {anchor}, the frame past its buffer"
6004                            );
6005                        } else {
6006                            assert!(
6007                                off.is_none_or(|x| x >= 0.5),
6008                                "{codec:?}: the frame decodes from anchor {older} alone ({})",
6009                                fmt(off)
6010                            );
6011                        }
6012                    }
6013                }
6014                None => assert_eq!(
6015                    reference,
6016                    Reference::None,
6017                    "{codec:?}: lost {lost}, before every anchor"
6018                ),
6019            }
6020        }
6021    }
6022
6023    /// A session opened at 1080p and resized in place to 720p keeps the buffer it declared, which
6024    /// the driver never raises, and still predicts past a loss six frames deep from the key frame
6025    /// the resize forced, its first anchor, decoding as the complete stream does. Ignored by
6026    /// default.
6027    #[test]
6028    #[ignore]
6029    fn gpu_a_shrink_predicts_past_a_loss_deeper_than_its_buffer() {
6030        use crate::encoders::reference::Reference;
6031        use crate::encoders::sps::h264_max_num_ref_frames;
6032        for codec in [Codec::H264, Codec::H265] {
6033            let mut s = settings(1920, 1080, 60.0);
6034            s.codec = codec;
6035            s.omit_stripe_headers = true;
6036            let mut enc = match host_session(&s) {
6037                Ok(enc) => enc,
6038                Err(e) if codec == Codec::H265 => {
6039                    println!("{codec:?}: {e}");
6040                    continue;
6041                }
6042                Err(e) => panic!("{codec:?}: {e}"),
6043            };
6044            let encode = |enc: &mut NvencEncoder, i: usize, w: usize, h: usize| {
6045                let out = enc
6046                    .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6047                    .expect("encode");
6048                (out, enc.last_reference())
6049            };
6050            let (first, reference) = encode(&mut enc, 0, 1920, 1080);
6051            if reference == Reference::Untracked {
6052                println!("{codec:?}: this device cannot invalidate a reference");
6053                continue;
6054            }
6055            let declared = enc.dpb;
6056            if codec == Codec::H264 {
6057                assert_eq!(
6058                    h264_max_num_ref_frames(&first),
6059                    Some(4),
6060                    "1080p at level 4.2 admits four"
6061                );
6062            }
6063            (s.width, s.height) = (1280, 720);
6064            assert!(
6065                enc.reconfigure_resolution(&s).expect("in-place shrink"),
6066                "{codec:?}"
6067            );
6068            assert_eq!(
6069                enc.dpb, declared,
6070                "{codec:?}: the driver never raises the buffer"
6071            );
6072            let mut frames = Vec::new();
6073            for i in 1..=8 {
6074                let (out, reference) = encode(&mut enc, i, 1280, 720);
6075                let want = if i == 1 {
6076                    Reference::None
6077                } else {
6078                    Reference::Frame(i as u16 - 1)
6079                };
6080                assert_eq!(reference, want, "{codec:?} frame {i}");
6081                frames.push(out);
6082            }
6083            assert!(
6084                enc.invalidate_reference(2),
6085                "{codec:?}: the device refused the invalidation"
6086            );
6087            let (out, reference) = encode(&mut enc, 9, 1280, 720);
6088            assert_eq!(
6089                reference,
6090                Reference::Frame(1),
6091                "{codec:?}: a loss six deep, from the resize's key frame"
6092            );
6093            frames.push(out);
6094            let off = apart_without(codec, &frames, 1..8);
6095            println!(
6096                "{codec:?}: at 720p on a {declared}-frame buffer, frame 9 without frames 2-8 is {off:.3} off the complete decode"
6097            );
6098            assert!(
6099                off < 0.5,
6100                "{codec:?}: the decoder that lost frames 2-8 shows {off:.2} off the one that saw them"
6101            );
6102        }
6103    }
6104
6105    /// On a real GPU, print what a loss costs the picture at CBR: the frames from one `depth`
6106    /// frames deep on never reach the decoder, as a receiver's frame buffer holds back what
6107    /// predicts from a lost frame, the loss is reported, and the recovery frame's size and the
6108    /// luma PSNR of it and the fourteen frames after it are measured against their sources, over
6109    /// losses at scattered phases; depth 0 is the same frames with nothing lost. Prints
6110    /// measurements to quote rather than asserting. Ignored by default.
6111    #[test]
6112    #[ignore]
6113    fn gpu_bench_loss_recovery() {
6114        use crate::encoders::reference::Reference;
6115        use crate::webcam::decode::{Decoder as _, VideoDecoder};
6116        let (w, h) = (1920usize, 1080usize);
6117        for codec in [Codec::H264, Codec::H265] {
6118            let mut s = settings(w as i32, h as i32, 60.0);
6119            s.codec = codec;
6120            s.video_cbr_mode = true;
6121            s.video_bitrate_kbps = 8000;
6122            for depth in [0usize, 1, 2, 3, 5, 8, 12] {
6123                let mut enc = host_session(&s).expect("init");
6124                let mut dec = VideoDecoder::new(codec).unwrap();
6125                let mut pending: std::collections::VecDeque<Vec<u8>> = Default::default();
6126                let mut rng = 0x2545_f491_4f6c_dd1du64 ^ depth as u64;
6127                let (mut sizes, mut first, mut after, mut keys) =
6128                    (Vec::new(), Vec::new(), Vec::new(), 0);
6129                let mut i = 0usize;
6130                for event in 0..10 {
6131                    rng ^= rng << 13;
6132                    rng ^= rng >> 7;
6133                    rng ^= rng << 17;
6134                    let gap = if event == 0 {
6135                        60
6136                    } else {
6137                        20 + (rng % 37) as usize
6138                    };
6139                    for _ in 0..gap {
6140                        let out = enc
6141                            .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6142                            .expect("encode");
6143                        pending.push_back(out);
6144                        i += 1;
6145                        while pending.len() > depth + 1 {
6146                            let out = pending.pop_front().unwrap();
6147                            dec.decode(&out[VIDEO_HEADER_LEN..]).expect("decode");
6148                        }
6149                    }
6150                    // The newest `depth + 1` frames, the lost one and those after it, never arrive.
6151                    if depth == 0 {
6152                        for out in pending.drain(..) {
6153                            dec.decode(&out[VIDEO_HEADER_LEN..]).expect("decode");
6154                        }
6155                    } else {
6156                        pending.clear();
6157                        enc.invalidate_reference((i - 1 - depth) as u16);
6158                    }
6159                    let mut row = Vec::new();
6160                    for k in 0..15 {
6161                        let src = moving_frame(w, h, i);
6162                        let out = enc
6163                            .encode_cpu_argb(&src, w * 4, i as u64, 25, false)
6164                            .expect("encode");
6165                        if k == 0 {
6166                            sizes.push(out.len() - VIDEO_HEADER_LEN);
6167                            keys += usize::from(enc.last_reference() == Reference::None);
6168                        }
6169                        row.push(luma_psnr(&mut dec, &out, &src, w, h));
6170                        i += 1;
6171                    }
6172                    first.push(row[0]);
6173                    after.push(row.iter().sum::<f64>() / row.len() as f64);
6174                }
6175                sizes.sort();
6176                let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len() as f64;
6177                println!(
6178                    "RECOVERY {codec:?} depth {depth}: {keys}/10 key frames, answer {} B median, \
6179                     answer PSNR {:.2} dB mean ({:.2} worst), with the 14 after {:.2} dB mean",
6180                    sizes[sizes.len() / 2],
6181                    mean(&first),
6182                    first.iter().cloned().fold(f64::INFINITY, f64::min),
6183                    mean(&after),
6184                );
6185            }
6186        }
6187    }
6188
6189    /// NVENC counts H.264's `frame_num` in 256 values and the stream in 65536 (`WideFrameNum`): a
6190    /// loss covering frame 256, where NVENC's own count wraps, is predicted past rather than
6191    /// answered with a key frame, and the decoder that lost it shows the next frame as one that
6192    /// saw it does. Ignored by default.
6193    #[test]
6194    #[ignore]
6195    fn gpu_predicts_past_a_loss_at_the_devices_frame_num_wrap() {
6196        use crate::encoders::reference::Reference;
6197        use crate::encoders::sps::h264_frame_num_range;
6198        let (w, h) = (1280usize, 720usize);
6199        let mut s = settings(w as i32, h as i32, 60.0);
6200        s.omit_stripe_headers = true;
6201        let mut enc = host_session(&s).expect("H.264 session");
6202        let encode = |enc: &mut NvencEncoder, i: usize| {
6203            let out = enc
6204                .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6205                .expect("encode");
6206            (out, enc.last_reference())
6207        };
6208        let (first, reference) = encode(&mut enc, 0);
6209        if reference == Reference::Untracked {
6210            println!("this device cannot invalidate a reference, so nothing is tracked");
6211            return;
6212        }
6213        let range = h264_frame_num_range(&first).expect("the key frame carries the SPS") as usize;
6214        assert_eq!(range, 65536, "NVENC's 256 values, a byte wider");
6215        let mut frames = vec![first];
6216        for i in 1..=258 {
6217            frames.push(encode(&mut enc, i).0);
6218        }
6219        assert!(
6220            enc.invalidate_reference(256),
6221            "the device's wrap frame is reported lost"
6222        );
6223        let (out, reference) = encode(&mut enc, 259);
6224        assert!(
6225            matches!(reference, Reference::Frame(r) if r < 256),
6226            "{reference:?} past the loss"
6227        );
6228        frames.push(out);
6229        let off = apart_without(Codec::H264, &frames, 256..259);
6230        assert!(
6231            off < 0.5,
6232            "past the loss at the wrap the frame decodes {off:.2} off"
6233        );
6234    }
6235
6236    /// On a real GPU, a CBR session resized 720p→1080p folds the new bitrate into the resize
6237    /// reconfigure (asserts `averageBitRate` updated to 8 Mbit/s) and the first post-resize frame is
6238    /// an IDR at the new dimensions. Ignored by default.
6239    #[test]
6240    #[ignore]
6241    fn gpu_resolution_reconfigure_cbr() {
6242        let mut s = settings(1280, 720, 60.0);
6243        s.video_cbr_mode = true;
6244        s.video_bitrate_kbps = 4000;
6245        let mut enc = host_session(&s).expect("NVENC init");
6246        let f720 = frame(1280, 720, 10);
6247        for i in 0..3u64 {
6248            enc.encode_cpu_argb(&f720, 1280 * 4, i, 25, i == 0)
6249                .expect("encode 720p");
6250        }
6251        s.width = 1920;
6252        s.height = 1080;
6253        s.video_bitrate_kbps = 8000;
6254        enc.reconfigure_resolution(&s).expect("cbr resize+rate");
6255        assert_eq!(enc.encode_config.rcParams.averageBitRate, 8_000_000);
6256        let f1080 = frame(1920, 1080, 40);
6257        let pkt = enc
6258            .encode_cpu_argb(&f1080, 1920 * 4, 3, 25, false)
6259            .expect("encode 1080p");
6260        assert_eq!(pkt[1] & 0x0f, FRAME_KEY);
6261        assert_eq!(wire_dims(&pkt), (1920, 1080));
6262    }
6263
6264    /// A session told to keep one reference frame (`video_reference_frames`) declares a decoded
6265    /// picture buffer of one, H.264 and H.265 alike, and keeps no anchor; AV1 keeps its eight.
6266    /// Ignored by default.
6267    #[test]
6268    #[ignore]
6269    fn gpu_sessions_keep_the_reference_frames_they_are_given() {
6270        use crate::encoders::sps::h264_max_num_ref_frames;
6271        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
6272            let mut s = settings(1280, 720, 60.0);
6273            s.codec = codec;
6274            s.omit_stripe_headers = true;
6275            s.acknowledge_references = true;
6276            s.video_reference_frames = 1;
6277            let mut enc = match host_session(&s) {
6278                Ok(enc) => enc,
6279                Err(e) => {
6280                    println!("{codec:?}: {e}");
6281                    continue;
6282                }
6283            };
6284            let key = enc
6285                .encode_cpu_argb(&frame(1280, 720, 10), 1280 * 4, 0, 25, true)
6286                .expect("encode");
6287            let anchors = enc
6288                .references
6289                .as_ref()
6290                .map_or(0, ReferenceWindow::anchor_count);
6291            match codec {
6292                Codec::Av1 => assert_eq!(enc.dpb, AV1_REFERENCES, "AV1"),
6293                _ => {
6294                    assert_eq!(enc.dpb, 1, "{codec:?}");
6295                    assert_eq!(anchors, 0, "{codec:?}");
6296                }
6297            }
6298            if codec == Codec::H264 {
6299                assert_eq!(h264_max_num_ref_frames(&key), Some(1));
6300            }
6301        }
6302    }
6303
6304    /// Every H.264 session bounds reordering at zero, 4:2:0 and 4:4:4, and so does the IDR an
6305    /// in-place resize forces, which re-declares the stream. Ignored by default.
6306    #[test]
6307    #[ignore]
6308    fn gpu_h264_bounds_reordering_at_zero() {
6309        use crate::encoders::sps::fixtures::assert_no_reorder;
6310        for fullcolor in [false, true] {
6311            let mut s = settings(1920, 1080, 60.0);
6312            s.video_fullcolor = fullcolor;
6313            s.omit_stripe_headers = true;
6314            let mut enc = match host_session(&s) {
6315                Ok(enc) => enc,
6316                Err(e) => {
6317                    println!("fullcolor {fullcolor}: {e}");
6318                    continue;
6319                }
6320            };
6321            let key = enc
6322                .encode_cpu_argb(&frame(1920, 1080, 10), 1920 * 4, 0, 25, true)
6323                .expect("encode 1080p");
6324            assert_no_reorder(&key, &format!("NVENC fullcolor {fullcolor} 1080p"));
6325            s.width = 1280;
6326            s.height = 720;
6327            if enc.reconfigure_resolution(&s).expect("in-place resize") {
6328                let key = enc
6329                    .encode_cpu_argb(&frame(1280, 720, 20), 1280 * 4, 1, 25, false)
6330                    .expect("encode 720p");
6331                assert_no_reorder(
6332                    &key,
6333                    &format!("NVENC fullcolor {fullcolor} 720p after the resize"),
6334                );
6335            }
6336        }
6337    }
6338
6339    /// On a real GPU, a resize the driver refuses ends in the rebuild its callers already do:
6340    /// the ladder the session came from opens a new one at the new size, declaring the buffer
6341    /// its own level admits. Forced by asking to raise the buffer a 1080p session declared,
6342    /// which the driver refuses. Ignored by default.
6343    #[test]
6344    #[ignore]
6345    fn gpu_a_refused_resize_is_rebuilt() {
6346        use crate::encoders::sps::h264_max_num_ref_frames;
6347        use crate::encoders::{FrameEncoder, FrameSource, select_frame_encoder};
6348        let mut s = settings(1920, 1080, 60.0);
6349        let mut enc = host_session(&s).expect("NVENC init");
6350        let key = enc
6351            .encode_cpu_argb(&frame(1920, 1080, 10), 1920 * 4, 0, 25, true)
6352            .expect("encode 1080p");
6353        assert_eq!(
6354            h264_max_num_ref_frames(&key[VIDEO_HEADER_LEN..]),
6355            Some(4),
6356            "1080p at level 4.2 admits four"
6357        );
6358        enc.dpb = REFERENCE_FRAMES;
6359        s.width = 1280;
6360        s.height = 720;
6361        let prior = Some(FrameEncoder::Nvenc(enc));
6362        let Some(FrameEncoder::Nvenc(mut enc)) =
6363            select_frame_encoder(&mut s, FrameSource::Host { rgba: false }, prior, "test")
6364        else {
6365            panic!("the ladder rebuilt no NVENC session");
6366        };
6367        assert_eq!(
6368            enc.references.as_ref().map_or(0, ReferenceWindow::next_pts),
6369            0,
6370            "a new session, not the refused one"
6371        );
6372        let key = enc
6373            .encode_cpu_argb(&frame(1280, 720, 20), 1280 * 4, 1, 25, false)
6374            .expect("encode 720p");
6375        assert_eq!(key[1] & 0x0f, FRAME_KEY);
6376        assert_eq!(wire_dims(&key), (1280, 720));
6377        assert_eq!(
6378            h264_max_num_ref_frames(&key[VIDEO_HEADER_LEN..]),
6379            Some(REFERENCE_FRAMES),
6380            "720p declares eight"
6381        );
6382    }
6383
6384    /// Test helper: `value` placed so it ends where a page the process cannot touch begins, so a
6385    /// driver reading or writing past it faults. The mapping lives as long as the process.
6386    fn guarded<T>(value: T) -> &'static mut T {
6387        unsafe {
6388            let page = libc::sysconf(libc::_SC_PAGESIZE) as usize;
6389            let map = libc::mmap(
6390                ptr::null_mut(),
6391                2 * page,
6392                libc::PROT_READ | libc::PROT_WRITE,
6393                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
6394                -1,
6395                0,
6396            );
6397            assert_ne!(map, libc::MAP_FAILED);
6398            assert_eq!(
6399                libc::mprotect(map.cast::<u8>().add(page).cast(), page, libc::PROT_NONE),
6400                0
6401            );
6402            let at = map
6403                .cast::<u8>()
6404                .add(page - std::mem::size_of::<T>())
6405                .cast::<T>();
6406            at.write(value);
6407            &mut *at
6408        }
6409    }
6410
6411    /// On a real GPU, at every API version a driver may be negotiated at, the driver stays inside
6412    /// the structs it is handed: each is placed against a page the process cannot touch, where a
6413    /// read or write past it faults, and every version runs in a process of its own, pinned there
6414    /// with `PIXELFLUX_NVENC_MAX_API`. Ignored by default.
6415    #[test]
6416    #[ignore]
6417    fn gpu_the_driver_stays_inside_the_structs_it_is_handed() {
6418        const PINNED: &str = "PIXELFLUX_TEST_NVENC_PINNED";
6419        if std::env::var_os(PINNED).is_none() {
6420            let name = format!(
6421                "{}::gpu_the_driver_stays_inside_the_structs_it_is_handed",
6422                module_path!().split_once("::").unwrap().1
6423            );
6424            for api in ["10.0", "11.0", "11.1", "12.0", "12.1", "13.0"] {
6425                let out = std::process::Command::new(std::env::current_exe().unwrap())
6426                    .args([
6427                        name.as_str(),
6428                        "--exact",
6429                        "--ignored",
6430                        "--nocapture",
6431                        "--test-threads=1",
6432                    ])
6433                    .env("PIXELFLUX_NVENC_MAX_API", api)
6434                    .env(PINNED, "1")
6435                    .output()
6436                    .expect("the test binary runs");
6437                let stdout = String::from_utf8_lossy(&out.stdout);
6438                let report = stdout
6439                    .lines()
6440                    .find(|l| l.contains("stayed inside"))
6441                    .unwrap_or_default();
6442                println!("{report}");
6443                assert!(
6444                    out.status.success() && !report.is_empty(),
6445                    "API {api}: {}\n{stdout}",
6446                    out.status
6447                );
6448            }
6449            return;
6450        }
6451        let mut enc = host_session(&settings(1280, 720, 60.0)).expect("NVENC init");
6452        unsafe {
6453            let funcs = enc.nvenc_funcs;
6454            let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
6455            let mut open = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
6456                version: sv(NvStruct::OpenSessionExParams),
6457                deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
6458                device: enc.cuda_context as *mut c_void,
6459                apiVersion: neg_api(),
6460                ..Default::default()
6461            };
6462            let mut session = ptr::null_mut();
6463            assert_eq!(
6464                (funcs.nvEncOpenEncodeSessionEx.unwrap())(&mut open, &mut session),
6465                NVENCSTATUS::NV_ENC_SUCCESS
6466            );
6467            enc.init_params.encodeConfig = &mut enc.encode_config;
6468            let init = guarded(Negotiated::new(enc.init_params));
6469            let status = (funcs.nvEncInitializeEncoder.unwrap())(session, &mut init.value);
6470            (funcs.nvEncDestroyEncoder.unwrap())(session);
6471            assert_eq!(status, NVENCSTATUS::NV_ENC_SUCCESS, "initialize");
6472            let reconfigure = guarded(reconfigure_params(
6473                enc.init_params,
6474                nvenc_cur_ver(),
6475                true,
6476                true,
6477            ));
6478            let status = (funcs.nvEncReconfigureEncoder.unwrap())(
6479                enc.encoder_session,
6480                &mut reconfigure.value,
6481            );
6482            assert_eq!(status, NVENCSTATUS::NV_ENC_SUCCESS, "reconfigure");
6483            let output = enc.bitstream_buffers[0];
6484            let mut pic = NV_ENC_PIC_PARAMS {
6485                version: sv(NvStruct::PicParams),
6486                inputWidth: enc.width,
6487                inputHeight: enc.height,
6488                inputBuffer: enc.mapped_input_buffer,
6489                outputBitstream: output,
6490                bufferFmt: enc.input_format,
6491                pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
6492                ..Default::default()
6493            };
6494            assert_eq!(
6495                (funcs.nvEncEncodePicture.unwrap())(enc.encoder_session, &mut pic),
6496                NVENCSTATUS::NV_ENC_SUCCESS
6497            );
6498            let lock = guarded(Negotiated::new(NV_ENC_LOCK_BITSTREAM {
6499                version: sv(NvStruct::LockBitstream),
6500                outputBitstream: output,
6501                ..Default::default()
6502            }));
6503            let status = (funcs.nvEncLockBitstream.unwrap())(enc.encoder_session, &mut lock.value);
6504            assert_eq!(status, NVENCSTATUS::NV_ENC_SUCCESS, "lock");
6505            assert!(
6506                lock.value.bitstreamSizeInBytes > 0,
6507                "the locked bitstream carries the picture"
6508            );
6509            (funcs.nvEncUnlockBitstream.unwrap())(enc.encoder_session, output);
6510            (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
6511        }
6512        let (maj, min) = nvenc_cur_ver();
6513        println!("API {maj}.{min}: the driver stayed inside every struct");
6514    }
6515
6516    /// A source a capture keeps and repaints, as its shm segment is, is page-locked once and
6517    /// read as it is at each upload, and after `release_pinned_hosts`, as a reshape calls it, a
6518    /// new one is: each decoded frame shows the picture just painted, not an earlier one.
6519    /// Ignored by default.
6520    #[test]
6521    #[ignore]
6522    fn gpu_pinned_uploads_read_the_source_as_it_is_now() {
6523        use crate::webcam::decode::VideoDecoder;
6524        let (w, h) = (1280usize, 720usize);
6525        let mut enc = NvencEncoder::new(&settings(w as i32, h as i32, 60.0), ptr::null())
6526            .expect("NVENC init");
6527        assert!(enc.pin_uploads, "the production default pins");
6528        let mut dec = VideoDecoder::new(Codec::H264).expect("decoder");
6529        for (source, frames) in [(0, 0..4usize), (1, 4..8usize)] {
6530            if source == 1 {
6531                enc.release_pinned_hosts();
6532            }
6533            let mut shm = vec![0u8; w * h * 4];
6534            for i in frames {
6535                shm.copy_from_slice(&moving_frame(w, h, i));
6536                let pkt = enc
6537                    .encode_cpu_packed(&shm, w * 4, false, i as u64, 25, i == 0)
6538                    .expect("encode");
6539                let psnr = luma_psnr(&mut dec, &pkt, &moving_frame(w, h, i), w, h);
6540                assert!(
6541                    psnr > 30.0,
6542                    "source {source}, frame {i}: {psnr:.1} dB against the picture just painted"
6543                );
6544            }
6545        }
6546    }
6547
6548    /// On a real GPU whose driver caps the NVENC sessions a device runs at once, as it does on
6549    /// consumer boards, the probe of a device with every session taken answers `SESSIONS_TAKEN`,
6550    /// and once one frees it lists the codecs again. A device that takes 65 sessions at once has
6551    /// no cap to reach and says so. The sessions are closed before anything is asserted, so a
6552    /// failure leaves none held against the cap for the tests after it. Ignored by default.
6553    /// A capture restarted while a game holds the GPU's memory keeps its place: once a session
6554    /// has opened, the device's primary CUDA context outlives it (`keep_primary_context`), so a
6555    /// session reopened after another context has taken all but 48 MiB still opens and encodes,
6556    /// where a context created afresh needs about 106 MiB.
6557    #[test]
6558    #[ignore]
6559    fn gpu_a_session_reopened_under_a_full_gpu_keeps_its_context() {
6560        type Ctx = *mut c_void;
6561        let first = host_session(&settings(256, 128, 60.0)).expect("NVENC init");
6562        let device = first.cuda_device;
6563        drop(first);
6564        unsafe {
6565            let lib = Library::new("libcuda.so.1").expect("libcuda");
6566            let state: Symbol<unsafe extern "C" fn(CUdevice, *mut u32, *mut i32) -> CUresult> =
6567                lib.get(b"cuDevicePrimaryCtxGetState\0").unwrap();
6568            let create: Symbol<unsafe extern "C" fn(*mut Ctx, u32, CUdevice) -> CUresult> =
6569                lib.get(b"cuCtxCreate_v2\0").unwrap();
6570            let destroy: Symbol<unsafe extern "C" fn(Ctx) -> CUresult> =
6571                lib.get(b"cuCtxDestroy_v2\0").unwrap();
6572            let info: Symbol<unsafe extern "C" fn(*mut usize, *mut usize) -> CUresult> =
6573                lib.get(b"cuMemGetInfo_v2\0").unwrap();
6574            let alloc: Symbol<unsafe extern "C" fn(*mut u64, usize) -> CUresult> =
6575                lib.get(b"cuMemAlloc_v2\0").unwrap();
6576            let (mut flags, mut active) = (0u32, 0i32);
6577            assert_eq!(
6578                state(device, &mut flags, &mut active),
6579                CUresult::CUDA_SUCCESS
6580            );
6581
6582            let mut game: Ctx = ptr::null_mut();
6583            assert_eq!(create(&mut game, 0, device), CUresult::CUDA_SUCCESS);
6584            let (mut free, mut total) = (0usize, 0usize);
6585            let mut chunk = 256usize << 20;
6586            let mut taken = 0usize;
6587            while chunk >= 1 << 20 {
6588                let mut ptr = 0u64;
6589                info(&mut free, &mut total);
6590                if free < (48 << 20) + chunk || alloc(&mut ptr, chunk) != CUresult::CUDA_SUCCESS {
6591                    chunk /= 2;
6592                } else {
6593                    taken += chunk;
6594                }
6595            }
6596            info(&mut free, &mut total);
6597            println!(
6598                "took {} MiB; {} MiB of {} left",
6599                taken >> 20,
6600                free >> 20,
6601                total >> 20
6602            );
6603            let reopened = host_session(&settings(256, 128, 60.0)).map(|mut enc| {
6604                enc.encode_cpu_argb(&vec![0x40u8; 256 * 128 * 4], 256 * 4, 0, 25, true)
6605                    .map(|out| out.len())
6606            });
6607            destroy(game);
6608            println!(
6609                "primary context active after the first session: {active}; reopened: {reopened:?}"
6610            );
6611            assert_eq!(
6612                active, 1,
6613                "the primary context outlived the session that opened it"
6614            );
6615            let bytes = reopened.expect("the session reopens").expect("and encodes");
6616            assert!(bytes > 0);
6617        }
6618    }
6619
6620    #[test]
6621    #[ignore]
6622    fn gpu_a_device_out_of_sessions_says_so() {
6623        let enc = host_session(&settings(256, 128, 60.0)).expect("NVENC init");
6624        let mut sessions = Vec::new();
6625        let refused = unsafe {
6626            loop {
6627                let mut open = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
6628                    version: sv(NvStruct::OpenSessionExParams),
6629                    deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
6630                    device: enc.cuda_context as *mut c_void,
6631                    apiVersion: neg_api(),
6632                    ..Default::default()
6633                };
6634                let mut session = ptr::null_mut();
6635                match (enc.nvenc_funcs.nvEncOpenEncodeSessionEx.unwrap())(&mut open, &mut session) {
6636                    NVENCSTATUS::NV_ENC_SUCCESS => sessions.push(session),
6637                    status => break Some(status),
6638                }
6639                if sessions.len() == 64 {
6640                    break None;
6641                }
6642            }
6643        };
6644        let taken = probe_codecs(0);
6645        let opened = sessions.len();
6646        for session in sessions.drain(..) {
6647            unsafe { (enc.nvenc_funcs.nvEncDestroyEncoder.unwrap())(session) };
6648        }
6649        let Some(status) = refused else {
6650            println!("this device took 65 sessions at once: no cap to reach");
6651            return;
6652        };
6653        assert_eq!(
6654            session_refusal(status),
6655            SESSIONS_TAKEN,
6656            "session {} refused: {status:?}",
6657            opened + 2
6658        );
6659        assert_eq!(
6660            taken,
6661            Err(SESSIONS_TAKEN.to_string()),
6662            "every session taken"
6663        );
6664        assert!(
6665            probe_codecs(0).is_ok_and(|codecs| !codecs.is_empty()),
6666            "a freed session is listed again"
6667        );
6668    }
6669
6670    /// On a real GPU, a live frame-rate drop keeps the level the decoded picture buffer needs: a
6671    /// 1080p session opened at 120 fps declares the eight frames its level admits there, and the
6672    /// drop to 60 fps, whose own level admits fewer, is taken at that level, so a CBR session
6673    /// spends about twice the bits on each frame. Ignored by default.
6674    #[test]
6675    #[ignore]
6676    fn gpu_frame_rate_drop_keeps_the_level_the_buffer_needs() {
6677        for codec in [Codec::H264, Codec::H265] {
6678            let mut s = settings(1920, 1080, 120.0);
6679            s.codec = codec;
6680            s.video_cbr_mode = true;
6681            s.video_bitrate_kbps = 8000;
6682            let mut enc = host_session(&s).expect("NVENC init");
6683            let level = enc.declared_level();
6684            let frames: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(1920, 1080, i)).collect();
6685            let kbit = |enc: &mut NvencEncoder, range: std::ops::Range<u64>| {
6686                let n = (range.end - range.start) as f64;
6687                let bytes: usize = range
6688                    .map(|i| {
6689                        enc.encode_cpu_argb(&frames[i as usize % 16], 1920 * 4, i, 25, i == 0)
6690                            .expect("encode")
6691                            .len()
6692                            - VIDEO_HEADER_LEN
6693                    })
6694                    .sum();
6695                bytes as f64 * 8.0 / n / 1000.0
6696            };
6697            kbit(&mut enc, 0..20);
6698            let at120 = kbit(&mut enc, 20..80);
6699            s.target_fps = 60.0;
6700            assert!(
6701                enc.reconfigure_rate(&s),
6702                "{codec:?}: the driver refused 60 fps"
6703            );
6704            assert_eq!(
6705                enc.declared_level(),
6706                level,
6707                "{codec:?}: the level the buffer needs stays"
6708            );
6709            let at60 = kbit(&mut enc, 80..200);
6710            println!("{codec:?}: {at120:.1} kbit a frame at 120 fps, {at60:.1} at 60");
6711            assert!(
6712                at60 > 1.4 * at120,
6713                "{codec:?}: halving the frame rate left {at60:.1} kbit a frame against {at120:.1}"
6714            );
6715        }
6716    }
6717
6718    /// On a real GPU, a session of every codec the device carries is opened at the capture's
6719    /// rate as the fraction it names, a live change of rate reaches the driver the same way, and
6720    /// an H.264 stream declares the rate in its timing. Ignored by default.
6721    #[test]
6722    #[ignore]
6723    fn gpu_frame_rate_reaches_the_driver_as_its_fraction() {
6724        let f = frame(640, 360, 30);
6725        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
6726            for (num, den) in [(60000u32, 1001u32), (120000, 1001), (144000, 1001), (60, 1)] {
6727                let fps = num as f64 / den as f64;
6728                let mut s = settings(640, 360, fps);
6729                s.codec = codec;
6730                s.video_cbr_mode = true;
6731                s.video_bitrate_kbps = 8000;
6732                let mut enc = match host_session(&s) {
6733                    Ok(enc) => enc,
6734                    Err(e) if codec == Codec::Av1 && e.contains("engine") => {
6735                        println!("{codec:?}: {e}");
6736                        break;
6737                    }
6738                    Err(e) => panic!("{codec:?} at {num}/{den}: {e}"),
6739                };
6740                assert_eq!(
6741                    (enc.init_params.frameRateNum, enc.init_params.frameRateDen),
6742                    (num, den),
6743                    "{codec:?}"
6744                );
6745                let pkt = enc
6746                    .encode_cpu_argb(&f, 640 * 4, 0, 25, true)
6747                    .expect("encode");
6748                if codec == Codec::H264 {
6749                    let (tick, scale) = crate::encoders::sps::h264_timing(&pkt[VIDEO_HEADER_LEN..])
6750                        .expect("SPS timing");
6751                    assert_eq!(
6752                        scale as u64 * den as u64,
6753                        2 * num as u64 * tick as u64,
6754                        "the SPS declares {scale}/2x{tick}, not {num}/{den}"
6755                    );
6756                }
6757                s.target_fps = 30.0;
6758                assert!(
6759                    enc.reconfigure_rate(&s),
6760                    "{codec:?}: the driver refused 30 fps"
6761                );
6762                assert_eq!(
6763                    (enc.init_params.frameRateNum, enc.init_params.frameRateDen),
6764                    (30, 1)
6765                );
6766                s.target_fps = fps;
6767                assert!(
6768                    enc.reconfigure_rate(&s),
6769                    "{codec:?}: the driver refused {num}/{den}"
6770                );
6771                assert_eq!(
6772                    (enc.init_params.frameRateNum, enc.init_params.frameRateDen),
6773                    (num, den),
6774                    "{codec:?}"
6775                );
6776                enc.encode_cpu_argb(&f, 640 * 4, 1, 25, false)
6777                    .expect("encode after the change");
6778            }
6779        }
6780    }
6781
6782    /// On a real GPU, a live frame-rate rise past the declared level's macroblock rate raises the
6783    /// level at a key frame, whose sequence header is where a decoder learns it: a 1080p session
6784    /// opened at 60 fps declares level 4.2, and 120 fps takes 5.1. Ignored by default.
6785    #[test]
6786    #[ignore]
6787    fn gpu_frame_rate_rise_raises_the_level_at_a_key_frame() {
6788        let mut s = settings(1920, 1080, 60.0);
6789        let mut enc = host_session(&s).expect("NVENC init");
6790        let f = frame(1920, 1080, 30);
6791        for i in 0..3u64 {
6792            enc.encode_cpu_argb(&f, 1920 * 4, i, 25, i == 0)
6793                .expect("encode");
6794        }
6795        assert_eq!(enc.declared_level(), 42);
6796        s.target_fps = 120.0;
6797        assert!(enc.reconfigure_rate(&s), "the driver refused 120 fps");
6798        let pkt = enc
6799            .encode_cpu_argb(&f, 1920 * 4, 3, 25, false)
6800            .expect("encode");
6801        assert_eq!(
6802            pkt[1] & 0x0f,
6803            FRAME_KEY,
6804            "the raised level reaches the stream at a key frame"
6805        );
6806        let sps = crate::encoders::codec::annexb_nals(&pkt[VIDEO_HEADER_LEN..])
6807            .find(|n| n[0] & 0x1f == 7)
6808            .expect("an SPS");
6809        assert_eq!(sps[3], 51, "level_idc");
6810    }
6811
6812    /// On a real GPU, a live CBR rate change moves the VBV initial delay with the buffer: after
6813    /// the bitrate is lowered the delay equals the new, smaller buffer, the driver takes the
6814    /// reconfigure, and a steady desktop-like sequence lands at the new rate. What the driver
6815    /// makes of a delay left above the buffer is printed rather than asserted. Ignored by default.
6816    #[test]
6817    #[ignore]
6818    fn gpu_rate_reconfigure_moves_the_vbv_initial_delay_with_the_buffer() {
6819        let (w, h) = (1920usize, 1080usize);
6820        let mut s = settings(w as i32, h as i32, 60.0);
6821        s.video_cbr_mode = true;
6822        s.video_bitrate_kbps = 8000;
6823        let mut enc = host_session(&s).expect("NVENC init");
6824        let rate = |enc: &NvencEncoder| {
6825            let rc = enc.encode_config.rcParams;
6826            (rc.averageBitRate, rc.vbvBufferSize, rc.vbvInitialDelay)
6827        };
6828        let vbv8 = cbr_vbv(&s, 8_000_000);
6829        assert_eq!(rate(&enc), (8_000_000, vbv8, vbv8));
6830        let frames: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(w, h, i)).collect();
6831        enc.encode_cpu_argb(&frames[0], w * 4, 0, 25, true)
6832            .expect("encode");
6833
6834        s.video_bitrate_kbps = 2000;
6835        assert!(enc.reconfigure_rate(&s), "the driver takes the lower rate");
6836        let vbv2 = cbr_vbv(&s, 2_000_000);
6837        assert_eq!(rate(&enc), (2_000_000, vbv2, vbv2));
6838        let n = 120u64;
6839        let mut bytes = 0usize;
6840        for i in 1..=n {
6841            let pkt = enc
6842                .encode_cpu_argb(&frames[i as usize % 16], w * 4, i, 25, false)
6843                .expect("encode");
6844            bytes += pkt.len().saturating_sub(VIDEO_HEADER_LEN);
6845        }
6846        let kbps = bytes as f64 * 8.0 * 60.0 / n as f64 / 1000.0;
6847        println!("CBR 2000 kbps after the rate change: {kbps:.0} kbps over {n} steady frames");
6848        assert!(
6849            (1500.0..=2500.0).contains(&kbps),
6850            "the session encodes at the new rate: {kbps:.0} kbps"
6851        );
6852
6853        enc.encode_config.rcParams.vbvInitialDelay = vbv2 * 4;
6854        let (status, detail) = reconfigure_raw(&mut enc);
6855        println!("a VBV initial delay of four buffers: {status:?} ({detail})");
6856        enc.encode_config.rcParams.vbvInitialDelay = vbv2;
6857        assert_eq!(reconfigure_raw(&mut enc).0, NVENCSTATUS::NV_ENC_SUCCESS);
6858        enc.encode_cpu_argb(&frames[7], w * 4, n + 1, 25, false)
6859            .expect("the session still encodes");
6860    }
6861
6862    /// The slice count and the HEVC tier are properties of the bitstream a client decodes, so
6863    /// they are read back out of it on a real GPU: four VCL NAL units per H.264 and HEVC frame
6864    /// at two geometries, and an HEVC SPS carrying the High tier flag beside the pinned level.
6865    /// Ignored by default.
6866    #[test]
6867    #[ignore]
6868    fn gpu_frames_carry_four_slices_and_hevc_declares_high_tier() {
6869        use crate::encoders::codec::annexb_nals;
6870        for (w, h) in [(1280usize, 720usize), (640, 480)] {
6871            for codec in [Codec::H264, Codec::H265] {
6872                let mut s = settings(w as i32, h as i32, 60.0);
6873                s.codec = codec;
6874                let mut enc = host_session(&s).expect("NVENC init");
6875                for i in 0..4u64 {
6876                    let pkt = enc
6877                        .encode_cpu_argb(&frame(w, h, 20 + i as u8), w * 4, i, 25, i == 0)
6878                        .expect("encode");
6879                    let au = &pkt[VIDEO_HEADER_LEN..];
6880                    let vcl = annexb_nals(au)
6881                        .filter(|nal| match codec {
6882                            Codec::H265 => (nal[0] >> 1) & 0x3f < 32,
6883                            _ => matches!(nal[0] & 0x1f, 1 | 5),
6884                        })
6885                        .count();
6886                    assert_eq!(
6887                        vcl, SLICES_PER_FRAME as usize,
6888                        "{codec:?} {w}x{h} frame {i} slices"
6889                    );
6890                    if codec == Codec::H264 && i == 0 {
6891                        // After the one-byte NAL header the SPS RBSP carries profile_idc, the
6892                        // constraint-flag byte, then level_idc, which must be the current
6893                        // geometry's level rather than the 4K-headroom one so a decoder gating
6894                        // on it accepts the picture.
6895                        let sps = annexb_nals(au)
6896                            .find(|nal| nal[0] & 0x1f == 7)
6897                            .map(rbsp)
6898                            .expect("an SPS on the key frame");
6899                        assert_eq!(
6900                            sps[3] as u32,
6901                            nvenc_level(Codec::H264, w as u32, h as u32, 60, 0, true),
6902                            "level_idc at {w}x{h}"
6903                        );
6904                    }
6905                    if codec == Codec::H265 && i == 0 {
6906                        let sps = annexb_nals(au)
6907                            .find(|nal| (nal[0] >> 1) & 0x3f == 33)
6908                            .map(rbsp)
6909                            .expect("an SPS on the key frame");
6910                        // After the two-byte NAL header and the byte holding the VPS id, the
6911                        // sub-layer count, and the nesting flag, profile_tier_level opens with
6912                        // general_profile_space (2), general_tier_flag (1), and
6913                        // general_profile_idc (5); general_level_idc follows the 32
6914                        // compatibility flags and the 48 constraint bits.
6915                        assert_eq!((sps[3] >> 5) & 1, 1, "general_tier_flag at {w}x{h}");
6916                        assert_eq!(
6917                            sps[14] as u32,
6918                            nvenc_level(Codec::H265, w as u32, h as u32, 60, 0, true),
6919                            "general_level_idc at {w}x{h}"
6920                        );
6921                    }
6922                }
6923            }
6924        }
6925    }
6926
6927    /// On a real GPU: what `SLICES_PER_FRAME` slices cost against one at a fixed quantizer,
6928    /// H.264 and HEVC at 1080p on the gradient frames the other benches use. Prints the rates and
6929    /// the difference; ignored by default.
6930    #[test]
6931    #[ignore]
6932    fn gpu_bench_slices() {
6933        let (w, h) = (1920usize, 1080usize);
6934        let n: usize = std::env::var("NVENC_BENCH_FRAMES")
6935            .ok()
6936            .and_then(|v| v.parse().ok())
6937            .unwrap_or(240);
6938        let frames: Vec<Vec<u8>> = (0..8u8).map(|k| frame(w, h, 10 + 30 * k)).collect();
6939        for codec in [Codec::H264, Codec::H265] {
6940            let mut s = settings(w as i32, h as i32, 60.0);
6941            s.codec = codec;
6942            let mut sizes = Vec::new();
6943            for slices in [1u32, SLICES_PER_FRAME] {
6944                let tuning = NvencTuning {
6945                    slices,
6946                    ..NvencTuning::default()
6947                };
6948                let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), tuning) {
6949                    Ok(enc) => enc,
6950                    Err(e) => {
6951                        println!("{codec:?}: {e}");
6952                        continue;
6953                    }
6954                };
6955                enc.encode_cpu_packed(&frames[0], w * 4, false, 0, 25, true)
6956                    .expect("warm-up");
6957                let mut bytes = 0usize;
6958                per_frame(&format!("{codec:?} {slices} slice(s)"), n, |i| {
6959                    bytes += enc
6960                        .encode_cpu_packed(&frames[i % 8], w * 4, false, 1 + i as u64, 25, false)
6961                        .expect("encode")
6962                        .len();
6963                });
6964                println!("    {} kbps", bytes * 8 * 60 / n / 1000);
6965                sizes.push(bytes);
6966            }
6967            if let [one, many] = sizes[..] {
6968                println!(
6969                    "{codec:?}: {SLICES_PER_FRAME} slices cost {:+.2}% bitrate at a fixed quantizer",
6970                    (many as f64 / one as f64 - 1.0) * 100.0
6971                );
6972            }
6973        }
6974    }
6975
6976    /// On a real GPU: how closely a CBR session holds its target under each VBV policy choice,
6977    /// for H.264 and HEVC at 1080p, on three kinds of content: the alternating gradient frames
6978    /// the other benches use, where every frame is a scene cut; a steady desktop-like sequence
6979    /// with one moving block; and that sequence entered through one cut. Each row varies the VBV
6980    /// (one frame or one and a half), the initial delay (a full buffer or the driver's default),
6981    /// and the slice count (`SLICES_PER_FRAME` or one), and prints the achieved rate, the
6982    /// largest and smallest frame, the luma PSNR of the decoded picture against the source (mean
6983    /// and worst frame), and the wall time per frame. `NVENC_BENCH_KBPS` and `NVENC_BENCH_FPS`
6984    /// move the target from 8000 kbit/s at 60 fps. Ignored by default.
6985    #[test]
6986    #[ignore]
6987    fn gpu_bench_cbr_policy() {
6988        let (w, h) = (1920usize, 1080usize);
6989        let env = |name: &str, default: usize| {
6990            std::env::var(name)
6991                .ok()
6992                .and_then(|v| v.parse().ok())
6993                .unwrap_or(default)
6994        };
6995        let n = env("NVENC_BENCH_FRAMES", 120);
6996        let kbps = env("NVENC_BENCH_KBPS", 8000);
6997        let fps = env("NVENC_BENCH_FPS", 60);
6998        let cuts: Vec<Vec<u8>> = (0..8u8).map(|k| frame(w, h, 10 + 30 * k)).collect();
6999        let steady: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(w, h, i)).collect();
7000        let other = frame(w, h, 200);
7001        let contents: Vec<(&str, Vec<&Vec<u8>>)> = vec![
7002            (
7003                "scene cuts",
7004                std::iter::once(&cuts[0])
7005                    .chain((0..n).map(|i| &cuts[1 + i % 7]))
7006                    .collect(),
7007            ),
7008            (
7009                "steady",
7010                std::iter::once(&steady[0])
7011                    .chain((0..n).map(|i| &steady[1 + i % 15]))
7012                    .collect(),
7013            ),
7014            (
7015                "one cut",
7016                std::iter::once(&other)
7017                    .chain((0..n).map(|i| &steady[i % 16]))
7018                    .collect(),
7019            ),
7020        ];
7021        println!("CBR {kbps} kbit/s at {fps} fps, {n} frames per row");
7022        for codec in [Codec::H264, Codec::H265] {
7023            for (content, seq) in &contents {
7024                for vbv_frames in [1.0f64, 1.5] {
7025                    for full_delay in [true, false] {
7026                        for slices in [SLICES_PER_FRAME, 1] {
7027                            let mut s = settings(w as i32, h as i32, fps as f64);
7028                            s.codec = codec;
7029                            s.video_cbr_mode = true;
7030                            s.video_bitrate_kbps = kbps as i32;
7031                            s.video_vbv_multiplier = vbv_frames;
7032                            let tuning = NvencTuning {
7033                                slices,
7034                                ..NvencTuning::default()
7035                            };
7036                            let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), tuning) {
7037                                Ok(enc) => enc,
7038                                Err(e) => {
7039                                    println!("{codec:?}: {e}");
7040                                    continue;
7041                                }
7042                            };
7043                            if !full_delay {
7044                                enc.encode_config.rcParams.vbvInitialDelay = 0;
7045                                assert_eq!(
7046                                    reconfigure_raw(&mut enc).0,
7047                                    NVENCSTATUS::NV_ENC_SUCCESS
7048                                );
7049                            }
7050                            let label = format!(
7051                                "{codec:?} {content}: VBV {vbv_frames} frame(s), initial delay {}, {slices} slice(s)",
7052                                if full_delay { "full" } else { "default" }
7053                            );
7054                            cbr_row(&label, &mut enc, codec, seq, w, h, fps);
7055                        }
7056                    }
7057                }
7058            }
7059        }
7060    }
7061
7062    /// On a real GPU: which H.264 rate-control setting lets a CBR session run past a small VBV.
7063    /// Holds a 1080p H.264 session at the target `NVENC_BENCH_KBPS` and `NVENC_BENCH_FPS` name
7064    /// on the scene-cut and one-cut sequences at one and one and a half frames of buffer, and
7065    /// changes one setting per row against the production session: single-pass and
7066    /// full-resolution two-pass rate control in place of the quarter-resolution first pass,
7067    /// `strictGOPTarget` off, and a quantizer ceiling of 51. Ignored by default.
7068    #[test]
7069    #[ignore]
7070    fn gpu_bench_cbr_rate_control() {
7071        let (w, h) = (1920usize, 1080usize);
7072        let env = |name: &str, default: usize| {
7073            std::env::var(name)
7074                .ok()
7075                .and_then(|v| v.parse().ok())
7076                .unwrap_or(default)
7077        };
7078        let n = env("NVENC_BENCH_FRAMES", 120);
7079        let kbps = env("NVENC_BENCH_KBPS", 8000);
7080        let fps = env("NVENC_BENCH_FPS", 60);
7081        let cuts: Vec<Vec<u8>> = (0..8u8).map(|k| frame(w, h, 10 + 30 * k)).collect();
7082        let steady: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(w, h, i)).collect();
7083        let other = frame(w, h, 200);
7084        let contents: Vec<(&str, Vec<&Vec<u8>>)> = vec![
7085            (
7086                "scene cuts",
7087                std::iter::once(&cuts[0])
7088                    .chain((0..n).map(|i| &cuts[1 + i % 7]))
7089                    .collect(),
7090            ),
7091            (
7092                "one cut",
7093                std::iter::once(&other)
7094                    .chain((0..n).map(|i| &steady[i % 16]))
7095                    .collect(),
7096            ),
7097        ];
7098        let variants = [
7099            (
7100                "production",
7101                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
7102                true,
7103                0,
7104            ),
7105            (
7106                "single pass",
7107                NV_ENC_MULTI_PASS::NV_ENC_MULTI_PASS_DISABLED,
7108                true,
7109                0,
7110            ),
7111            (
7112                "two-pass full",
7113                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_FULL_RESOLUTION,
7114                true,
7115                0,
7116            ),
7117            (
7118                "strictGOPTarget off",
7119                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
7120                false,
7121                0,
7122            ),
7123            (
7124                "max QP 51",
7125                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
7126                true,
7127                51,
7128            ),
7129        ];
7130        println!("H264 CBR {kbps} kbit/s at {fps} fps, {n} frames per row");
7131        for (content, seq) in &contents {
7132            for vbv_frames in [1.0f64, 1.5] {
7133                for (name, multipass, strict, max_qp) in variants {
7134                    let mut s = settings(w as i32, h as i32, fps as f64);
7135                    s.video_cbr_mode = true;
7136                    s.video_bitrate_kbps = kbps as i32;
7137                    s.video_vbv_multiplier = vbv_frames;
7138                    s.video_max_qp = max_qp;
7139                    let tuning = NvencTuning {
7140                        multipass,
7141                        ..NvencTuning::default()
7142                    };
7143                    let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), tuning) {
7144                        Ok(enc) => enc,
7145                        Err(e) => {
7146                            println!("{name}: {e}");
7147                            continue;
7148                        }
7149                    };
7150                    if !strict {
7151                        enc.encode_config.rcParams.set_strictGOPTarget(0);
7152                        assert_eq!(reconfigure_raw(&mut enc).0, NVENCSTATUS::NV_ENC_SUCCESS);
7153                    }
7154                    cbr_row(
7155                        &format!("{content}: VBV {vbv_frames} frame(s), {name}"),
7156                        &mut enc,
7157                        Codec::H264,
7158                        seq,
7159                        w,
7160                        h,
7161                        fps,
7162                    );
7163                }
7164            }
7165        }
7166    }
7167
7168    /// The reason HEVC pins High tier: with the level pinned rather than autoselected,
7169    /// NVENC validates the requested bitrate against that level's MaxBR, and Main tier's
7170    /// ceiling is low enough that an ordinary 4K desktop session runs into it.
7171    ///
7172    /// The test opens the same CBR HEVC session twice, once at each tier, at a bitrate
7173    /// above the Main-tier ceiling for the pinned level. Tier 1 must open. Tier 0 is
7174    /// reported rather than asserted: what the driver does above the ceiling is the thing
7175    /// being measured, and a future driver that stops rejecting it should not turn this
7176    /// into a red test, it should just make the printed line say so.
7177    #[test]
7178    #[ignore]
7179    fn gpu_hevc_high_tier_opens_above_the_main_tier_ceiling() {
7180        let mut s = settings(3840, 2160, 60.0);
7181        s.codec = Codec::H265;
7182        s.video_cbr_mode = true;
7183        // Above the Main-tier MaxBR of every 5.x level (40 Mbit/s at 5.1, 60 at 5.2).
7184        s.video_bitrate_kbps = 100_000;
7185
7186        let high = host_session(&s);
7187        match &high {
7188            Ok(enc) => unsafe {
7189                let c = enc.encode_config.encodeCodecConfig.hevcConfig;
7190                assert_eq!(c.tier, 1);
7191                println!(
7192                    "HEVC High tier at {} kbps: opened, level {}",
7193                    s.video_bitrate_kbps, c.level,
7194                );
7195            },
7196            Err(e) => panic!("High tier must open above the Main-tier ceiling: {e}"),
7197        }
7198        drop(high);
7199
7200        // The same session at Main tier, through the tuned constructor so only the tier differs.
7201        let main = NvencEncoder::new_tuned(
7202            &s,
7203            ptr::null(),
7204            NvencTuning {
7205                hevc_high_tier: false,
7206                ..NvencTuning::default()
7207            },
7208        );
7209        match main {
7210            Ok(enc) => unsafe {
7211                println!(
7212                    "HEVC Main tier at {} kbps: opened on level {}",
7213                    s.video_bitrate_kbps, enc.encode_config.encodeCodecConfig.hevcConfig.level,
7214                )
7215            },
7216            Err(e) => println!(
7217                "HEVC Main tier at {} kbps: refused, the Main-tier ceiling: {e}",
7218                s.video_bitrate_kbps,
7219            ),
7220        }
7221    }
7222
7223    /// On a real GPU of two or more engines under API 12.1 or later, an AV1 session splits its
7224    /// frames at 1080p, in three strips on three engines, and an HEVC one only while a resize
7225    /// holds it at 4K, and the split frames decode to the picture painted; on one engine
7226    /// nothing splits. Ignored by default.
7227    #[test]
7228    #[ignore]
7229    fn gpu_split_frame_follows_the_codec_and_picture() {
7230        use crate::webcam::decode::VideoDecoder;
7231        use NV_ENC_SPLIT_ENCODE_MODE as Split;
7232        for codec in [Codec::H265, Codec::Av1] {
7233            let mut s = settings(1920, 1080, 60.0);
7234            s.codec = codec;
7235            let mut enc = match host_session(&s) {
7236                Ok(enc) => enc,
7237                Err(e) if e.contains("engine") => {
7238                    println!("{codec:?}: {e}");
7239                    continue;
7240                }
7241                Err(e) => panic!("{codec:?}: {e}"),
7242            };
7243            if !enc.engines.is_some_and(|n| n > 1) || nvenc_cur_ver() < (12, 1) {
7244                println!(
7245                    "{codec:?}: {}, API {:?}",
7246                    enc.split_summary(),
7247                    nvenc_cur_ver()
7248                );
7249                assert_eq!(
7250                    enc.init_params.splitEncodeMode(),
7251                    Split::NV_ENC_SPLIT_AUTO_MODE as u32
7252                );
7253                continue;
7254            }
7255            for (n, (w, h)) in [(1920usize, 1080usize), (3840, 2160), (1920, 1080)]
7256                .into_iter()
7257                .enumerate()
7258            {
7259                if n > 0 {
7260                    s.width = w as i32;
7261                    s.height = h as i32;
7262                    assert!(enc.reconfigure_resolution(&s).expect("resize"));
7263                }
7264                let want = match codec {
7265                    Codec::Av1 if enc.engines == Some(3) => Split::NV_ENC_SPLIT_THREE_FORCED_MODE,
7266                    Codec::Av1 => Split::NV_ENC_SPLIT_AUTO_FORCED_MODE,
7267                    _ if w * h >= 3840 * 2160 => Split::NV_ENC_SPLIT_AUTO_FORCED_MODE,
7268                    _ => Split::NV_ENC_SPLIT_AUTO_MODE,
7269                };
7270                assert_eq!(
7271                    enc.init_params.splitEncodeMode(),
7272                    want as u32,
7273                    "{codec:?} at {w}x{h}: {}",
7274                    enc.split_summary()
7275                );
7276                let mut dec = VideoDecoder::new(codec).expect("decoder");
7277                for i in 0..6usize {
7278                    let f = moving_frame(w, h, i);
7279                    let pkt = enc
7280                        .encode_cpu_packed(&f, w * 4, false, (n * 10 + i) as u64, 25, i == 0)
7281                        .expect("encode");
7282                    let psnr = luma_psnr(&mut dec, &pkt, &f, w, h);
7283                    assert!(psnr > 30.0, "{codec:?} {w}x{h} frame {i}: {psnr:.1} dB");
7284                }
7285                println!("{codec:?} {w}x{h}: {}", enc.split_summary());
7286            }
7287        }
7288    }
7289
7290    /// On a real GPU, a 1080p60 CBR session whose target lies past the ceiling of the level the
7291    /// picture alone would declare (H.264 4.2 at 62.5 Mbit/s, HEVC 4.1 High at 50, AV1's
7292    /// headroom 5.1 at the 26.7 `NVENC_AV1_RATE` holds it to where the driver does) opens on the
7293    /// level the rate raises it to, and a live raise past the ceiling, to 200 Mbit/s, which AV1
7294    /// holds at 106.7, is taken rather than refused. Ignored by default.
7295    #[test]
7296    #[ignore]
7297    fn gpu_cbr_targets_past_the_picture_level_open() {
7298        for (codec, kbps, level) in [
7299            (Codec::H264, 100_000, 50),
7300            (Codec::H265, 60_000, 150),
7301            (Codec::Av1, 30_000, 14),
7302        ] {
7303            let mut s = settings(1920, 1080, 60.0);
7304            s.codec = codec;
7305            s.video_cbr_mode = true;
7306            s.video_bitrate_kbps = kbps;
7307            let mut enc = match host_session(&s) {
7308                Ok(enc) => enc,
7309                Err(e) if e.contains("engine") => {
7310                    println!("{codec:?}: {e}");
7311                    continue;
7312                }
7313                Err(e) => panic!("{codec:?} at {kbps} kbps must open on level {level}: {e}"),
7314            };
7315            let level = if codec == Codec::Av1 && !NVENC_AV1_WEIGHTED.load(Ordering::Relaxed) {
7316                13
7317            } else {
7318                level
7319            };
7320            assert_eq!(
7321                enc.declared_level(),
7322                level,
7323                "{codec:?} level at {kbps} kbps"
7324            );
7325            s.video_bitrate_kbps = 200_000;
7326            assert!(
7327                enc.reconfigure_rate(&s),
7328                "{codec:?} live raise to 200 Mbit/s refused"
7329            );
7330            assert!(
7331                enc.declared_level() > level,
7332                "{codec:?} level did not rise with the target"
7333            );
7334            println!(
7335                "{codec:?}: {kbps} kbps opened on level {level}; 200 Mbit/s live took level {}",
7336                enc.declared_level()
7337            );
7338        }
7339    }
7340
7341    /// On a real GPU with AV1, a 30 Mbit/s session declares 5.1, Annex A's level for the rate,
7342    /// where the driver admits it, and where the driver holds AV1 to `NVENC_AV1_RATE` it opens on
7343    /// 5.2 after the refusal, as does a live raise from 8 Mbit/s, at a key frame; either session
7344    /// then codes a frame. Ignored by default.
7345    #[test]
7346    #[ignore]
7347    fn gpu_av1_declares_the_lowest_level_the_driver_admits() {
7348        let f = frame(1920, 1080, 30);
7349        for raised in [false, true] {
7350            NVENC_AV1_WEIGHTED.store(false, Ordering::Relaxed);
7351            let mut s = settings(1920, 1080, 60.0);
7352            s.codec = Codec::Av1;
7353            s.video_cbr_mode = true;
7354            s.video_bitrate_kbps = if raised { 8_000 } else { 30_000 };
7355            let mut enc = match host_session(&s) {
7356                Ok(enc) => enc,
7357                Err(e) if e.contains("engine") => {
7358                    println!("Av1: {e}");
7359                    return;
7360                }
7361                Err(e) => panic!("Av1 at {} kbps: {e}", s.video_bitrate_kbps),
7362            };
7363            enc.encode_cpu_argb(&f, 1920 * 4, 0, 25, true)
7364                .expect("encode");
7365            if raised {
7366                assert_eq!(enc.declared_level(), 13);
7367                s.video_bitrate_kbps = 30_000;
7368                assert!(
7369                    enc.reconfigure_rate(&s),
7370                    "the raise to 30 Mbit/s was refused"
7371                );
7372            }
7373            let weighted = NVENC_AV1_WEIGHTED.load(Ordering::Relaxed);
7374            assert_eq!(enc.declared_level(), if weighted { 14 } else { 13 });
7375            let pkt = enc
7376                .encode_cpu_argb(&f, 1920 * 4, 1, 25, false)
7377                .expect("encode at the level");
7378            if raised && weighted {
7379                assert!(
7380                    crate::encoders::codec::av1_is_key(&pkt[VIDEO_HEADER_LEN..]),
7381                    "a raised level reaches the decoder at a key frame"
7382                );
7383            }
7384            println!(
7385                "Av1 30 Mbit/s {}: level {} ({})",
7386                if raised { "raised from 8" } else { "opened" },
7387                enc.declared_level(),
7388                if weighted {
7389                    "the driver holds two thirds of Annex A"
7390                } else {
7391                    "Annex A"
7392                }
7393            );
7394        }
7395    }
7396
7397    /// On a real GPU, a live reconfigure costs a key frame only where one is asked for: a plain
7398    /// CBR rate change and a paint-over quantizer change go on predicting, and a target past the
7399    /// level's bitrate ceiling raises it at a key frame. A driver negotiated below 12.2 reads the
7400    /// flags elsewhere, so run it under `PIXELFLUX_NVENC_MAX_API` at each negotiable version too.
7401    /// Ignored by default.
7402    #[test]
7403    #[ignore]
7404    fn gpu_reconfigures_cost_a_key_frame_only_where_asked() {
7405        let f = frame(1920, 1080, 30);
7406        let kind = |enc: &mut NvencEncoder, n: u64, crf: u32| {
7407            enc.encode_cpu_argb(&f, 1920 * 4, n, crf, n == 0)
7408                .expect("encode")[1]
7409                & 0x0f
7410        };
7411        let mut s = settings(1920, 1080, 60.0);
7412        s.video_cbr_mode = true;
7413        s.video_bitrate_kbps = 8000;
7414        let mut enc = host_session(&s).expect("NVENC init");
7415        kind(&mut enc, 0, 25);
7416        for (n, kbps) in [(1, 6000), (2, 8000), (3, 6000)] {
7417            s.video_bitrate_kbps = kbps;
7418            assert!(enc.reconfigure_rate(&s));
7419            assert_eq!(
7420                kind(&mut enc, n, 25),
7421                FRAME_DELTA,
7422                "{:?}: after the change to {kbps} kbit/s",
7423                nvenc_cur_ver()
7424            );
7425        }
7426        s.video_bitrate_kbps = 200_000;
7427        assert!(enc.reconfigure_rate(&s));
7428        assert_eq!(
7429            kind(&mut enc, 4, 25),
7430            FRAME_KEY,
7431            "{:?}: the raised level reaches the stream at a key frame",
7432            nvenc_cur_ver()
7433        );
7434        let mut enc = host_session(&settings(1920, 1080, 60.0)).expect("NVENC init");
7435        for n in 0..6u64 {
7436            let want = if n == 0 { FRAME_KEY } else { FRAME_DELTA };
7437            assert_eq!(
7438                kind(&mut enc, n, if n % 2 == 0 { 25 } else { 18 }),
7439                want,
7440                "{:?}: paint-over frame {n}",
7441                nvenc_cur_ver()
7442            );
7443        }
7444    }
7445
7446    /// On a real GPU, print the device-memory cost of one 1080p session (via `nvidia-smi`),
7447    /// for measuring the reconfigure-headroom overhead. Ignored by default.
7448    #[test]
7449    #[ignore]
7450    fn gpu_vram_probe() {
7451        // The measurement wants the driver's own view of the device, which only nvidia-smi
7452        // gives from outside the session's context; a host or container without it reports that
7453        // rather than failing a test that cannot measure anything. Summed over every GPU it lists,
7454        // so the delta is the session's on whichever device it opened.
7455        fn used_mb() -> Option<i64> {
7456            let out = std::process::Command::new("nvidia-smi")
7457                .args(["--query-gpu=memory.used", "--format=csv,noheader,nounits"])
7458                .output()
7459                .ok()?;
7460            String::from_utf8_lossy(&out.stdout)
7461                .lines()
7462                .map(|l| l.trim().parse::<i64>().ok())
7463                .sum()
7464        }
7465        let s = settings(1920, 1080, 60.0);
7466        let Some(before) = used_mb() else {
7467            println!("VRAM probe skipped: nvidia-smi answered nothing");
7468            return;
7469        };
7470        let mut enc = host_session(&s).expect("init");
7471        let f = frame(1920, 1080, 5);
7472        for i in 0..3u64 {
7473            enc.encode_cpu_argb(&f, 1920 * 4, i, 25, i == 0)
7474                .expect("encode");
7475        }
7476        println!(
7477            "VRAM delta for one 1080p session: {} MiB",
7478            used_mb().unwrap_or(before) - before
7479        );
7480    }
7481
7482    /// On a real GPU, a session that starts taller than the default 2304 headroom (portrait
7483    /// 4K: 2160×4096, within NVENC's 4096 H.264 cap) takes its own size as the `maxEncode` ceiling
7484    /// and encodes at that resolution. Ignored by default.
7485    /// The VUI has to describe what the session actually emits, on a real GPU and through the
7486    /// same caps negotiation a session takes: BT.709 at limited range, which is what the kernel
7487    /// writes for 4:2:0 and what NVENC's own conversion follows for 4:4:4. Primaries and
7488    /// transfer follow the source, which is sRGB desktop pixels and therefore BT.709 too. A
7489    /// client that inverts the wrong matrix, or expands a limited-range frame as full-range,
7490    /// shifts color visibly.
7491    #[test]
7492    #[ignore]
7493    fn gpu_vui_describes_the_conversion() {
7494        for fullcolor in [false, true] {
7495            let mut s = settings(1280, 720, 60.0);
7496            s.video_fullcolor = fullcolor;
7497            let enc = host_session(&s).expect("NVENC init");
7498            // encodeCodecConfig is a union; a successful init leaves the H.264 arm live.
7499            let h264 = unsafe { &enc.encode_config.encodeCodecConfig.h264Config };
7500            let vui = &h264.h264VUIParameters;
7501            assert_eq!(
7502                vui.colourMatrix as u32,
7503                NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709 as u32,
7504                "fullcolor={fullcolor}"
7505            );
7506            assert_eq!(
7507                vui.colourPrimaries as u32,
7508                NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709 as u32
7509            );
7510            assert_eq!(
7511                vui.transferCharacteristics as u32,
7512                NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709 as u32
7513            );
7514            assert_eq!(vui.videoFullRangeFlag, 0, "fullcolor={fullcolor}");
7515            assert_eq!(h264.chromaFormatIDC, if fullcolor { 3 } else { 1 });
7516        }
7517    }
7518
7519    /// On a real GPU with a render node: the chroma convert reaches the zero-copy path too. A
7520    /// dmabuf painted with alternating single-pixel columns whose pair averages to gray comes
7521    /// back with neutral chroma, whichever way the driver mapped the import — pitch-linear, read
7522    /// in place, or a CUDA array, read through a texture. Neither case copies the RGB.
7523    /// Ignored by default.
7524    #[test]
7525    #[ignore]
7526    fn gpu_dmabuf_chroma_is_sited_at_the_block_center() {
7527        use crate::webcam::decode::{Codec, Decoder as _, VideoDecoder};
7528        let (w, h) = (256u32, 256u32);
7529        let s = settings(w as i32, h as i32, 60.0);
7530        let (gbm, mut renderer) = gpu_render();
7531        let egl_display = renderer.egl_context().display().get_display_handle().handle;
7532        let (_bo, dmabuf) = column_dmabuf(&gbm, &mut renderer, w, h);
7533        let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
7534        assert!(enc.csc.is_some(), "this GPU took no chroma convert");
7535        let pkt = enc.encode(&dmabuf, 0, 20, true).expect("dmabuf encode");
7536        let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
7537        assert!(
7538            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
7539            "no picture"
7540        );
7541        let v = dec.frame().expect("decoded frame");
7542        let (mut su, mut sv) = (0.0f64, 0.0f64);
7543        let n = (v.chroma_height() * v.chroma_width()) as f64;
7544        for r in 0..v.chroma_height() {
7545            for c in 0..v.chroma_width() {
7546                let i = r * v.uv_stride + c;
7547                su += f64::from(v.u[i]);
7548                sv += f64::from(v.v[i]);
7549            }
7550        }
7551        let (u, cr) = (su / n, sv / n);
7552        println!(
7553            "[chroma-siting] dmabuf mapped as {}: ({u:.1}, {cr:.1})",
7554            mapped_kind(&enc)
7555        );
7556        let off = (u - 128.0).hypot(cr - 128.0);
7557        assert!(
7558            off <= 2.0,
7559            "the zero-copy path leaves chroma {off:.1} off neutral"
7560        );
7561    }
7562
7563    /// A dmabuf painted with alternating single-pixel columns of blue and yellow, whose pair
7564    /// averages to gray.
7565    fn column_dmabuf(
7566        gbm: &gbm::Device<std::fs::File>,
7567        renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
7568        w: u32,
7569        h: u32,
7570    ) -> (gbm::BufferObject<()>, Dmabuf) {
7571        use gbm::{BufferObjectFlags, Format as GbmFormat};
7572        use smithay::backend::renderer::{Bind, Color32F, Frame, Renderer};
7573        use smithay::utils::{Physical, Rectangle, Size, Transform};
7574        let bo = gbm
7575            .create_buffer_object::<()>(w, h, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
7576            .expect("GBM buffer");
7577        let mut dmabuf = crate::create_dmabuf_from_bo(&bo);
7578        {
7579            let mut fb = renderer.bind(&mut dmabuf).expect("bind dmabuf");
7580            let size: Size<i32, Physical> = (w as i32, h as i32).into();
7581            let mut frame = renderer
7582                .render(&mut fb, size, Transform::Normal)
7583                .expect("render");
7584            let full: Rectangle<i32, Physical> = Rectangle::from_size(size);
7585            frame
7586                .clear(Color32F::new(0.0, 0.0, 1.0, 1.0), &[full])
7587                .expect("clear");
7588            for x in (1..w as i32).step_by(2) {
7589                let column: Rectangle<i32, Physical> =
7590                    Rectangle::new((x, 0).into(), (1, h as i32).into());
7591                frame
7592                    .draw_solid(
7593                        column,
7594                        &[Rectangle::from_size(column.size)],
7595                        Color32F::new(1.0, 1.0, 0.0, 1.0),
7596                    )
7597                    .expect("draw column");
7598            }
7599            let sync = frame.finish().expect("finish");
7600            let _ = sync.wait();
7601        }
7602        (bo, dmabuf)
7603    }
7604
7605    /// On a real GPU: a 4:2:0 session sites chroma at the center of the block on both axes.
7606    /// Rows of a color pair that averages to gray, and columns of the same pair, both come back
7607    /// neutral. NVENC's own conversion averages the rows but weights the columns 3:1, which is
7608    /// what `ChromaConvert` replaces; with the convert disabled the column case lands three
7609    /// quarters of the way to the left column's chroma, which is the color subpixel-antialiased
7610    /// text would keep on its glyph edges. That conversion follows the BT.709 the session
7611    /// declares, so the mix is compared against BT.709 here and against BT.601 in
7612    /// `gpu_hardware_conversion_matches_the_declared_matrix`, which declares that instead. A
7613    /// 4:4:4 session subsamples nothing, so it keeps the hardware conversion. Ignored by
7614    /// default.
7615    #[test]
7616    #[ignore]
7617    fn gpu_chroma_is_sited_at_the_block_center() {
7618        use crate::encoders::chroma_siting::{BT709, chroma};
7619        let (w, h) = (256usize, 256usize);
7620        let (blue, yellow) = ([0.0, 0.0, 255.0], [255.0, 255.0, 0.0]);
7621        let st = settings(w as i32, h as i32, 60.0);
7622        let mut enc = host_session(&st).expect("NVENC init");
7623        assert!(enc.csc.is_some(), "this GPU took no chroma convert");
7624        let rows = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, false));
7625        let cols = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, true));
7626        println!("[chroma-siting] convert: rows {rows:?} columns {cols:?}");
7627        for (label, (u, v)) in [("rows", rows), ("columns", cols)] {
7628            let off = (u - 128.0).hypot(v - 128.0);
7629            assert!(off <= 2.0, "{label} come back {off:.1} off neutral chroma");
7630        }
7631        unsafe {
7632            let cu = enc.cuda.clone();
7633            (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
7634            if let Some(csc) = enc.csc.take() {
7635                csc.release(&cu, &enc.nvenc_funcs, enc.encoder_session);
7636            }
7637            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
7638        }
7639        let hardware = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, true));
7640        let weighted = chroma([0, 1, 2].map(|i| 0.75 * blue[i] + 0.25 * yellow[i]), BT709);
7641        println!("[chroma-siting] hardware: columns {hardware:?} against 3:1 {weighted:?}");
7642        assert!(
7643            (hardware.0 - weighted.0).hypot(hardware.1 - weighted.1) <= 2.0,
7644            "NVENC's own conversion weights the columns 3:1: {hardware:?} against {weighted:?}"
7645        );
7646
7647        let full = RustCaptureSettings {
7648            video_fullcolor: true,
7649            ..st
7650        };
7651        let enc444 = host_session(&full).expect("NVENC init");
7652        if enc444.is_fullcolor() {
7653            assert!(
7654                enc444.csc.is_none(),
7655                "a 4:4:4 session has no chroma to site and takes no convert"
7656            );
7657        }
7658    }
7659
7660    /// On a real GPU, in both chroma formats: the color chart decodes back to the painted
7661    /// color when the matrix the VUI declares is inverted, which is what a client does with
7662    /// every frame. 4:2:0 comes through the kernel and 4:4:4 through NVENC's own conversion, so
7663    /// this is what holds both to the declared matrix — the siting check cannot see a wrong one,
7664    /// its tile being neutral whichever matrix converts it. OpenH264, the H.264 decoder the tests
7665    /// carry, refuses a 4:4:4 stream's SPS, so that stream is held to what its SPS declares and
7666    /// the 4:4:4 picture is read back from the HEVC session, which converts the same way. Ignored
7667    /// by default.
7668    #[test]
7669    #[ignore]
7670    fn gpu_chart_decodes_to_the_color_that_was_painted() {
7671        use crate::encoders::chroma_siting::{BT709, chart_bgra, chart_error};
7672        use crate::encoders::sps::{h264_chroma_format_idc, read_color};
7673        use crate::webcam::decode::{Decoder as _, VideoDecoder};
7674        let (w, h) = (256usize, 128usize);
7675        for (codec, fullcolor) in [
7676            (Codec::H264, false),
7677            (Codec::H264, true),
7678            (Codec::H265, true),
7679        ] {
7680            let st = RustCaptureSettings {
7681                codec,
7682                video_fullcolor: fullcolor,
7683                ..settings(w as i32, h as i32, 60.0)
7684            };
7685            let mut enc = host_session(&st).expect("NVENC init");
7686            if fullcolor && !enc.is_fullcolor() {
7687                println!("[chart] this GPU carries no 4:4:4 {codec:?}");
7688                continue;
7689            }
7690            assert_eq!(
7691                enc.csc.is_some(),
7692                !fullcolor,
7693                "the convert follows the chroma format"
7694            );
7695            let check = |enc: &mut NvencEncoder, bgra: &[u8], w: usize, frame: u64, what: &str| {
7696                let pkt = enc
7697                    .encode_cpu_packed(bgra, w * 4, false, frame, 20, true)
7698                    .expect("encode");
7699                let stream = &pkt[VIDEO_HEADER_LEN..];
7700                if codec == Codec::H264 && fullcolor {
7701                    assert_eq!(
7702                        h264_chroma_format_idc(stream),
7703                        Some(3),
7704                        "the SPS declares 4:4:4"
7705                    );
7706                    let sps = crate::encoders::codec::annexb_nals(stream)
7707                        .find(|n| n[0] & 0x1f == 7)
7708                        .expect("an SPS");
7709                    assert_eq!(
7710                        read_color(sps).map(|s| (s.matrix, s.full_range)),
7711                        Some((1, false)),
7712                        "the SPS declares BT.709 limited"
7713                    );
7714                    println!("[chart] NVENC H264 4:4:4{what}: the SPS declares 4:4:4 BT.709");
7715                    return;
7716                }
7717                let mut dec = VideoDecoder::new(codec).expect("decoder");
7718                assert!(dec.decode(stream).expect("decode"), "no picture");
7719                let worst = chart_error(&dec.frame().expect("decoded frame"), BT709);
7720                println!(
7721                    "[chart] NVENC {codec:?} 4:4:4 {fullcolor}{what}: worst |dRGB| {worst:.1}"
7722                );
7723                assert!(
7724                    worst <= 8.0,
7725                    "the session paints {worst:.1} off the chart{what}"
7726                );
7727            };
7728            check(&mut enc, &chart_bgra(w, h), w, 0, "");
7729
7730            // A resize rebuilds the convert's surface around the new geometry, so the chart is
7731            // read back again at a size the session was not opened for.
7732            let (w2, h2) = (w + 64, h + 32);
7733            let grown = RustCaptureSettings {
7734                width: w2 as i32,
7735                height: h2 as i32,
7736                ..st
7737            };
7738            assert!(
7739                enc.reconfigure_resolution(&grown).expect("resize"),
7740                "the resize was taken"
7741            );
7742            assert_eq!(
7743                enc.csc.is_some(),
7744                !fullcolor,
7745                "the convert followed the resize"
7746            );
7747            check(&mut enc, &chart_bgra(w2, h2), w2, 1, " after a resize");
7748        }
7749    }
7750
7751    /// On a real GPU, the path a driver that refuses the PTX takes: a session the kernel is
7752    /// kept out of still encodes, and NVENC's own conversion follows the BT.709 its VUI
7753    /// declares — the same session with the matrix declared as SMPTE170M measures the BT.601
7754    /// mix instead, which is how that dependency was established and why the fallback can
7755    /// declare a matrix at all. Only the siting differs, the columns landing on the 3:1
7756    /// weighting. Ignored by default.
7757    #[test]
7758    #[ignore]
7759    fn gpu_hardware_conversion_matches_the_declared_matrix() {
7760        use crate::encoders::chroma_siting::{BT601, BT709, chart_bgra, chart_error, chroma};
7761        use crate::webcam::decode::{Decoder as _, VideoDecoder};
7762        let (w, h) = (256usize, 256usize);
7763        let (blue, yellow) = ([0.0, 0.0, 255.0], [255.0, 255.0, 0.0]);
7764        let st = settings(w as i32, h as i32, 60.0);
7765        let tuning = NvencTuning {
7766            hardware_csc: true,
7767            ..Default::default()
7768        };
7769        let mut enc = NvencEncoder::new_tuned(&st, ptr::null(), tuning).expect("NVENC init");
7770        assert!(
7771            enc.csc.is_none(),
7772            "the session was asked for NVENC's own conversion"
7773        );
7774
7775        let pkt = enc
7776            .encode_cpu_packed(&chart_bgra(w, h), w * 4, false, 0, 20, true)
7777            .expect("encode");
7778        let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
7779        assert!(
7780            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
7781            "no picture"
7782        );
7783        let worst = chart_error(&dec.frame().expect("decoded frame"), BT709);
7784        println!("[csc] NVENC's own conversion: chart worst |dRGB| {worst:.1}");
7785        assert!(
7786            worst <= 8.0,
7787            "the hardware conversion paints {worst:.1} off the declared matrix"
7788        );
7789
7790        let hardware = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, true));
7791        let mix = [0, 1, 2].map(|i| 0.75 * blue[i] + 0.25 * yellow[i]);
7792        let (weighted_601, weighted_709) = (chroma(mix, BT601), chroma(mix, BT709));
7793        let off = |c: (f64, f64)| (hardware.0 - c.0).hypot(hardware.1 - c.1);
7794        println!(
7795            "[csc] NVENC's own conversion: columns {hardware:?} against the 3:1 mix under \
7796             BT.709 {weighted_709:?} and BT.601 {weighted_601:?}"
7797        );
7798        assert!(
7799            off(weighted_709) <= 2.0 && off(weighted_709) < off(weighted_601),
7800            "the hardware conversion did not follow the declared matrix: {hardware:?}"
7801        );
7802    }
7803
7804    /// On a real GPU: the external-pointer path encodes the caller's buffer where it lies. The
7805    /// session's own staging surface is painted a color the frame does not contain first, so a
7806    /// picture that had been copied through it would decode to that color instead — this is the
7807    /// X11 NvFBC hand-over, without needing NvFBC to reach it. A pitch that cannot cover the
7808    /// session's rows is refused rather than read past. Ignored by default.
7809    #[test]
7810    #[ignore]
7811    fn gpu_external_pointer_is_encoded_where_it_lies() {
7812        use crate::encoders::chroma_siting::{BT709, ycbcr};
7813        use crate::webcam::decode::VideoDecoder;
7814        let (w, h) = (256usize, 128usize);
7815        let st = settings(w as i32, h as i32, 60.0);
7816        let mut enc = host_session(&st).expect("NVENC init");
7817        let paint = [32u8, 192, 64];
7818        let poison = [240u8, 16, 200];
7819        let (external, external_pitch) = unsafe {
7820            let cu = enc.cuda.clone();
7821            (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
7822            let (mut ptr_, mut pitch) = (0 as CUdeviceptr, 0usize);
7823            assert_eq!(
7824                (cu.cuMemAllocPitch_v2)(&mut ptr_, &mut pitch, w * 4, h, 16),
7825                CUresult::CUDA_SUCCESS
7826            );
7827            paint_surface(&cu, ptr_, pitch, w, h, paint);
7828            paint_surface(&cu, enc.input_device_ptr, enc.input_pitch, w, h, poison);
7829            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
7830            (ptr_, pitch)
7831        };
7832        let pkt = enc
7833            .encode_cuda_pitch(external, external_pitch, false, 0, 20, true)
7834            .expect("external encode");
7835        let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
7836        let (mean, _) = decoded_means(&mut dec, &pkt, (0, 0, w as i32, h as i32));
7837        let want = ycbcr(paint.map(f64::from), BT709);
7838        println!("[external] decoded {mean:?} against the painted {want:?}");
7839        for i in 0..3 {
7840            assert!(
7841                (mean[i] - want[i]).abs() <= 8.0,
7842                "plane {i} came back {:.1}, not the painted {:.1}: the frame was staged, not read in place",
7843                mean[i],
7844                want[i]
7845            );
7846        }
7847        assert!(
7848            enc.encode_cuda_pitch(external, w * 4 - 4, false, 1, 20, false)
7849                .is_err(),
7850            "a pitch too short for the session's rows must be refused"
7851        );
7852        enc.release_external_input();
7853        unsafe {
7854            let cu = enc.cuda.clone();
7855            (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
7856            (cu.cuMemFree_v2)(external);
7857            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
7858        }
7859    }
7860
7861    /// On a real GPU: a host frame asked to be hashed is hashed where it was uploaded, beside its
7862    /// encode, its band hashes taken with the frame: one a band, the same for the same picture,
7863    /// different only in the band a caret changed, and none for a frame that did not ask; and
7864    /// what that costs the encode. Ignored by default.
7865    #[test]
7866    #[ignore]
7867    fn gpu_an_uploaded_frame_is_hashed_beside_its_encode() {
7868        let (w, h, rows) = (1920usize, 1080usize, 32u32);
7869        let mut enc = host_session(&settings(w as i32, h as i32, 60.0)).expect("NVENC init");
7870        let mut picture = frame(w, h, 3);
7871        let encode = |enc: &mut NvencEncoder, picture: &[u8], n: u64, hash: bool| {
7872            if hash {
7873                assert!(enc.hash_next_upload(rows), "the driver takes the band hash");
7874            }
7875            let out = enc.encode_cpu_packed(picture, w * 4, false, n, 23, n == 0);
7876            assert!(out.is_ok_and(|o| !o.is_empty()), "frame {n} encodes");
7877            enc.take_upload_hashes()
7878        };
7879        let first = encode(&mut enc, &picture, 0, true).expect("hashes come with the frame");
7880        assert_eq!(first.len(), h.div_ceil(rows as usize));
7881        assert_eq!(
7882            encode(&mut enc, &picture, 1, true),
7883            Some(first.clone()),
7884            "one picture, one hash"
7885        );
7886        assert_eq!(encode(&mut enc, &picture, 2, false), None, "none unasked");
7887        for y in 200..220 {
7888            for x in 300..302 {
7889                picture[(y * w + x) * 4..][..3].copy_from_slice(&[0, 0, 0]);
7890            }
7891        }
7892        let caret = encode(&mut enc, &picture, 3, true).unwrap();
7893        let changed: Vec<usize> = (0..first.len()).filter(|&i| first[i] != caret[i]).collect();
7894        assert_eq!(changed, vec![200 / rows as usize], "a 2x20 caret");
7895
7896        let mut timed = |hash: bool, from: u64| {
7897            let t = std::time::Instant::now();
7898            for n in from..from + 120 {
7899                picture[(n as usize % h) * w * 4] ^= 0xff;
7900                encode(&mut enc, &picture, n, hash);
7901            }
7902            t.elapsed().as_secs_f64() * 1e3 / 120.0
7903        };
7904        let (plain, hashed) = (timed(false, 4), timed(true, 124));
7905        let (plain2, hashed2) = (timed(false, 244), timed(true, 364));
7906        eprintln!(
7907            "1080p host encode: {:.3} / {:.3} ms plain, {:.3} / {:.3} ms with the device hash",
7908            plain, plain2, hashed, hashed2
7909        );
7910    }
7911
7912    /// On a real GPU: the band hash reads a caret as the one band it is in and two pixels
7913    /// swapped as a change, but neither the alpha byte nor the pitch's padding as one; and what
7914    /// it costs a 1080p frame against the encode of it. Ignored by default.
7915    #[test]
7916    #[ignore]
7917    fn gpu_band_hash_reads_the_bands_that_changed() {
7918        let (w, h, rows) = (1920usize, 1080usize, 32usize);
7919        let pitch = w * 4 + 64;
7920        let mut enc = host_session(&settings(w as i32, h as i32, 60.0)).expect("NVENC init");
7921        let (cu, ctx) = (enc.cuda.clone(), enc.cuda_context);
7922        let picture = frame(w, h, 3);
7923        let mut host = vec![0u8; pitch * h];
7924        for y in 0..h {
7925            host[y * pitch..][..w * 4].copy_from_slice(&picture[y * w * 4..][..w * 4]);
7926        }
7927        let mut dev: CUdeviceptr = 0;
7928        unsafe {
7929            (cu.cuCtxPushCurrent_v2)(ctx);
7930            assert_eq!(
7931                (cu.cuMemAlloc_v2)(&mut dev, pitch * h),
7932                CUresult::CUDA_SUCCESS
7933            );
7934            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
7935        }
7936        let upload = |host: &[u8]| unsafe {
7937            (cu.cuCtxPushCurrent_v2)(ctx);
7938            assert_eq!(
7939                (cu.cuMemcpyHtoD_v2)(dev, host.as_ptr() as *const c_void, host.len()),
7940                CUresult::CUDA_SUCCESS
7941            );
7942            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
7943        };
7944        let changed = |a: &[u64], b: &[u64]| -> Vec<usize> {
7945            (0..a.len()).filter(|&i| a[i] != b[i]).collect()
7946        };
7947        upload(&host);
7948        let hash =
7949            |enc: &mut NvencEncoder| enc.band_hashes(dev, pitch, w as u32, h as u32, rows as u32);
7950        let jit = std::time::Instant::now();
7951        let first = hash(&mut enc).expect("the driver takes the band hash");
7952        let jit = jit.elapsed().as_secs_f64() * 1e3;
7953        assert_eq!(first.len(), h.div_ceil(rows));
7954        assert_eq!(hash(&mut enc).unwrap(), first, "one frame, one hash");
7955
7956        for y in 200..220 {
7957            for x in 300..302 {
7958                host[y * pitch + x * 4..][..3].copy_from_slice(&[0, 0, 0]);
7959            }
7960        }
7961        upload(&host);
7962        let caret = hash(&mut enc).unwrap();
7963        assert_eq!(changed(&first, &caret), vec![200 / rows], "a 2x20 caret");
7964
7965        for y in 0..h {
7966            for x in 0..w {
7967                host[y * pitch + x * 4 + 3] ^= 0x5a;
7968            }
7969            host[y * pitch + w * 4..][..64].fill(0xa5);
7970        }
7971        upload(&host);
7972        assert_eq!(
7973            hash(&mut enc).unwrap(),
7974            caret,
7975            "the alpha byte and the padding are not the picture"
7976        );
7977
7978        let (a, b) = (650 * pitch + 100 * 4, 650 * pitch + 101 * 4);
7979        assert_ne!(host[a..a + 3], host[b..b + 3]);
7980        for i in 0..4 {
7981            host.swap(a + i, b + i);
7982        }
7983        upload(&host);
7984        assert_eq!(
7985            changed(&caret, &hash(&mut enc).unwrap()),
7986            vec![650 / rows],
7987            "two pixels swapped"
7988        );
7989
7990        let mut time = |w: u32, h: u32| -> Vec<f64> {
7991            let mut ms: Vec<f64> = (0..300)
7992                .map(|_| {
7993                    let t = std::time::Instant::now();
7994                    enc.band_hashes(dev, pitch, w, h, rows as u32).unwrap();
7995                    t.elapsed().as_secs_f64() * 1e3
7996                })
7997                .collect();
7998            ms.sort_by(f64::total_cmp);
7999            ms
8000        };
8001        let (hashing, fixed) = (time(w as u32, h as u32), time(64, rows as u32));
8002        let mut encoding: Vec<f64> = (0..120u64)
8003            .map(|i| {
8004                let t = std::time::Instant::now();
8005                enc.encode_cuda_pitch(dev, pitch, false, i, 25, i == 0)
8006                    .expect("encode");
8007                t.elapsed().as_secs_f64() * 1e3
8008            })
8009            .collect();
8010        encoding.sort_by(f64::total_cmp);
8011        println!(
8012            "[band hash] 1080p: {:.3} ms median, {:.3} ms p99 a frame ({:.3} ms of it for a 64x{rows} \
8013             one), the first {jit:.1} ms with the module's load; the encode {:.3} ms median",
8014            hashing[150], hashing[297], fixed[150], encoding[60]
8015        );
8016        enc.release_external_input();
8017        unsafe {
8018            (cu.cuCtxPushCurrent_v2)(ctx);
8019            (cu.cuMemFree_v2)(dev);
8020            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8021        }
8022    }
8023
8024    /// Test helper: fill a pitched device surface with one BGRA color.
8025    unsafe fn paint_surface(
8026        cuda: &CudaFunctions,
8027        dst: CUdeviceptr,
8028        dst_pitch: usize,
8029        w: usize,
8030        h: usize,
8031        rgb: [u8; 3],
8032    ) {
8033        let mut host = vec![255u8; w * h * 4];
8034        for px in host.as_chunks_mut::<4>().0 {
8035            px[..3].copy_from_slice(&[rgb[2], rgb[1], rgb[0]]);
8036        }
8037        let copy = CUDA_MEMCPY2D {
8038            srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
8039            srcHost: host.as_ptr() as *const c_void,
8040            srcPitch: w * 4,
8041            dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
8042            dstDevice: dst,
8043            dstPitch: dst_pitch,
8044            WidthInBytes: w * 4,
8045            Height: h,
8046            ..Default::default()
8047        };
8048        assert_eq!(
8049            (cuda.cuMemcpy2D_v2)(&copy),
8050            CUresult::CUDA_SUCCESS,
8051            "paint the surface"
8052        );
8053    }
8054
8055    /// A frame of two colors alternating by column or by row, so every 2x2 block averages to
8056    /// the gray between them.
8057    fn color_pair(w: usize, h: usize, a: [f64; 3], b: [f64; 3], by_column: bool) -> Vec<u8> {
8058        let mut f = vec![255u8; w * h * 4];
8059        for y in 0..h {
8060            for x in 0..w {
8061                let p = if (if by_column { x } else { y }) % 2 == 0 {
8062                    a
8063                } else {
8064                    b
8065                };
8066                f[(y * w + x) * 4..][..3].copy_from_slice(&[p[2] as u8, p[1] as u8, p[0] as u8]);
8067            }
8068        }
8069        f
8070    }
8071
8072    /// Encode one key frame of `bgra` and return the mean chroma of the decoded picture.
8073    fn encode_and_measure(enc: &mut NvencEncoder, bgra: &[u8]) -> (f64, f64) {
8074        use crate::webcam::decode::{Decoder as _, VideoDecoder};
8075        let w = enc.width() as usize;
8076        let pkt = enc
8077            .encode_cpu_packed(bgra, w * 4, false, 0, 20, true)
8078            .expect("packed encode");
8079        let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
8080        assert!(
8081            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
8082            "no picture from this access unit"
8083        );
8084        let v = dec.frame().expect("decoded frame");
8085        let (mut su, mut sv) = (0.0f64, 0.0f64);
8086        let n = (v.chroma_height() * v.chroma_width()) as f64;
8087        for r in 0..v.chroma_height() {
8088            for c in 0..v.chroma_width() {
8089                let i = r * v.uv_stride + c;
8090                su += f64::from(v.u[i]);
8091                sv += f64::from(v.v[i]);
8092            }
8093        }
8094        (su / n, sv / n)
8095    }
8096
8097    /// On a real GPU: what the chroma convert costs a frame, against NVENC converting the packed
8098    /// input itself, and how far its luma lands from the host convert's. Prints; ignored by default.
8099    #[test]
8100    #[ignore]
8101    fn gpu_bench_chroma_convert() {
8102        use crate::encoders::software::convert_to_yuv_mt;
8103        for (w, h) in [(1920usize, 1080usize), (3840, 2160)] {
8104            let st = settings(w as i32, h as i32, 60.0);
8105            let bgra = crate::encoders::chroma_siting::bgra(w, h);
8106            let mut kernel = host_session(&st).expect("NVENC init");
8107            assert!(kernel.csc.is_some());
8108            let with = bench_frames(&mut kernel, &bgra);
8109            let mut hardware = host_session(&st).expect("NVENC init");
8110            unsafe {
8111                let cu = hardware.cuda.clone();
8112                (cu.cuCtxPushCurrent_v2)(hardware.cuda_context);
8113                if let Some(csc) = hardware.csc.take() {
8114                    csc.release(&cu, &hardware.nvenc_funcs, hardware.encoder_session);
8115                }
8116                (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8117            }
8118            let without = bench_frames(&mut hardware, &bgra);
8119            let (cw, ch) = (w / 2, h / 2);
8120            let (mut y, mut u, mut v) = (vec![0u8; w * h], vec![0u8; cw * ch], vec![0u8; cw * ch]);
8121            let mut host = f64::MAX;
8122            for _ in 0..20 {
8123                let t = std::time::Instant::now();
8124                convert_to_yuv_mt(
8125                    &bgra,
8126                    (w * 4) as u32,
8127                    w,
8128                    h,
8129                    false,
8130                    false,
8131                    false,
8132                    false,
8133                    &mut y,
8134                    &mut u,
8135                    &mut v,
8136                    (w, cw),
8137                    8,
8138                )
8139                .expect("host convert");
8140                host = host.min(t.elapsed().as_secs_f64() * 1000.0);
8141            }
8142            println!(
8143                "[bench] {w}x{h}: encode {with:.2} ms with the convert, {without:.2} ms on NVENC's own; the host convert costs {host:.2} ms on 8 threads"
8144            );
8145        }
8146    }
8147
8148    /// Mean per-frame `encode_cpu_packed` time over 60 frames, after a warm-up.
8149    fn bench_frames(enc: &mut NvencEncoder, bgra: &[u8]) -> f64 {
8150        let w = enc.width() as usize;
8151        for i in 0..10 {
8152            enc.encode_cpu_packed(bgra, w * 4, false, i, 25, i == 0)
8153                .expect("warm-up");
8154        }
8155        let t = std::time::Instant::now();
8156        for i in 0..60u64 {
8157            enc.encode_cpu_packed(bgra, w * 4, false, 10 + i, 25, false)
8158                .expect("encode");
8159        }
8160        t.elapsed().as_secs_f64() * 1000.0 / 60.0
8161    }
8162
8163    #[test]
8164    #[ignore]
8165    fn gpu_init_above_default_headroom() {
8166        let s = settings(2160, 4096, 30.0);
8167        let mut enc = host_session(&s).expect("NVENC init portrait 4K");
8168        assert_eq!(enc.init_params.maxEncodeWidth, 4096);
8169        assert_eq!(enc.init_params.maxEncodeHeight, 4096);
8170        let f = frame(2160, 4096, 20);
8171        let pkt = enc
8172            .encode_cpu_argb(&f, 2160 * 4, 0, 25, true)
8173            .expect("encode portrait 4K");
8174        assert_eq!(wire_dims(&pkt), (2160, 4096));
8175    }
8176
8177    /// Thread CPU time, for the per-frame CPU cost of an encode path independent of how long
8178    /// the thread waited on the GPU.
8179    fn thread_cpu() -> std::time::Duration {
8180        let mut ts = libc::timespec {
8181            tv_sec: 0,
8182            tv_nsec: 0,
8183        };
8184        unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) };
8185        std::time::Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
8186    }
8187
8188    /// Test helper: run `f` `n` times and report wall and thread-CPU microseconds per call.
8189    fn per_frame(label: &str, n: usize, mut f: impl FnMut(usize)) -> (f64, f64) {
8190        let t0 = std::time::Instant::now();
8191        let c0 = thread_cpu();
8192        for i in 0..n {
8193            f(i);
8194        }
8195        let wall = t0.elapsed().as_secs_f64() * 1e6 / n as f64;
8196        let cpu = (thread_cpu() - c0).as_secs_f64() * 1e6 / n as f64;
8197        println!("{label}: {wall:.0} us wall/frame, {cpu:.0} us cpu/frame ({n} frames)");
8198        (wall, cpu)
8199    }
8200
8201    /// Test helper: the raw GBM device and GLES renderer of the render node named by
8202    /// `PIXELFLUX_TEST_RENDER_NODE`, else the NVIDIA GPU's, brought up exactly as the compositor
8203    /// brings them up.
8204    fn gpu_render() -> (
8205        gbm::Device<std::fs::File>,
8206        smithay::backend::renderer::gles::GlesRenderer,
8207    ) {
8208        let node = std::env::var("PIXELFLUX_TEST_RENDER_NODE")
8209            .ok()
8210            .or_else(|| crate::auto_select_render_node(Some("nvidia")))
8211            .expect("no NVIDIA render node");
8212        crate::gpu_render_init(std::path::Path::new(&node)).expect("GPU render init")
8213    }
8214
8215    /// Background and block colors painted into test dmabufs, as `Color32F` components.
8216    const BG: [f32; 3] = [0.1, 0.2, 0.8];
8217    const FG: [f32; 3] = [0.9, 0.3, 0.1];
8218
8219    /// Limited-range Y/Cb/Cr of a painted color, whose components are 0..1.
8220    fn painted_ycbcr(rgb: [f32; 3]) -> [f64; 3] {
8221        use crate::encoders::chroma_siting::{BT709, ycbcr};
8222        ycbcr(rgb.map(|c| f64::from(c) * 255.0), BT709)
8223    }
8224
8225    /// Where the foreground block of a `seed`-painted `w×h` frame sits: a quarter-size block
8226    /// whose origin moves with the seed.
8227    fn block_rect(w: u32, h: u32, seed: u32) -> (i32, i32, i32, i32) {
8228        let x = ((seed * 37) % (w / 2)) as i32 & !1;
8229        let y = ((seed * 53) % (h / 2)) as i32 & !1;
8230        (x, y, (w / 4) as i32 & !1, (h / 4) as i32 & !1)
8231    }
8232
8233    /// Test helper: allocate a `w×h` ARGB8888 render-target dmabuf on `gbm` and paint it with
8234    /// the GLES renderer — `BG` everywhere and an `FG` block at `block_rect(seed)` — waiting for
8235    /// the render to land before returning, as the compositor does before encoding.
8236    fn painted_dmabuf(
8237        gbm: &gbm::Device<std::fs::File>,
8238        renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
8239        w: u32,
8240        h: u32,
8241        seed: u32,
8242    ) -> (gbm::BufferObject<()>, Dmabuf) {
8243        use gbm::{BufferObjectFlags, Format as GbmFormat};
8244        use smithay::backend::renderer::{Bind, Color32F, Frame, Renderer};
8245        use smithay::utils::{Physical, Rectangle, Size, Transform};
8246        let bo = gbm
8247            .create_buffer_object::<()>(w, h, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
8248            .expect("GBM buffer");
8249        let mut dmabuf = crate::create_dmabuf_from_bo(&bo);
8250        {
8251            let mut fb = renderer.bind(&mut dmabuf).expect("bind dmabuf");
8252            let size: Size<i32, Physical> = (w as i32, h as i32).into();
8253            let mut frame = renderer
8254                .render(&mut fb, size, Transform::Normal)
8255                .expect("render");
8256            let full: Rectangle<i32, Physical> = Rectangle::from_size(size);
8257            frame
8258                .clear(Color32F::new(BG[0], BG[1], BG[2], 1.0), &[full])
8259                .expect("clear");
8260            let (x, y, bw, bh) = block_rect(w, h, seed);
8261            let block: Rectangle<i32, Physical> = Rectangle::new((x, y).into(), (bw, bh).into());
8262            frame
8263                .draw_solid(
8264                    block,
8265                    &[Rectangle::from_size(block.size)],
8266                    Color32F::new(FG[0], FG[1], FG[2], 1.0),
8267                )
8268                .expect("draw block");
8269            let sync = frame.finish().expect("finish");
8270            let _ = sync.wait();
8271        }
8272        (bo, dmabuf)
8273    }
8274
8275    /// Test helper: decode one H.264 access unit (the bytes behind the wire header)
8276    /// with the crate's H.264 decoder and return the mean Y/Cb/Cr inside `rect` and outside it.
8277    fn decoded_means(
8278        dec: &mut crate::webcam::decode::VideoDecoder,
8279        pkt: &[u8],
8280        rect: (i32, i32, i32, i32),
8281    ) -> ([f64; 3], [f64; 3]) {
8282        use crate::webcam::decode::Decoder;
8283        assert!(
8284            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
8285            "no picture from this access unit"
8286        );
8287        let v = dec.frame().expect("decoded frame");
8288        let (rx, ry, rw, rh) = rect;
8289        let inside = |x: usize, y: usize| {
8290            x as i32 >= rx && (x as i32) < rx + rw && y as i32 >= ry && (y as i32) < ry + rh
8291        };
8292        let mut acc = [[0f64; 3]; 2];
8293        let mut cnt = [0f64; 2];
8294        for y in 0..v.height {
8295            for x in 0..v.width {
8296                let k = if inside(x, y) { 0 } else { 1 };
8297                acc[k][0] += v.y[y * v.y_stride + x] as f64;
8298                acc[k][1] += v.u[(y / 2) * v.uv_stride + x / 2] as f64;
8299                acc[k][2] += v.v[(y / 2) * v.uv_stride + x / 2] as f64;
8300                cnt[k] += 1.0;
8301            }
8302        }
8303        let mean = |k: usize| [acc[k][0] / cnt[k], acc[k][1] / cnt[k], acc[k][2] / cnt[k]];
8304        (mean(0), mean(1))
8305    }
8306
8307    /// Assert decoded region means sit within `tol` of the limited-range values of the painted
8308    /// colors — a wrong pitch, byte order, or stale buffer lands far outside this.
8309    fn assert_painted(label: &str, block: [f64; 3], bg: [f64; 3], tol: f64) {
8310        let (eb, eg) = (painted_ycbcr(FG), painted_ycbcr(BG));
8311        for i in 0..3 {
8312            assert!(
8313                (block[i] - eb[i]).abs() <= tol,
8314                "{label}: block plane {i} = {:.1}, expected {:.1}",
8315                block[i],
8316                eb[i]
8317            );
8318            assert!(
8319                (bg[i] - eg[i]).abs() <= tol,
8320                "{label}: background plane {i} = {:.1}, expected {:.1}",
8321                bg[i],
8322                eg[i]
8323            );
8324        }
8325    }
8326
8327    /// Whether every cached dmabuf import of `enc` is registered with NVENC in place.
8328    fn all_direct(enc: &NvencEncoder) -> bool {
8329        !enc.dmabuf_cache.is_empty()
8330            && enc
8331                .dmabuf_cache
8332                .values()
8333                .all(|c| matches!(c.input, DmaBufInput::Direct { .. }))
8334    }
8335
8336    /// How the driver mapped the cached dmabuf imports of `enc`, for the test output.
8337    fn mapped_kind(enc: &NvencEncoder) -> &'static str {
8338        match enc
8339            .dmabuf_cache
8340            .values()
8341            .next()
8342            .map(|c| c.egl_frame.frame_type)
8343        {
8344            Some(CU_EGL_FRAME_TYPE_PITCH) => "pitch-linear",
8345            Some(CU_EGL_FRAME_TYPE_ARRAY) => "a CUDA array",
8346            _ => "an unknown frame kind",
8347        }
8348    }
8349
8350    /// On a real GPU with a render node: two GLES-painted dmabufs encode through the dmabuf path
8351    /// and decode to the painted colors at the painted positions, first with the direct
8352    /// registration enabled (in place when the driver maps the import pitch-linear, otherwise the
8353    /// copy arm) and then with it disabled — the two streams must agree, and the decoded content
8354    /// of both must match the paint. Prints which path the driver gave. Ignored by default; needs
8355    /// a render node of the NVIDIA GPU.
8356    #[test]
8357    #[ignore]
8358    fn gpu_dmabuf_direct_and_copy_paths_decode_to_the_paint() {
8359        use crate::webcam::decode::{Codec, VideoDecoder};
8360        let (w, h) = (1920u32, 1080u32);
8361        let s = settings(w as i32, h as i32, 60.0);
8362        let (gbm, mut renderer) = gpu_render();
8363        let egl_display = renderer.egl_context().display().get_display_handle().handle;
8364        let bufs: Vec<_> = (1..=2u32)
8365            .map(|seed| (seed, painted_dmabuf(&gbm, &mut renderer, w, h, seed)))
8366            .collect();
8367        let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
8368
8369        let run = |enc: &mut NvencEncoder, label: &str| -> Vec<Vec<u8>> {
8370            let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
8371            let mut out = Vec::new();
8372            for i in 0..6u64 {
8373                let (seed, (_, dmabuf)) = &bufs[(i % 2) as usize];
8374                let pkt = enc.encode(dmabuf, i, 25, i == 0).expect("dmabuf encode");
8375                assert_eq!(wire_dims(&pkt), (w as u16, h as u16));
8376                let (block, bg) = decoded_means(&mut dec, &pkt, block_rect(w, h, *seed));
8377                assert_painted(&format!("{label} frame {i}"), block, bg, 6.0);
8378                out.push(pkt[VIDEO_HEADER_LEN..].to_vec());
8379            }
8380            out
8381        };
8382
8383        // Painting the session's own staging surface a color neither frame contains proves the
8384        // direct arm never passes through it: a copied frame would decode to this instead.
8385        if all_direct(&enc) {
8386            unsafe {
8387                let cu = enc.cuda.clone();
8388                (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
8389                paint_surface(
8390                    &cu,
8391                    enc.input_device_ptr,
8392                    enc.input_pitch,
8393                    w as usize,
8394                    h as usize,
8395                    [240, 16, 200],
8396                );
8397                (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8398            }
8399        }
8400        let direct = run(&mut enc, "direct");
8401        println!(
8402            "driver mapped the dmabuf {}: {}",
8403            mapped_kind(&enc),
8404            if all_direct(&enc) {
8405                "registered in place"
8406            } else {
8407                "direct registration unavailable, copy arm used"
8408            }
8409        );
8410
8411        // The copy arm is driven from a session that never registered an import in place: a
8412        // same-size reconfigure keeps the geometry, and so keeps the imports it cached, since
8413        // only a real resolution change drains them.
8414        let mut copy_only = NvencEncoder::new(&s, egl_display).expect("NVENC init");
8415        copy_only.direct_dmabuf = false;
8416        let copied = run(&mut copy_only, "copy");
8417        assert!(!all_direct(&copy_only));
8418        let identical = direct.iter().zip(&copied).all(|(a, b)| a == b);
8419        println!(
8420            "direct vs copy streams: {} ({} vs {} bytes)",
8421            if identical {
8422                "byte-identical"
8423            } else {
8424                "differ"
8425            },
8426            direct.iter().map(Vec::len).sum::<usize>(),
8427            copied.iter().map(Vec::len).sum::<usize>()
8428        );
8429        if let Ok(dir) = std::env::var("NVENC_TEST_DUMP_DIR") {
8430            std::fs::write(format!("{dir}/dmabuf-direct.h264"), direct.concat()).unwrap();
8431            std::fs::write(format!("{dir}/dmabuf-copy.h264"), copied.concat()).unwrap();
8432        }
8433    }
8434
8435    /// On a real GPU with a render node: a 10-bit HEVC session takes dmabufs through the convert
8436    /// at either chroma, registered in place and through the copy arm, and the two arms code
8437    /// the same stream. Skips a device that lists no 10-bit HEVC. `NVENC_TEST_DUMP_DIR` keeps
8438    /// the streams for a decoder that takes them. Ignored by default.
8439    #[test]
8440    #[ignore]
8441    fn gpu_dmabuf_ten_bit_arms_agree() {
8442        let (w, h) = (1920u32, 1080u32);
8443        let (gbm, mut renderer) = gpu_render();
8444        let egl_display = renderer.egl_context().display().get_display_handle().handle;
8445        let bufs: Vec<_> = (1..=2u32)
8446            .map(|seed| painted_dmabuf(&gbm, &mut renderer, w, h, seed))
8447            .collect();
8448        for fullcolor in [false, true] {
8449            let mut s = settings(w as i32, h as i32, 60.0);
8450            s.codec = Codec::H265;
8451            s.video_fullcolor = fullcolor;
8452            s.video_bit_depth = 10;
8453            let mut streams = Vec::new();
8454            for direct in [true, false] {
8455                let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
8456                if enc.bit_depth() != 10 {
8457                    println!("this device codes no 10-bit HEVC");
8458                    return;
8459                }
8460                enc.direct_dmabuf = direct;
8461                let stream: Vec<u8> = (0..6u64)
8462                    .flat_map(|i| {
8463                        let (_, dmabuf) = &bufs[(i % 2) as usize];
8464                        let pkt = enc.encode(dmabuf, i, 25, i == 0).expect("dmabuf encode");
8465                        pkt[VIDEO_HEADER_LEN..].to_vec()
8466                    })
8467                    .collect();
8468                println!(
8469                    "4:4:4 {fullcolor} direct {direct}: mapped {}, {} bytes",
8470                    mapped_kind(&enc),
8471                    stream.len()
8472                );
8473                if let Ok(dir) = std::env::var("NVENC_TEST_DUMP_DIR") {
8474                    std::fs::write(
8475                        format!("{dir}/dmabuf-10bit-{fullcolor}-{direct}.h265"),
8476                        &stream,
8477                    )
8478                    .unwrap();
8479                }
8480                streams.push(stream);
8481            }
8482            assert_eq!(streams[0], streams[1], "4:4:4 {fullcolor}: the arms differ");
8483        }
8484    }
8485
8486    /// On a real GPU: a host frame handed over as BGRA (`rgba_input = false`) and the same image
8487    /// handed over as RGBA bytes (`rgba_input = true`) both decode to the painted colors — the
8488    /// input surface is re-registered in the other byte order in place — and the session keeps
8489    /// encoding across the switch. Ignored by default.
8490    #[test]
8491    #[ignore]
8492    fn gpu_packed_bgra_and_rgba_inputs_agree() {
8493        use crate::webcam::decode::{Codec, VideoDecoder};
8494        let (w, h) = (1280u32, 720u32);
8495        let s = settings(w as i32, h as i32, 60.0);
8496        let rect = block_rect(w, h, 3);
8497        let paint = |rgba: bool| -> Vec<u8> {
8498            let to_u8 = |c: f32| (c * 255.0).round() as u8;
8499            let mut f = vec![0u8; (w * h * 4) as usize];
8500            for y in 0..h as i32 {
8501                for x in 0..w as i32 {
8502                    let inside =
8503                        x >= rect.0 && x < rect.0 + rect.2 && y >= rect.1 && y < rect.1 + rect.3;
8504                    let c = if inside { FG } else { BG };
8505                    let px = &mut f[((y as u32 * w + x as u32) * 4) as usize..][..4];
8506                    let (r, g, b) = (to_u8(c[0]), to_u8(c[1]), to_u8(c[2]));
8507                    if rgba {
8508                        px.copy_from_slice(&[r, g, b, 255]);
8509                    } else {
8510                        px.copy_from_slice(&[b, g, r, 255]);
8511                    }
8512                }
8513            }
8514            f
8515        };
8516        let bgra = paint(false);
8517        let rgba = paint(true);
8518        let mut enc = host_session(&s).expect("NVENC init");
8519        let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
8520        let stride = (w * 4) as usize;
8521        // Byte order reaches the chroma convert as its own argument and the hardware conversion
8522        // as the packed surface's registered format, so both mechanisms are driven here: the
8523        // second pass is the path a GPU whose driver refuses the kernel takes.
8524        let pass = |enc: &mut NvencEncoder, dec: &mut VideoDecoder, tag: &str| {
8525            for (i, (buf, is_rgba)) in
8526                [(&bgra, false), (&rgba, true), (&bgra, false), (&rgba, true)]
8527                    .into_iter()
8528                    .enumerate()
8529            {
8530                let pkt = enc
8531                    .encode_cpu_packed(buf, stride, is_rgba, i as u64, 25, i == 0)
8532                    .expect("packed encode");
8533                if enc.csc.is_none() {
8534                    assert_eq!(
8535                        enc.input_format,
8536                        if is_rgba {
8537                            NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
8538                        } else {
8539                            NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
8540                        }
8541                    );
8542                }
8543                let (block, bg) = decoded_means(dec, &pkt, rect);
8544                assert_painted(&format!("{tag} frame {i} rgba={is_rgba}"), block, bg, 6.0);
8545            }
8546        };
8547        pass(&mut enc, &mut dec, "convert");
8548        unsafe {
8549            let cu = enc.cuda.clone();
8550            (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
8551            if let Some(csc) = enc.csc.take() {
8552                csc.release(&cu, &enc.nvenc_funcs, enc.encoder_session);
8553            }
8554            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8555        }
8556        pass(&mut enc, &mut dec, "hardware");
8557    }
8558
8559    /// On a real GPU: per-frame wall time of every NVENC preset, rate-control pass mode, and
8560    /// adaptive quantization, 1080p CBR on both codecs. Prints all; ignored by default.
8561    #[test]
8562    #[ignore]
8563    fn gpu_bench_tuning() {
8564        let (w, h) = (1920u32, 1080u32);
8565        let n: usize = std::env::var("NVENC_BENCH_FRAMES")
8566            .ok()
8567            .and_then(|v| v.parse().ok())
8568            .unwrap_or(240);
8569        let mut s = settings(w as i32, h as i32, 60.0);
8570        s.video_cbr_mode = true;
8571        s.video_bitrate_kbps = 8000;
8572        let frames: Vec<Vec<u8>> = (0..8u8)
8573            .map(|k| frame(w as usize, h as usize, 10 + 30 * k))
8574            .collect();
8575        let stride = (w * 4) as usize;
8576        let presets = [
8577            ("P1", NV_ENC_PRESET_P1_GUID),
8578            ("P2", NV_ENC_PRESET_P2_GUID),
8579            ("P3", NV_ENC_PRESET_P3_GUID),
8580            ("P4", NV_ENC_PRESET_P4_GUID),
8581            ("P5", NV_ENC_PRESET_P5_GUID),
8582            ("P6", NV_ENC_PRESET_P6_GUID),
8583            ("P7", NV_ENC_PRESET_P7_GUID),
8584        ];
8585        let passes = [
8586            ("single pass", NV_ENC_MULTI_PASS::NV_ENC_MULTI_PASS_DISABLED),
8587            (
8588                "two-pass quarter",
8589                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
8590            ),
8591            (
8592                "two-pass full",
8593                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_FULL_RESOLUTION,
8594            ),
8595        ];
8596        let mut cases: Vec<(String, NvencTuning)> = Vec::new();
8597        for (name, preset) in presets {
8598            cases.push((
8599                format!("{name} two-pass quarter"),
8600                NvencTuning {
8601                    preset,
8602                    ..NvencTuning::default()
8603                },
8604            ));
8605        }
8606        for (name, multipass) in passes {
8607            cases.push((
8608                format!("P4 {name}"),
8609                NvencTuning {
8610                    multipass,
8611                    ..NvencTuning::default()
8612                },
8613            ));
8614        }
8615        cases.push((
8616            "P4 two-pass quarter spatial AQ".into(),
8617            NvencTuning {
8618                spatial_aq: true,
8619                ..NvencTuning::default()
8620            },
8621        ));
8622        cases.push((
8623            "P4 two-pass quarter temporal AQ".into(),
8624            NvencTuning {
8625                temporal_aq: true,
8626                ..NvencTuning::default()
8627            },
8628        ));
8629        for codec in [Codec::H264, Codec::H265] {
8630            s.codec = codec;
8631            for (label, tuning) in &cases {
8632                let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), *tuning) {
8633                    Ok(enc) => enc,
8634                    Err(e) => {
8635                        println!("{codec:?} {label}: {e}");
8636                        continue;
8637                    }
8638                };
8639                enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true)
8640                    .expect("warm-up");
8641                let mut bytes = 0usize;
8642                per_frame(&format!("{codec:?} {label}"), n, |i| {
8643                    bytes += enc
8644                        .encode_cpu_packed(&frames[i % 8], stride, false, 1 + i as u64, 25, false)
8645                        .expect("encode")
8646                        .len();
8647                });
8648                println!("    {} kbps", bytes * 8 * 60 / n / 1000);
8649            }
8650        }
8651    }
8652
8653    /// On a real GPU: the production tuning against the presets P1 and P2, one L0 reference, and
8654    /// each split-frame mode, on real content, per codec at 1080p, 1440p, and 2160p at CBR 8, 14,
8655    /// and 30 Mbit/s: a page of text scrolling four rows a frame and a texture panned six and
8656    /// three pixels. Per row the encode time at the median and the 99th percentile, the bitrate
8657    /// against the target, and the luma PSNR. `NVENC_BENCH_HEIGHTS` (1080,1440,2160) and
8658    /// `NVENC_BENCH_CODECS` (H264,H265,Av1) pick rows, `NVENC_BENCH_FRAMES` their length. Prints
8659    /// all; ignored by default.
8660    #[test]
8661    #[ignore]
8662    fn gpu_bench_tuning_on_content() {
8663        use crate::webcam::decode::VideoDecoder;
8664        let n: usize = std::env::var("NVENC_BENCH_FRAMES")
8665            .ok()
8666            .and_then(|v| v.parse().ok())
8667            .unwrap_or(90);
8668        let glyph = |c: u32, gx: usize, gy: usize| {
8669            let s = c.wrapping_mul(2654435761) >> 7;
8670            (s & 1 == 1 && gx == 1)
8671                || (s & 2 == 2 && gx == 5)
8672                || (s & 4 == 4 && gy == 1)
8673                || (s & 8 == 8 && gy == 6)
8674                || (s & 16 == 16 && gy == 11)
8675                || (s & 32 == 32 && gx * 2 == gy)
8676        };
8677        let only = |var: &str, item: String| {
8678            std::env::var(var).map_or(true, |v| v.split(',').any(|x| x == item))
8679        };
8680        use NV_ENC_SPLIT_ENCODE_MODE as Split;
8681        for (w, h, kbps) in [
8682            (1920usize, 1080usize, 8000),
8683            (2560, 1440, 14000),
8684            (3840, 2160, 30000),
8685        ] {
8686            if !only("NVENC_BENCH_HEIGHTS", h.to_string()) {
8687                continue;
8688            }
8689            let mut page = vec![255u8; w * 2 * h * 4];
8690            for y in 0..2 * h {
8691                let (cy, gy) = (y / 18, y % 18);
8692                if cy % 9 == 8 || gy >= 13 {
8693                    continue;
8694                }
8695                for x in 0..w {
8696                    let (cx, gx) = (x / 10, x % 10);
8697                    let c = (cx as u32).wrapping_mul(31).wrapping_add(cy as u32 * 977);
8698                    if gx < 7 && (cx + cy * 3) % 13 != 0 && glyph(c, gx, gy) {
8699                        page[(y * w + x) * 4..(y * w + x) * 4 + 3].copy_from_slice(&[40, 30, 20]);
8700                    }
8701                }
8702            }
8703            let (tw, th) = (w * 2, h * 2);
8704            let mut tex = vec![0u8; tw * th * 4];
8705            for y in 0..th {
8706                for x in 0..tw {
8707                    let (fx, fy) = (x as f32, y as f32);
8708                    let v = 128.0
8709                        + 50.0 * (fx / 97.0).sin() * (fy / 61.0).cos()
8710                        + 30.0 * (fx / 13.0 + fy / 29.0).sin()
8711                        + ((x as u32).wrapping_mul(2654435761) ^ (y as u32).wrapping_mul(40503))
8712                            as f32
8713                            / u32::MAX as f32
8714                            * 24.0;
8715                    let i = (y * tw + x) * 4;
8716                    tex[i] = v as u8;
8717                    tex[i + 1] = (v * 0.8) as u8;
8718                    tex[i + 2] = (255.0 - v) as u8;
8719                }
8720            }
8721            let mut bufs = [vec![0u8; w * h * 4], vec![0u8; w * h * 4]];
8722            let draw = |scene: usize, t: usize, out: &mut Vec<u8>| {
8723                for y in 0..h {
8724                    let row = &mut out[y * w * 4..(y + 1) * w * 4];
8725                    if scene == 0 {
8726                        let src = (y + t * 4) % (2 * h);
8727                        row.copy_from_slice(&page[src * w * 4..(src + 1) * w * 4]);
8728                    } else {
8729                        let (ox, oy) = ((t * 6) % w, (y + t * 3) % h);
8730                        row.copy_from_slice(&tex[(oy * tw + ox) * 4..(oy * tw + ox + w) * 4]);
8731                    }
8732                }
8733            };
8734            for codec in [Codec::H264, Codec::H265, Codec::Av1] {
8735                if !only("NVENC_BENCH_CODECS", format!("{codec:?}")) {
8736                    continue;
8737                }
8738                let mut rows: Vec<(String, NvencTuning)> = vec![
8739                    ("production".into(), NvencTuning::default()),
8740                    (
8741                        "P1".into(),
8742                        NvencTuning {
8743                            preset: NV_ENC_PRESET_P1_GUID,
8744                            ..NvencTuning::default()
8745                        },
8746                    ),
8747                    (
8748                        "P2".into(),
8749                        NvencTuning {
8750                            preset: NV_ENC_PRESET_P2_GUID,
8751                            ..NvencTuning::default()
8752                        },
8753                    ),
8754                    (
8755                        "P3 L0=1".into(),
8756                        NvencTuning {
8757                            ref_l0: Some(NV_ENC_NUM_REF_FRAMES::NV_ENC_NUM_REF_FRAMES_1),
8758                            ..NvencTuning::default()
8759                        },
8760                    ),
8761                ];
8762                if codec != Codec::H264 {
8763                    for (name, split) in [
8764                        ("split off", Split::NV_ENC_SPLIT_DISABLE_MODE),
8765                        ("split driver's", Split::NV_ENC_SPLIT_AUTO_MODE),
8766                        ("split forced", Split::NV_ENC_SPLIT_AUTO_FORCED_MODE),
8767                        ("split two", Split::NV_ENC_SPLIT_TWO_FORCED_MODE),
8768                        ("split three", Split::NV_ENC_SPLIT_THREE_FORCED_MODE),
8769                    ] {
8770                        rows.push((
8771                            format!("P3 {name}"),
8772                            NvencTuning {
8773                                split: Some(split),
8774                                ..NvencTuning::default()
8775                            },
8776                        ));
8777                    }
8778                }
8779                for (scene, scene_name) in [(0usize, "text"), (1, "pan")] {
8780                    for (label, tuning) in &rows {
8781                        let mut s = settings(w as i32, h as i32, 60.0);
8782                        s.codec = codec;
8783                        s.video_cbr_mode = true;
8784                        s.video_bitrate_kbps = kbps;
8785                        let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), *tuning) {
8786                            Ok(enc) => enc,
8787                            Err(e) => {
8788                                println!("{codec:?} {w}x{h} {label}: {e}");
8789                                continue;
8790                            }
8791                        };
8792                        let engines = unsafe {
8793                            query_cap(
8794                                &enc.nvenc_funcs,
8795                                enc.encoder_session,
8796                                codec_guid(codec).unwrap(),
8797                                NV_ENC_CAPS::NV_ENC_CAPS_NUM_ENCODER_ENGINES,
8798                            )
8799                        };
8800                        let mut dec = VideoDecoder::new(codec).unwrap();
8801                        let (mut times, mut bytes, mut psnr) =
8802                            (Vec::with_capacity(n), 0usize, 0f64);
8803                        for t in 0..n + 5 {
8804                            let k = t % 2;
8805                            draw(scene, t, &mut bufs[k]);
8806                            let t0 = std::time::Instant::now();
8807                            let out = enc
8808                                .encode_cpu_packed(&bufs[k], w * 4, false, t as u64, 25, t == 0)
8809                                .expect("encode");
8810                            let ms = t0.elapsed().as_secs_f64() * 1e3;
8811                            let p = luma_psnr(&mut dec, &out, &bufs[k], w, h);
8812                            if t >= 5 {
8813                                times.push(ms);
8814                                bytes += out.len();
8815                                psnr += p;
8816                            }
8817                        }
8818                        times.sort_by(|a, b| a.total_cmp(b));
8819                        println!(
8820                            "TUNING {codec:?} {w}x{h} {scene_name} {label} (engines {engines:?}): encode {:.2} ms p50 {:.2} p99, {:.2} Mbit/s against {:.1}, PSNR {:.2} dB",
8821                            times[n / 2],
8822                            times[(n * 99) / 100],
8823                            bytes as f64 * 8.0 * 60.0 / n as f64 / 1e6,
8824                            kbps as f64 / 1000.0,
8825                            psnr / n as f64
8826                        );
8827                    }
8828                }
8829            }
8830        }
8831    }
8832
8833    /// On a real GPU with a render node: per-frame wall and CPU cost of the dmabuf path with the
8834    /// in-place registration (when the driver maps the import pitch-linear) against the per-frame
8835    /// copy, 1080p, two painted buffers alternating. Prints both; ignored by default.
8836    #[test]
8837    #[ignore]
8838    fn gpu_bench_dmabuf_paths() {
8839        let (w, h) = (1920u32, 1080u32);
8840        let n: usize = std::env::var("NVENC_BENCH_FRAMES")
8841            .ok()
8842            .and_then(|v| v.parse().ok())
8843            .unwrap_or(300);
8844        let s = settings(w as i32, h as i32, 60.0);
8845        let (gbm, mut renderer) = gpu_render();
8846        let egl_display = renderer.egl_context().display().get_display_handle().handle;
8847        let bufs: Vec<_> = (1..=2u32)
8848            .map(|seed| painted_dmabuf(&gbm, &mut renderer, w, h, seed).1)
8849            .collect();
8850        let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
8851        for pass in 0..2 {
8852            enc.direct_dmabuf = pass == 0;
8853            enc.reconfigure_resolution(&s)
8854                .expect("reconfigure drains the import cache");
8855            enc.encode(&bufs[0], 0, 25, true).expect("warm-up");
8856            enc.encode(&bufs[1], 1, 25, false).expect("warm-up");
8857            let label = if all_direct(&enc) {
8858                format!("dmabuf registered in place ({})", mapped_kind(&enc))
8859            } else if pass == 0 {
8860                format!(
8861                    "dmabuf copy arm (direct registration unavailable, mapped {})",
8862                    mapped_kind(&enc)
8863                )
8864            } else {
8865                format!("dmabuf per-frame copy (mapped {})", mapped_kind(&enc))
8866            };
8867            per_frame(&label, n, |i| {
8868                enc.encode(&bufs[i % 2], 2 + i as u64, 25, false)
8869                    .expect("encode");
8870            });
8871        }
8872    }
8873
8874    /// On a real GPU: per-frame wall and CPU cost of the readback→NVENC hand-over, 1080p host
8875    /// frames: the packed upload with the hardware CSC (BGRA and RGBA), and that same packed
8876    /// upload as a synchronous copy. Prints all; ignored by default.
8877    #[test]
8878    #[ignore]
8879    fn gpu_bench_readback_upload() {
8880        let (w, h) = (1920u32, 1080u32);
8881        let n: usize = std::env::var("NVENC_BENCH_FRAMES")
8882            .ok()
8883            .and_then(|v| v.parse().ok())
8884            .unwrap_or(300);
8885        let s = settings(w as i32, h as i32, 60.0);
8886        let frames: Vec<Vec<u8>> = (0..4u8)
8887            .map(|k| frame(w as usize, h as usize, 10 + 40 * k))
8888            .collect();
8889        let stride = (w * 4) as usize;
8890        let mut enc = host_session(&s).expect("NVENC init");
8891
8892        enc.reconfigure_resolution(&s).expect("reconfigure");
8893        enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true)
8894            .expect("warm-up");
8895        per_frame(
8896            "encode_cpu_packed BGRA (pinned, async upload, hardware CSC)",
8897            n,
8898            |i| {
8899                enc.encode_cpu_packed(&frames[i % 4], stride, false, 1 + i as u64, 25, false)
8900                    .expect("packed");
8901            },
8902        );
8903
8904        enc.reconfigure_resolution(&s).expect("reconfigure");
8905        enc.encode_cpu_packed(&frames[0], stride, true, 0, 25, true)
8906            .expect("warm-up");
8907        per_frame(
8908            "encode_cpu_packed RGBA (pinned, async upload, hardware CSC)",
8909            n,
8910            |i| {
8911                enc.encode_cpu_packed(&frames[i % 4], stride, true, 1 + i as u64, 25, false)
8912                    .expect("packed");
8913            },
8914        );
8915
8916        enc.reconfigure_resolution(&s).expect("reconfigure");
8917        enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true)
8918            .expect("warm-up");
8919        per_frame(
8920            "packed BGRA with a synchronous cuMemcpy2D upload",
8921            n,
8922            |i| unsafe {
8923                let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
8924                let src = &frames[i % 4];
8925                enc.pin_host_source(src.as_ptr() as usize, src.len());
8926                let copy = CUDA_MEMCPY2D {
8927                    srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
8928                    srcHost: src.as_ptr() as *const c_void,
8929                    srcPitch: stride,
8930                    dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
8931                    dstDevice: enc.input_device_ptr,
8932                    dstPitch: enc.input_pitch,
8933                    WidthInBytes: stride,
8934                    Height: h as usize,
8935                    ..Default::default()
8936                };
8937                assert_eq!((enc.cuda.cuMemcpy2D_v2)(&copy), CUresult::CUDA_SUCCESS);
8938                enc.submit_frame(
8939                    enc.mapped_input_buffer,
8940                    enc.input_format,
8941                    1 + i as u64,
8942                    false,
8943                )
8944                .expect("submit");
8945                (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
8946            },
8947        );
8948    }
8949
8950    /// How a bitstream lock waits for the encode: blocking in the driver, spinning on
8951    /// `doNotWait` locks, or sleeping between them.
8952    #[derive(Clone, Copy, Debug)]
8953    enum LockWait {
8954        Block,
8955        Spin,
8956        Sleep(std::time::Duration),
8957    }
8958
8959    /// On a real GPU, the time from `nvEncEncodePicture` to a locked bitstream at 1080p under
8960    /// each lock strategy, with the CPU each spends and how many not-ready answers the driver
8961    /// gives: the spin marks when the encode really completes, the blocking lock shows what the
8962    /// driver's own wait adds, and each sleep interval what its quantization adds. Ignored by
8963    /// default.
8964    #[test]
8965    #[ignore]
8966    fn gpu_bench_lock_strategies() {
8967        let s = settings(1920, 1080, 60.0);
8968        let mut enc = host_session(&s).expect("NVENC init");
8969        let frames = [frame(1920, 1080, 10), frame(1920, 1080, 90)];
8970        let n = 240usize;
8971        let strategies = [
8972            LockWait::Spin,
8973            LockWait::Block,
8974            LockWait::Sleep(std::time::Duration::from_micros(50)),
8975            LockWait::Sleep(std::time::Duration::from_micros(200)),
8976            LockWait::Sleep(std::time::Duration::from_micros(1000)),
8977        ];
8978        for pass in 0..2 {
8979            for &strategy in &strategies {
8980                let mut waits: Vec<f64> = Vec::with_capacity(n);
8981                let mut busy_total = 0u64;
8982                let c0 = thread_cpu();
8983                for i in 0..n {
8984                    let (wait, busy) =
8985                        unsafe { lock_round_trip(&mut enc, &frames[i % 2], i as u64, strategy) };
8986                    waits.push(wait);
8987                    busy_total += busy;
8988                }
8989                let cpu_us = (thread_cpu() - c0).as_secs_f64() * 1e6 / n as f64;
8990                waits.sort_by(|a, b| a.partial_cmp(b).unwrap());
8991                let mean = waits.iter().sum::<f64>() / n as f64;
8992                let pct = |q: f64| waits[((n as f64 - 1.0) * q) as usize];
8993                let empty = EMPTY_SUCCESSES.swap(0, std::sync::atomic::Ordering::Relaxed);
8994                if pass == 1 {
8995                    println!(
8996                        "{strategy:?}: submit->locked mean {mean:.0} us p50 {:.0} p95 {:.0} p99 {:.0} max {:.0}; {cpu_us:.0} us cpu/frame; {:.1} not-ready answers/frame; {:.2} empty successes/frame",
8997                        pct(0.5),
8998                        pct(0.95),
8999                        pct(0.99),
9000                        waits[n - 1],
9001                        busy_total as f64 / n as f64,
9002                        empty as f64 / n as f64
9003                    );
9004                }
9005            }
9006        }
9007    }
9008
9009    /// Successful `doNotWait` locks that came back with an empty bitstream, across the bench.
9010    static EMPTY_SUCCESSES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
9011
9012    /// Test helper: upload one frame synchronously, submit it, wait for its bitstream with
9013    /// `strategy`, and unlock it; the wait in microseconds and the not-ready answers seen.
9014    unsafe fn lock_round_trip(
9015        enc: &mut NvencEncoder,
9016        pixels: &[u8],
9017        frame_number: u64,
9018        strategy: LockWait,
9019    ) -> (f64, u64) {
9020        let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
9021        let copy = CUDA_MEMCPY2D {
9022            srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
9023            srcHost: pixels.as_ptr() as *const c_void,
9024            srcPitch: (enc.width * 4) as usize,
9025            dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
9026            dstDevice: enc.input_device_ptr,
9027            dstPitch: enc.input_pitch,
9028            WidthInBytes: (enc.width * 4) as usize,
9029            Height: enc.height as usize,
9030            ..Default::default()
9031        };
9032        assert_eq!((enc.cuda.cuMemcpy2D_v2)(&copy), CUresult::CUDA_SUCCESS);
9033        let output_bitstream = enc.bitstream_buffers[enc.current_buffer_idx];
9034        enc.current_buffer_idx = (enc.current_buffer_idx + 1) % enc.bitstream_buffers.len();
9035        let mut pic_params = NV_ENC_PIC_PARAMS {
9036            version: sv(NvStruct::PicParams),
9037            inputWidth: enc.width,
9038            inputHeight: enc.height,
9039            inputBuffer: enc.mapped_input_buffer,
9040            outputBitstream: output_bitstream,
9041            bufferFmt: enc.input_format,
9042            pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
9043            encodePicFlags: if frame_number == 0 {
9044                NV_ENC_PIC_FLAGS::NV_ENC_PIC_FLAG_FORCEIDR as u32
9045            } else {
9046                0
9047            },
9048            ..Default::default()
9049        };
9050        let t0 = std::time::Instant::now();
9051        let st =
9052            (enc.nvenc_funcs.nvEncEncodePicture.unwrap())(enc.encoder_session, &mut pic_params);
9053        assert_eq!(st, NVENCSTATUS::NV_ENC_SUCCESS);
9054        let mut negotiated = Negotiated::new(NV_ENC_LOCK_BITSTREAM {
9055            version: sv(NvStruct::LockBitstream),
9056            outputBitstream: output_bitstream,
9057            ..Default::default()
9058        });
9059        let lock_params = &mut negotiated.value;
9060        lock_params.set_doNotWait(
9061            matches!(strategy, LockWait::Block)
9062                .then_some(0)
9063                .unwrap_or(1),
9064        );
9065        let lock_fn = enc.nvenc_funcs.nvEncLockBitstream.unwrap();
9066        let mut busy = 0u64;
9067        let mut empty_successes = 0u64;
9068        loop {
9069            match lock_fn(enc.encoder_session, lock_params) {
9070                NVENCSTATUS::NV_ENC_SUCCESS if lock_params.bitstreamSizeInBytes > 0 => break,
9071                NVENCSTATUS::NV_ENC_SUCCESS => {
9072                    empty_successes += 1;
9073                    (enc.nvenc_funcs.nvEncUnlockBitstream.unwrap())(
9074                        enc.encoder_session,
9075                        output_bitstream,
9076                    );
9077                    if let LockWait::Sleep(d) = strategy {
9078                        std::thread::sleep(d);
9079                    }
9080                    if matches!(strategy, LockWait::Block) {
9081                        panic!("blocking lock returned an empty bitstream");
9082                    }
9083                }
9084                NVENCSTATUS::NV_ENC_ERR_LOCK_BUSY => {
9085                    busy += 1;
9086                    if let LockWait::Sleep(d) = strategy {
9087                        std::thread::sleep(d);
9088                    }
9089                }
9090                other => panic!("lock failed: {other:?}"),
9091            }
9092        }
9093        let wait = t0.elapsed().as_secs_f64() * 1e6;
9094        if empty_successes > 0 {
9095            EMPTY_SUCCESSES.fetch_add(empty_successes, std::sync::atomic::Ordering::Relaxed);
9096        }
9097        (enc.nvenc_funcs.nvEncUnlockBitstream.unwrap())(enc.encoder_session, output_bitstream);
9098        (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
9099        (wait, busy)
9100    }
9101}
9102
9103#[cfg(test)]
9104mod version_tests {
9105    use super::*;
9106
9107    /// Every version-tagged struct, in one fixed order, paired with its pinned compile-time
9108    /// `NV_ENC_*_VER` constant — the reference both version tests iterate.
9109    const ALL: [(NvStruct, u32); 13] = [
9110        (NvStruct::FunctionList, NV_ENCODE_API_FUNCTION_LIST_VER),
9111        (
9112            NvStruct::OpenSessionExParams,
9113            NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER,
9114        ),
9115        (NvStruct::Config, NV_ENC_CONFIG_VER),
9116        (NvStruct::RcParams, NV_ENC_RC_PARAMS_VER),
9117        (NvStruct::PresetConfig, NV_ENC_PRESET_CONFIG_VER),
9118        (NvStruct::InitializeParams, NV_ENC_INITIALIZE_PARAMS_VER),
9119        (NvStruct::ReconfigureParams, NV_ENC_RECONFIGURE_PARAMS_VER),
9120        (NvStruct::RegisterResource, NV_ENC_REGISTER_RESOURCE_VER),
9121        (NvStruct::MapInputResource, NV_ENC_MAP_INPUT_RESOURCE_VER),
9122        (
9123            NvStruct::CreateBitstreamBuffer,
9124            NV_ENC_CREATE_BITSTREAM_BUFFER_VER,
9125        ),
9126        (NvStruct::PicParams, NV_ENC_PIC_PARAMS_VER),
9127        (NvStruct::LockBitstream, NV_ENC_LOCK_BITSTREAM_VER),
9128        (NvStruct::CapsParam, NV_ENC_CAPS_PARAM_VER),
9129    ];
9130
9131    /// For the pinned nvcodec-sys version, `nvenc_struct_ver` must reproduce every
9132    /// compile-time `NV_ENC_*_VER` constant exactly — guaranteeing a current driver is stamped
9133    /// byte-for-byte identically — and the packed major/minor must round-trip `NVENCAPI_VERSION`.
9134    /// Fails loudly if the bundled header is bumped without extending the revision table.
9135    #[test]
9136    fn table_is_identity_for_pinned_version() {
9137        let maj = NVENCAPI_VERSION & 0xFF;
9138        let min = (NVENCAPI_VERSION >> 24) & 0xFF;
9139        for (s, base) in ALL {
9140            assert_eq!(nvenc_struct_ver(s, maj, min), base, "{:?}", s);
9141        }
9142        assert_eq!(maj | (min << 24), NVENCAPI_VERSION);
9143    }
9144
9145    /// A reconfigure's reset and IDR flags land where each version's own header puts them:
9146    /// behind initialize params of 1808 bytes below 12.2, and of 1800 from it.
9147    #[test]
9148    fn reconfigure_flags_sit_where_each_version_reads_them() {
9149        for (maj, min, at) in [
9150            (10, 0, 1816),
9151            (11, 0, 1816),
9152            (11, 1, 1816),
9153            (12, 0, 1816),
9154            (12, 1, 1816),
9155            (12, 2, 1808),
9156            (13, 0, 1808),
9157        ] {
9158            let params =
9159                reconfigure_params(NV_ENC_INITIALIZE_PARAMS::default(), (maj, min), true, true);
9160            let bytes = unsafe {
9161                std::slice::from_raw_parts(
9162                    (&params as *const Negotiated<_>).cast::<u8>(),
9163                    std::mem::size_of_val(&params),
9164                )
9165            };
9166            let word = |at: usize| u32::from_ne_bytes(bytes[at..at + 4].try_into().unwrap());
9167            assert_eq!(
9168                (word(1808), word(1816)),
9169                if at == 1816 { (0, 3) } else { (3, 0) },
9170                "{maj}.{min}"
9171            );
9172        }
9173    }
9174
9175    /// The structs handed to the driver run as long as the longest layout a negotiable version
9176    /// gives them: the initialize params 1808 bytes and the reconfigure params around them 1824
9177    /// in the SDK 10.0 to 12.1 headers, and the lock params 1552 in the 12.1 one.
9178    #[test]
9179    fn negotiated_structs_hold_the_longest_layout() {
9180        assert_eq!(
9181            std::mem::size_of::<Negotiated<NV_ENC_INITIALIZE_PARAMS>>(),
9182            1808
9183        );
9184        assert_eq!(
9185            std::mem::size_of::<Negotiated<NV_ENC_RECONFIGURE_PARAMS>>(),
9186            1824
9187        );
9188        assert_eq!(
9189            std::mem::size_of::<Negotiated<NV_ENC_LOCK_BITSTREAM>>(),
9190            1552
9191        );
9192    }
9193
9194    /// `nvenc_struct_ver` must reproduce the exact `NV_ENC_*_VER` words each SDK defined, for
9195    /// every negotiable version 10.0 through 13.0.
9196    ///
9197    /// The expected words are hardcoded from `nvEncodeAPI.h` at the FFmpeg nv-codec-headers tags
9198    /// listed in `NvStruct::rev`, one row per SDK version in `ALL` order. (The n10.0.26.2 header
9199    /// spells the flag `1<<31` rather than `1u<<31`; the bit is the same.) This is what lets the
9200    /// 13.0-layout structs be stamped with an older SDK's word when the session down-negotiates.
9201    #[test]
9202    fn table_matches_historical_headers() {
9203        #[rustfmt::skip]
9204        let expected: [(u32, u32, [u32; 12]); 7] = [
9205            (10, 0, [0x7002000A, 0x7001000A, 0xF007000A, 0x7001000A, 0xF004000A, 0xF005000A, 0xF001000A,
9206                     0x7003000A, 0x7004000A, 0x7001000A, 0xF004000A, 0x7001000A]),
9207            (11, 0, [0x7002000B, 0x7001000B, 0xF007000B, 0x7001000B, 0xF004000B, 0xF005000B, 0xF001000B,
9208                     0x7003000B, 0x7004000B, 0x7001000B, 0xF004000B, 0x7001000B]),
9209            (11, 1, [0x7102000B, 0x7101000B, 0xF107000B, 0x7101000B, 0xF104000B, 0xF105000B, 0xF101000B,
9210                     0x7103000B, 0x7104000B, 0x7101000B, 0xF104000B, 0x7101000B]),
9211            (12, 0, [0x7002000C, 0x7001000C, 0xF008000C, 0x7001000C, 0xF004000C, 0xF005000C, 0xF001000C,
9212                     0x7004000C, 0x7004000C, 0x7001000C, 0xF006000C, 0x7002000C]),
9213            (12, 1, [0x7102000C, 0x7101000C, 0xF108000C, 0x7101000C, 0xF104000C, 0xF106000C, 0xF101000C,
9214                     0x7104000C, 0x7104000C, 0x7101000C, 0xF106000C, 0xF101000C]),
9215            (12, 2, [0x7202000C, 0x7201000C, 0xF209000C, 0x7201000C, 0xF205000C, 0xF207000C, 0xF202000C,
9216                     0x7205000C, 0x7204000C, 0x7201000C, 0xF207000C, 0xF202000C]),
9217            (13, 0, [0x7002000D, 0x7001000D, 0xF009000D, 0x7001000D, 0xF005000D, 0xF007000D, 0xF002000D,
9218                     0x7005000D, 0x7004000D, 0x7001000D, 0xF007000D, 0xF002000D]),
9219        ];
9220        for (maj, min, words) in expected {
9221            for ((s, _), want) in ALL.iter().zip(words) {
9222                assert_eq!(
9223                    nvenc_struct_ver(*s, maj, min),
9224                    want,
9225                    "{:?} at {}.{}",
9226                    s,
9227                    maj,
9228                    min
9229                );
9230            }
9231        }
9232    }
9233}
9234
9235#[cfg(test)]
9236mod decision_tests {
9237    use super::*;
9238
9239    /// A caps query that returns `Some(0)` means the GPU lacks 4:4:4, so a 4:4:4 request is met
9240    /// with 4:2:0 and flagged as a downgrade; `Some(1)` keeps 4:4:4; an unqueryable cap (`None`)
9241    /// leaves the request untouched rather than downgrading on missing information.
9242    #[test]
9243    fn caps_chroma_downgrade() {
9244        let d = decide_caps(true, 1920, 1080, Some(0), None, None);
9245        assert!(!d.fullcolor && d.downgraded_color && d.too_large.is_none());
9246
9247        let d = decide_caps(true, 1920, 1080, Some(1), None, None);
9248        assert!(d.fullcolor && !d.downgraded_color);
9249
9250        let d = decide_caps(true, 1920, 1080, None, None, None);
9251        assert!(d.fullcolor && !d.downgraded_color);
9252
9253        // A 4:2:0 request is never a downgrade whatever the cap says.
9254        let d = decide_caps(false, 1920, 1080, Some(0), None, None);
9255        assert!(!d.fullcolor && !d.downgraded_color);
9256    }
9257
9258    /// A capture beyond the driver's reported maximum dimensions is flagged `too_large` (the
9259    /// caller then declines NVENC and uses software); a capture within them, or one whose caps are
9260    /// unknown, is not.
9261    #[test]
9262    fn caps_dimension_gate() {
9263        assert_eq!(
9264            decide_caps(false, 5120, 2160, None, Some(4096), Some(4096)).too_large,
9265            Some((4096, 4096))
9266        );
9267        assert_eq!(
9268            decide_caps(false, 3840, 4320, None, Some(4096), Some(4096)).too_large,
9269            Some((4096, 4096))
9270        );
9271        assert!(
9272            decide_caps(false, 3840, 2160, None, Some(4096), Some(4096))
9273                .too_large
9274                .is_none()
9275        );
9276        assert!(
9277            decide_caps(false, 7680, 4320, None, None, None)
9278                .too_large
9279                .is_none()
9280        );
9281        // A zero cap is treated as unknown, not as "everything is too large".
9282        assert!(
9283            decide_caps(false, 3840, 2160, None, Some(0), Some(0))
9284                .too_large
9285                .is_none()
9286        );
9287    }
9288
9289    /// The resize headroom lifts the request to the 5.2-ceiling floor but never past the driver
9290    /// maximum, so initializing with headroom cannot itself exceed what the GPU supports.
9291    #[test]
9292    fn headroom_is_floored_and_capped() {
9293        assert_eq!(nvenc_headroom(1920, 4096, Some(8192)), 4096);
9294        assert_eq!(nvenc_headroom(3840, 4096, Some(4096)), 4096);
9295        assert_eq!(nvenc_headroom(6000, 4096, Some(8192)), 6000);
9296        assert_eq!(nvenc_headroom(1920, 4096, None), 4096);
9297        assert_eq!(nvenc_headroom(1920, 4096, Some(2048)), 2048);
9298    }
9299
9300    /// H.264 and HEVC advertise the current geometry's level, so a decoder that gates on the
9301    /// level takes a 1080p stream a 4K-headroom session would once have marked 5.2; a resize
9302    /// re-declares it and a live rate change between the common rates does not. AV1 holds the
9303    /// headroom level, which NVENC requires it to cover at init and every AV1 decoder accepts.
9304    #[test]
9305    fn nvenc_level_follows_geometry_except_av1() {
9306        // The 1080p60 level, the one older Apple and Intel decoders gate on, is well below the
9307        // 4K-headroom level the session used to pin: H.264 4.2 and HEVC 4.1 rather than 5.2/5.1.
9308        assert_eq!(nvenc_level(Codec::H264, 1920, 1080, 60, 0, true), 42);
9309        assert_eq!(nvenc_level(Codec::H265, 1920, 1080, 60, 0, true), 123);
9310        // The current level rises with the picture, back to the headroom level at 4K.
9311        assert_eq!(nvenc_level(Codec::H264, 3840, 2160, 60, 0, true), 52);
9312        assert_eq!(nvenc_level(Codec::H265, 3840, 2160, 60, 0, true), 153);
9313        // A picture counts in whole coding tree blocks: 720p at 144 fps is 4.1 by its samples.
9314        assert_eq!(h265_level(1280, 720, 144, 0, true), 123);
9315        assert_eq!(nvenc_level(Codec::H265, 1280, 720, 144, 0, true), 150);
9316        // The current level admits the current picture on every codec.
9317        for (w, h) in [(1280u32, 720u32), (1920, 1080), (3840, 2160)] {
9318            let macroblocks = (w as u64 / 16) * (h as u64 / 16);
9319            assert!(
9320                h264_max_macroblocks(nvenc_level(Codec::H264, w, h, 60, 0, true)) >= macroblocks,
9321                "H.264 level at {w}x{h} cannot hold the picture"
9322            );
9323            assert!(
9324                h265_max_picture(nvenc_level(Codec::H265, w, h, 60, 0, true))
9325                    >= (w as u64) * (h as u64),
9326                "HEVC level at {w}x{h} cannot hold the picture"
9327            );
9328        }
9329        // AV1 holds the headroom level whatever the capture, since NVENC checks it against
9330        // maxEncodeWidth x maxEncodeHeight at init.
9331        let pixels = (HEADROOM_WIDTH * HEADROOM_HEIGHT) as u64;
9332        for (w, h) in [(1280, 720), (1920, 1080), (3840, 2160)] {
9333            assert_eq!(
9334                nvenc_level(Codec::Av1, w, h, 60, 0, true),
9335                nvenc_level(Codec::Av1, HEADROOM_WIDTH, HEADROOM_HEIGHT, 60, 0, true),
9336                "AV1 level moved with the capture at {w}x{h}"
9337            );
9338            assert!(
9339                av1_max_picture(nvenc_level(Codec::Av1, w, h, 60, 0, true)) >= pixels,
9340                "AV1 level at {w}x{h} cannot hold the headroom"
9341            );
9342        }
9343        // A live rate change between the common rates keeps the level, so it carries no IDR.
9344        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
9345            assert_eq!(
9346                nvenc_level(codec, 1920, 1080, 30, 0, true),
9347                nvenc_level(codec, 1920, 1080, 60, 0, true),
9348                "{codec:?} level moved between 30 and 60 fps"
9349            );
9350        }
9351        // A CBR target past the level's ceiling takes the first level that admits it: the
9352        // driver refuses the session otherwise, as an invalid level.
9353        assert_eq!(
9354            nvenc_level(Codec::H264, 1920, 1080, 60, 100_000_000, true),
9355            50
9356        );
9357        assert_eq!(
9358            nvenc_level(Codec::H265, 1920, 1080, 60, 60_000_000, true),
9359            150
9360        );
9361        assert_eq!(
9362            nvenc_level(Codec::H265, 1920, 1080, 60, 60_000_000, false),
9363            156
9364        );
9365        // An AV1 level holds its Annex A MaxBitrate, 5.1 40 Mbit/s, 5.2 60, 6.1 100, 6.2 160;
9366        // weighed by `NVENC_AV1_RATE`, two thirds of it: 5.1 26.7, 5.2 40, 6.1 66.7, 6.2 106.7.
9367        for (bps, annex_a, weighted) in [
9368            (26_666_666, 13, 13),
9369            (26_666_667, 13, 14),
9370            (40_000_000, 13, 14),
9371            (40_000_001, 14, 17),
9372            (45_000_000, 14, 17),
9373            (60_000_001, 17, 17),
9374            (66_666_667, 17, 18),
9375            (100_000_001, 18, 18),
9376            (106_666_666, 18, 18),
9377        ] {
9378            for (weigh, level) in [(false, annex_a), (true, weighted)] {
9379                assert_eq!(
9380                    nvenc_level(
9381                        Codec::Av1,
9382                        1920,
9383                        1080,
9384                        60,
9385                        level_rate(Codec::Av1, bps, weigh),
9386                        true
9387                    ),
9388                    level,
9389                    "AV1 at {bps} bit/s, weighed {weigh}"
9390                );
9391            }
9392        }
9393        assert_eq!(level_rate(Codec::H265, 60_000_000, true), 60_000_000);
9394    }
9395
9396    /// A frame splits across the engines for AV1 at any picture, in three strips on three, and
9397    /// for HEVC from 4K, never for H.264, on one engine, or before API 12.1, whose bits there are
9398    /// another flag's.
9399    #[test]
9400    fn split_mode_follows_the_codec_and_picture() {
9401        use NV_ENC_SPLIT_ENCODE_MODE::{
9402            NV_ENC_SPLIT_AUTO_FORCED_MODE as Forced, NV_ENC_SPLIT_AUTO_MODE as Driver,
9403            NV_ENC_SPLIT_THREE_FORCED_MODE as Three,
9404        };
9405        for (codec, w, h, engines, api, want) in [
9406            (Codec::Av1, 1280, 720, Some(2), (13, 0), Forced),
9407            (Codec::Av1, 3840, 2160, Some(3), (12, 1), Three),
9408            (Codec::Av1, 1920, 1080, Some(3), (13, 0), Three),
9409            (Codec::Av1, 1920, 1080, Some(4), (13, 0), Forced),
9410            (Codec::Av1, 1920, 1080, Some(3), (12, 0), Driver),
9411            (Codec::H265, 3840, 2160, Some(3), (13, 0), Forced),
9412            (Codec::H265, 1920, 1080, Some(3), (13, 0), Driver),
9413            (Codec::Av1, 1920, 1080, Some(1), (13, 0), Driver),
9414            (Codec::Av1, 1920, 1080, None, (13, 0), Driver),
9415            (Codec::H265, 2560, 1440, Some(2), (13, 0), Driver),
9416            (Codec::H265, 3840, 2160, Some(2), (13, 0), Forced),
9417            (Codec::H265, 2160, 3840, Some(2), (13, 0), Forced),
9418            (Codec::H265, 3840, 2160, Some(2), (12, 0), Driver),
9419            (Codec::H264, 3840, 2160, Some(2), (13, 0), Driver),
9420        ] {
9421            assert_eq!(
9422                split_mode(codec, w, h, engines, api),
9423                want,
9424                "{codec:?} {w}x{h} on {engines:?} engines, API {api:?}"
9425            );
9426        }
9427    }
9428
9429    /// A CBR target past every level's ceiling is held to the top one, which the driver opens,
9430    /// and one inside it passes as asked.
9431    #[test]
9432    fn cbr_targets_hold_to_the_top_level() {
9433        for (codec, kbps, bps) in [
9434            (Codec::Av1, 8_000, 8_000_000),
9435            (Codec::Av1, 106_666, 106_666_000),
9436            (Codec::Av1, 200_000, 106_666_666),
9437            (Codec::H265, 800_000, 800_000_000),
9438            (Codec::H265, 1_000_000, 800_000_000),
9439            (Codec::H264, 1_000_000, 1_000_000_000),
9440            (Codec::H264, 5_000_000, 1_000_000_000),
9441        ] {
9442            let s = RustCaptureSettings {
9443                codec,
9444                video_bitrate_kbps: kbps,
9445                ..Default::default()
9446            };
9447            assert_eq!(cbr_bps(&s), bps, "{codec:?} at {kbps} kbps");
9448        }
9449        assert_eq!(
9450            nvenc_level(
9451                Codec::Av1,
9452                1920,
9453                1080,
9454                240,
9455                nvenc_rate_ceiling(Codec::Av1) as u64,
9456                true
9457            ),
9458            18
9459        );
9460    }
9461
9462    /// AV1 Annex A MaxPicSize for a seq_level_idx the ladder can return.
9463    fn av1_max_picture(level: u32) -> u64 {
9464        match level {
9465            8 | 9 => 2_359_296,
9466            12..=15 => 8_912_896,
9467            _ => 35_651_584,
9468        }
9469    }
9470
9471    /// HEVC Annex A MaxLumaPs for a general_level_idc the ladder can return.
9472    fn h265_max_picture(level: u32) -> u64 {
9473        match level {
9474            123 => 2_228_224,
9475            150 | 153 | 156 => 8_912_896,
9476            _ => 35_651_584,
9477        }
9478    }
9479
9480    /// H.264 Annex A MaxFS for a level_idc the ladder can return.
9481    fn h264_max_macroblocks(level: u32) -> u64 {
9482        match level {
9483            41 => 8192,
9484            42 => 8704,
9485            50 => 22080,
9486            51 | 52 => 36864,
9487            _ => 139264,
9488        }
9489    }
9490
9491    /// Two buffers that reuse one fd number but differ in any identity field are distinct, so a
9492    /// cache keyed by fd cannot return a stale import: a new inode (the kernel reissues one per
9493    /// dma-buf since 5.3), a new size, or new geometry each breaks the match.
9494    #[test]
9495    fn dmabuf_identity_distinguishes_recycled_fd() {
9496        let base = DmaBufIdentity {
9497            dev: 1,
9498            ino: 10,
9499            size: 100,
9500            modifier: 0,
9501            width: 1920,
9502            height: 1080,
9503        };
9504        assert_eq!(base, base);
9505        let mut new_ino = base;
9506        new_ino.ino = 11;
9507        assert_ne!(base, new_ino);
9508        let mut new_size = base;
9509        new_size.size = 200;
9510        assert_ne!(base, new_size);
9511        let mut new_mod = base;
9512        new_mod.modifier = 1;
9513        assert_ne!(base, new_mod);
9514        let mut new_geom = base;
9515        new_geom.width = 1280;
9516        assert_ne!(base, new_geom);
9517    }
9518
9519    /// `probe` reads a real fd deterministically and folds the modifier and geometry into the
9520    /// identity: the same fd and parameters yield equal identities, and changing the modifier or the
9521    /// geometry changes the identity even on the same fd.
9522    #[test]
9523    fn dmabuf_identity_probe_is_stable_and_parameterized() {
9524        use std::os::fd::AsRawFd;
9525        let f = std::fs::File::open("/dev/null").expect("open /dev/null");
9526        let fd = f.as_raw_fd();
9527        let a = DmaBufIdentity::probe(fd, 0x1234, 1920, 1080);
9528        assert_eq!(a, DmaBufIdentity::probe(fd, 0x1234, 1920, 1080));
9529        assert_ne!(a, DmaBufIdentity::probe(fd, 0x9999, 1920, 1080));
9530        assert_ne!(a, DmaBufIdentity::probe(fd, 0x1234, 1280, 720));
9531    }
9532
9533    /// Test helper: a mapped `CUeglFrame` of one plane with the given kind, geometry, and pitch,
9534    /// its first plane at `plane` (a device pointer for the pitch kind, an array handle for the
9535    /// array kind) in four 8-bit channels.
9536    fn egl_frame(frame_type: u32, w: u32, h: u32, pitch: u32, plane: usize) -> CUeglFrame {
9537        let mut f: CUeglFrame = unsafe { std::mem::zeroed() };
9538        f.frame_type = frame_type;
9539        f.plane_count = 1;
9540        f.width = w;
9541        f.height = h;
9542        f.pitch = pitch;
9543        f.num_channels = 4;
9544        f.cu_format = CU_AD_FORMAT_U8;
9545        f.frame.p_pitch = [plane as *mut c_void, ptr::null_mut(), ptr::null_mut()];
9546        f
9547    }
9548
9549    /// A pitch-linear mapping whose first plane covers the session geometry at a 4-byte-aligned
9550    /// pitch of at least `width * 4` is registered with NVENC in place as a device pointer at its
9551    /// own pitch; a null plane, a short or unaligned pitch, a frame smaller than the session, or a
9552    /// frame without planes each take the per-frame copy.
9553    #[test]
9554    fn pitch_linear_frame_direct_registration_rules() {
9555        let pitch_ok = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7680, 0x1000);
9556        assert_eq!(
9557            direct_plane(&pitch_ok, 1920, 1080),
9558            Some(DirectPlane::Pitch(7680))
9559        );
9560        let padded = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 8192, 0x1000);
9561        assert_eq!(
9562            direct_plane(&padded, 1920, 1080),
9563            Some(DirectPlane::Pitch(8192))
9564        );
9565        let larger = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 2048, 1200, 8192, 0x1000);
9566        assert_eq!(
9567            direct_plane(&larger, 1920, 1080),
9568            Some(DirectPlane::Pitch(8192))
9569        );
9570
9571        let null_plane = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7680, 0);
9572        assert_eq!(direct_plane(&null_plane, 1920, 1080), None);
9573        let short_pitch = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7676, 0x1000);
9574        assert_eq!(direct_plane(&short_pitch, 1920, 1080), None);
9575        let unaligned = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7682, 0x1000);
9576        assert_eq!(direct_plane(&unaligned, 1920, 1080), None);
9577        let smaller = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1280, 720, 7680, 0x1000);
9578        assert_eq!(direct_plane(&smaller, 1920, 1080), None);
9579        let mut no_planes = pitch_ok;
9580        no_planes.plane_count = 0;
9581        assert_eq!(direct_plane(&no_planes, 1920, 1080), None);
9582        assert_eq!(direct_plane(&pitch_ok, 0, 1080), None);
9583        let mut unknown_kind = pitch_ok;
9584        unknown_kind.frame_type = 7;
9585        assert_eq!(direct_plane(&unknown_kind, 1920, 1080), None);
9586    }
9587
9588    /// A CUDA-array mapping of four 8-bit channels covering the session geometry is registered in
9589    /// place as a CUDA array whose pitch word is the array's row width in bytes; an array of any
9590    /// other element layout, a null array, or one smaller than the session takes the copy.
9591    #[test]
9592    fn cuda_array_frame_direct_registration_rules() {
9593        let array = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1920, 1080, 0, 0x2000);
9594        assert_eq!(
9595            direct_plane(&array, 1920, 1080),
9596            Some(DirectPlane::Array(7680))
9597        );
9598        let wider = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 2048, 1080, 0, 0x2000);
9599        assert_eq!(
9600            direct_plane(&wider, 1920, 1080),
9601            Some(DirectPlane::Array(8192))
9602        );
9603
9604        let null_array = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1920, 1080, 0, 0);
9605        assert_eq!(direct_plane(&null_array, 1920, 1080), None);
9606        let mut one_channel = array;
9607        one_channel.num_channels = 1;
9608        assert_eq!(direct_plane(&one_channel, 1920, 1080), None);
9609        let mut wide_elements = array;
9610        wide_elements.cu_format = 3;
9611        assert_eq!(direct_plane(&wide_elements, 1920, 1080), None);
9612        let smaller = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1280, 720, 0, 0x2000);
9613        assert_eq!(direct_plane(&smaller, 1920, 1080), None);
9614    }
9615
9616    /// The dmabuf fourcc picks the NVENC packed format with the same byte order: XR24 / AR24 are
9617    /// B,G,R,A in memory and so NVENC `ARGB`; XB24 / AB24 are R,G,B,A and so `ABGR`; anything else
9618    /// has no packed 8-bit NVENC equivalent.
9619    #[test]
9620    fn fourcc_selects_nvenc_byte_order() {
9621        for code in [Fourcc::Argb8888, Fourcc::Xrgb8888] {
9622            assert_eq!(
9623                fourcc_nvenc_format(code),
9624                Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB)
9625            );
9626        }
9627        for code in [Fourcc::Abgr8888, Fourcc::Xbgr8888] {
9628            assert_eq!(
9629                fourcc_nvenc_format(code),
9630                Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR)
9631            );
9632        }
9633        for code in [
9634            Fourcc::Rgb565,
9635            Fourcc::Nv12,
9636            Fourcc::Argb2101010,
9637            Fourcc::Bgra8888,
9638            Fourcc::Rgba8888,
9639        ] {
9640            assert_eq!(fourcc_nvenc_format(code), None, "{code:?}");
9641        }
9642    }
9643
9644    /// The EGL import names X-formats by their alpha twin and leaves everything else alone.
9645    #[test]
9646    fn egl_import_names_alpha_twins() {
9647        assert_eq!(egl_import_fourcc(Fourcc::Xrgb8888), Fourcc::Argb8888);
9648        assert_eq!(egl_import_fourcc(Fourcc::Xbgr8888), Fourcc::Abgr8888);
9649        for code in [Fourcc::Argb8888, Fourcc::Abgr8888, Fourcc::Nv12] {
9650            assert_eq!(egl_import_fourcc(code), code);
9651        }
9652    }
9653}