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, VecDeque};
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::dmabuf_identity::DmaBufIdentity;
45use super::frame_rate::FrameRate;
46use super::reference::{ANCHORS, Invalidation, Reference, ReferenceReport, ReferenceWindow};
47use super::session::{PeakBucket, RateSettings};
48use super::sps::{WideFrameNum, h264_frame_num_range};
49use crate::CaptureSettings;
50use nvcodec_sys::cuda::*;
51use nvcodec_sys::*;
52
53/// Opaque CUDA module and kernel handles; the driver API's own types, which the committed
54/// bindings do not carry.
55type CUmodule = *mut c_void;
56type CUfunction = *mut c_void;
57
58/// The ARGB/ABGR → NV12 convert and the band hash, as PTX the driver JIT-compiles at session
59/// open. PTX is the portable form: `libcuda` compiles it for whatever GPU is present, so nothing
60/// beyond the driver NVENC already needs has to be installed, and `.version 3.1`/`.target sm_30`
61/// keeps every NVENC-capable GPU in range.
62const ARGB_TO_NV12_PTX: &[u8] = include_bytes!("argb_to_nv12.ptx");
63
64/// JIT the kernels' module into the current context; `None` where the driver refuses it.
65unsafe fn load_kernels(cuda: &CudaFunctions) -> Option<CUmodule> {
66    let mut ptx = ARGB_TO_NV12_PTX.to_vec();
67    ptx.push(0);
68    let mut module: CUmodule = ptr::null_mut();
69    ((cuda.cuModuleLoadData)(&mut module, ptx.as_ptr() as *const c_void) == CUresult::CUDA_SUCCESS)
70        .then_some(module)
71}
72
73/// EGL C-interop type aliases and the `EGL_*` attribute constants used to wrap a dmabuf as
74/// an `EGLImageKHR` for CUDA import.
75type EGLDisplay = *const c_void;
76type EGLImageKHR = *mut c_void;
77type EGLint = i32;
78type EGLenum = u32;
79type EGLBoolean = u32;
80
81const EGL_NO_IMAGE_KHR: EGLImageKHR = ptr::null_mut();
82const EGL_LINUX_DMA_BUF_EXT: u32 = 0x3270;
83const EGL_DMA_BUF_PLANE0_FD_EXT: EGLint = 0x3272;
84const EGL_DMA_BUF_PLANE0_OFFSET_EXT: EGLint = 0x3273;
85const EGL_DMA_BUF_PLANE0_PITCH_EXT: EGLint = 0x3274;
86const EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT: EGLint = 0x3443;
87const EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT: EGLint = 0x3444;
88const EGL_WIDTH: EGLint = 0x3057;
89const EGL_HEIGHT: EGLint = 0x3056;
90const EGL_LINUX_DRM_FOURCC_EXT: EGLint = 0x3271;
91const EGL_NONE: EGLint = 0x3038;
92
93/// Opaque CUDA graphics-resource handle for the EGL interop path — an `EGLImageKHR`
94/// registered with CUDA maps to one of these.
95type CUgraphicsResource = *mut c_void;
96
97/// `CUeglFrame::frame_type` values: the mapped planes are CUDA arrays, or pitch-linear device
98/// memory.
99const CU_EGL_FRAME_TYPE_ARRAY: u32 = 0;
100const CU_EGL_FRAME_TYPE_PITCH: u32 = 1;
101/// `CUeglFrame::cu_format` of an 8-bit-per-channel plane (`CU_AD_FORMAT_UNSIGNED_INT8`).
102const CU_AD_FORMAT_U8: u32 = 1;
103
104/// `CU_TRSF_READ_AS_INTEGER`, a `cuda.h` macro the bindings do not carry: a texture fetch
105/// returns the stored bytes rather than normalized floats.
106const CU_TRSF_READ_AS_INTEGER: u32 = 1;
107
108/// A CUDA frame mapped from an EGLImage: the `cuGraphicsResourceGetMappedEglFrame` result
109/// describing the imported dmabuf's plane pointers, geometry, pitch, and pixel format.
110#[repr(C)]
111#[derive(Clone, Copy)]
112struct CUeglFrame {
113    frame: CUeglFrameUnion,
114    width: u32,
115    height: u32,
116    depth: u32,
117    pitch: u32,
118    plane_count: u32,
119    num_channels: u32,
120    frame_type: u32,
121    egl_color_format: u32,
122    cu_format: u32,
123}
124
125/// The mapped frame's plane pointers, as either CUDA arrays or pitch-linear device
126/// pointers — `CUeglFrame::frame_type` selects which arm of the union is valid.
127#[repr(C)]
128#[derive(Clone, Copy)]
129union CUeglFrameUnion {
130    p_array: [CUarray; 3],
131    p_pitch: [*mut c_void; 3],
132}
133
134type EglCreateImageKhrFn = unsafe extern "C" fn(
135    dpy: EGLDisplay,
136    ctx: *mut c_void,
137    target: EGLenum,
138    buffer: *mut c_void,
139    attrib_list: *const EGLint,
140) -> EGLImageKHR;
141type EglDestroyImageKhrFn = unsafe extern "C" fn(dpy: EGLDisplay, image: EGLImageKHR) -> EGLBoolean;
142
143/// Dynamically loaded EGL entry points (from `libEGL`) for creating and destroying the
144/// `EGLImageKHR` that wraps a dmabuf. `_lib` keeps the library resident for the pointers' life.
145struct EglFunctions {
146    _lib: Library,
147    eglGetProcAddress: unsafe extern "C" fn(procname: *const c_char) -> *mut c_void,
148    eglCreateImageKHR: EglCreateImageKhrFn,
149    eglDestroyImageKHR: EglDestroyImageKhrFn,
150}
151
152/// Dynamically loaded CUDA driver-API entry points (from `libcuda`) for context, device,
153/// memory, host-pin, and EGL-interop calls. `_lib` keeps the library resident for the pointers' life.
154struct CudaFunctions {
155    _lib: Library,
156    cuInit: unsafe extern "C" fn(flags: u32) -> CUresult,
157    cuDeviceGet: unsafe extern "C" fn(device: *mut CUdevice, ordinal: i32) -> CUresult,
158    cuDeviceGetByPCIBusId:
159        unsafe extern "C" fn(dev: *mut CUdevice, pciBusId: *const c_char) -> CUresult,
160    cuDevicePrimaryCtxRetain: unsafe extern "C" fn(pctx: *mut CUcontext, dev: CUdevice) -> CUresult,
161    cuCtxPushCurrent_v2: unsafe extern "C" fn(ctx: CUcontext) -> CUresult,
162    cuCtxPopCurrent_v2: unsafe extern "C" fn(pctx: *mut CUcontext) -> CUresult,
163    cuDevicePrimaryCtxRelease_v2: unsafe extern "C" fn(dev: CUdevice) -> CUresult,
164    cuMemAlloc_v2: unsafe extern "C" fn(dptr: *mut CUdeviceptr, bytesize: usize) -> CUresult,
165    cuMemAllocPitch_v2: unsafe extern "C" fn(
166        dptr: *mut CUdeviceptr,
167        pPitch: *mut usize,
168        WidthInBytes: usize,
169        Height: usize,
170        ElementSizeBytes: u32,
171    ) -> CUresult,
172    cuMemFree_v2: unsafe extern "C" fn(dptr: CUdeviceptr) -> CUresult,
173    cuMemcpyHtoD_v2: unsafe extern "C" fn(
174        dstDevice: CUdeviceptr,
175        srcHost: *const c_void,
176        ByteCount: usize,
177    ) -> CUresult,
178    cuMemcpyDtoH_v2: unsafe extern "C" fn(
179        dstHost: *mut c_void,
180        srcDevice: CUdeviceptr,
181        ByteCount: usize,
182    ) -> CUresult,
183    cuMemcpy2D_v2: unsafe extern "C" fn(pCopy: *const CUDA_MEMCPY2D) -> CUresult,
184    cuMemcpy2DAsync_v2:
185        unsafe extern "C" fn(pCopy: *const CUDA_MEMCPY2D, hStream: CUstream) -> CUresult,
186    cuStreamSynchronize: unsafe extern "C" fn(hStream: CUstream) -> CUresult,
187    cuModuleLoadData: unsafe extern "C" fn(module: *mut CUmodule, image: *const c_void) -> CUresult,
188    cuModuleGetFunction: unsafe extern "C" fn(
189        hfunc: *mut CUfunction,
190        hmod: CUmodule,
191        name: *const c_char,
192    ) -> CUresult,
193    cuModuleUnload: unsafe extern "C" fn(hmod: CUmodule) -> CUresult,
194    cuTexObjectCreate: unsafe extern "C" fn(
195        tex: *mut CUtexObject,
196        res: *const CUDA_RESOURCE_DESC,
197        sampling: *const CUDA_TEXTURE_DESC,
198        view: *const c_void,
199    ) -> CUresult,
200    cuTexObjectDestroy: unsafe extern "C" fn(tex: CUtexObject) -> CUresult,
201    #[allow(clippy::type_complexity)]
202    cuLaunchKernel: unsafe extern "C" fn(
203        f: CUfunction,
204        gx: u32,
205        gy: u32,
206        gz: u32,
207        bx: u32,
208        by: u32,
209        bz: u32,
210        shared: u32,
211        stream: CUstream,
212        params: *mut *mut c_void,
213        extra: *mut *mut c_void,
214    ) -> CUresult,
215    cuMemHostRegister_v2:
216        unsafe extern "C" fn(p: *mut c_void, bytesize: usize, flags: u32) -> CUresult,
217    cuMemHostUnregister: unsafe extern "C" fn(p: *mut c_void) -> CUresult,
218    cuGraphicsEGLRegisterImage: unsafe extern "C" fn(
219        pCudaResource: *mut CUgraphicsResource,
220        image: EGLImageKHR,
221        flags: u32,
222    ) -> CUresult,
223    cuGraphicsUnregisterResource: unsafe extern "C" fn(resource: CUgraphicsResource) -> CUresult,
224    cuGraphicsResourceGetMappedEglFrame: unsafe extern "C" fn(
225        pEglFrame: *mut CUeglFrame,
226        resource: CUgraphicsResource,
227        index: u32,
228        mipLevel: u32,
229    ) -> CUresult,
230    cuDeviceGetCount: unsafe extern "C" fn(count: *mut i32) -> CUresult,
231    cuDeviceGetName: unsafe extern "C" fn(name: *mut c_char, len: i32, dev: CUdevice) -> CUresult,
232    cuDeviceGetUuid: unsafe extern "C" fn(uuid: *mut CUuuid, dev: CUdevice) -> CUresult,
233    cuGetErrorName: unsafe extern "C" fn(error: CUresult, pStr: *mut *const c_char) -> CUresult,
234    /// External semaphores (CUDA 10), bound only where present: the X server's blit semaphore is
235    /// imported and waited on through them, and a driver without them keeps the GetImage fence.
236    cuImportExternalSemaphore: Option<
237        unsafe extern "C" fn(
238            out: *mut CUexternalSemaphore,
239            desc: *const CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC,
240        ) -> CUresult,
241    >,
242    cuWaitExternalSemaphoresAsync: Option<
243        unsafe extern "C" fn(
244            semaphores: *const CUexternalSemaphore,
245            params: *const CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS,
246            count: u32,
247            stream: CUstream,
248        ) -> CUresult,
249    >,
250    cuDestroyExternalSemaphore: Option<unsafe extern "C" fn(sem: CUexternalSemaphore) -> CUresult>,
251}
252
253/// Dynamically loaded NVENC entry points (from `libnvidia-encode`).
254///
255/// - **`create_instance`** (`NvEncodeAPICreateInstance`): fills an `NV_ENCODE_API_FUNCTION_LIST`
256///   with the driver's encode entry points for a requested API-version word.
257/// - **`get_max_version`** (`NvEncodeAPIGetMaxSupportedVersion`): the highest API version the
258///   driver supports, used to cap version probing. `Option` because very old drivers lack it, in
259///   which case probing relies on `create_instance` acceptance alone.
260///
261/// `_lib` keeps the library resident for the function pointers' life.
262struct NvencLibrary {
263    _lib: Library,
264    create_instance:
265        unsafe extern "C" fn(functionList: *mut NV_ENCODE_API_FUNCTION_LIST) -> NVENCSTATUS,
266    get_max_version: Option<unsafe extern "C" fn(*mut u32) -> NVENCSTATUS>,
267}
268
269/// Negotiated NVENC API version `(major, minor)`, resolved once per process. `None` until
270/// `nvenc_negotiate` runs; every struct-version word and the session `apiVersion` derive from it.
271static NVENC_NEG_VER: std::sync::OnceLock<(u32, u32)> = std::sync::OnceLock::new();
272
273/// The NVENCAPI structs this encoder must stamp with a per-SDK version word — enumerated
274/// here precisely because getting that word exactly right is the whole mechanism that lets one
275/// compiled binary satisfy every driver's version check.
276///
277/// Each NVENCAPI struct carries a `version` field that the driver validates against the exact word
278/// its own SDK defined for that struct, rejecting anything else outright with
279/// `NV_ENC_ERR_INVALID_VERSION`. Only two parts of that packed word move between SDKs — the struct
280/// **revision** (bits 16-23) and the **`1<<31` flag** — so a session that has down-negotiated to an
281/// older API cannot send the compiled 13.0 words; it must stamp each struct with precisely the word
282/// that older SDK defined, while a current driver still receives its own native word. Naming the
283/// structs here is what lets `NvStruct::rev` supply the per-version `(revision, flag)` and
284/// `nvenc_struct_ver` assemble the word.
285#[derive(Clone, Copy, Debug)]
286enum NvStruct {
287    FunctionList,
288    OpenSessionExParams,
289    Config,
290    RcParams,
291    PresetConfig,
292    InitializeParams,
293    ReconfigureParams,
294    RegisterResource,
295    MapInputResource,
296    CreateBitstreamBuffer,
297    PicParams,
298    LockBitstream,
299    CapsParam,
300}
301
302impl NvStruct {
303    /// The `(struct revision, 1<<31 flag)` this struct uses under the SDK identified by the
304    /// packed API version `api` (`(major<<4)|minor`) — the only two sub-fields that move between
305    /// SDKs, and thus the entire per-version knowledge stamping a struct actually needs.
306    ///
307    /// The revision lands in bits 16-23 of the version word and the flag in bit 31; every other bit
308    /// is fixed, which is exactly why matching just these two on `api` reproduces each SDK's word.
309    /// The values are transcribed verbatim from `nvEncodeAPI.h` at the FFmpeg nv-codec-headers tags
310    /// 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
311    /// are ground truth rather than anything derived that could drift. Structs whose layout is stable
312    /// across those SDKs return a constant pair; the rest match on `api`. 10.0 is the negotiation
313    /// floor, so the oldest match arm also covers anything below it.
314    fn rev(self, api: u32) -> (u32, bool) {
315        match self {
316            NvStruct::FunctionList => (2, false),
317            NvStruct::OpenSessionExParams => (1, false),
318            NvStruct::Config => match api {
319                0xC2.. => (9, true),
320                0xC0..=0xC1 => (8, true),
321                _ => (7, true),
322            },
323            NvStruct::RcParams => (1, false),
324            NvStruct::PresetConfig => (if api >= 0xC2 { 5 } else { 4 }, true),
325            NvStruct::InitializeParams => match api {
326                0xC2.. => (7, true),
327                0xC1 => (6, true),
328                _ => (5, true),
329            },
330            NvStruct::ReconfigureParams => (if api >= 0xC2 { 2 } else { 1 }, true),
331            NvStruct::RegisterResource => match api {
332                0xC2.. => (5, false),
333                0xC0..=0xC1 => (4, false),
334                _ => (3, false),
335            },
336            NvStruct::MapInputResource => (4, false),
337            NvStruct::CreateBitstreamBuffer => (1, false),
338            NvStruct::PicParams => match api {
339                0xC2.. => (7, true),
340                0xC0..=0xC1 => (6, true),
341                _ => (4, true),
342            },
343            NvStruct::LockBitstream => match api {
344                0xC2.. => (2, true),
345                0xC1 => (1, true),
346                0xC0 => (2, false),
347                _ => (1, false),
348            },
349            NvStruct::CapsParam => (1, false),
350        }
351    }
352}
353
354/// Assemble the `NVENCAPI_STRUCT_VERSION` word for struct `s` at API version `(maj, min)`.
355///
356/// The 32-bit word packs, from `NvStruct::rev` and the API version:
357///
358/// 1. **API major** in bits 0-7, **API minor** in bits 24-27.
359/// 2. **Struct revision** (`rev`) in bits 16-23.
360/// 3. **Magic `0x7`** in bits 28-30.
361/// 4. **The `1<<31` flag** in bit 31, when this struct sets it at this version.
362///
363/// For the pinned nvcodec-sys headers this reproduces the compile-time `NV_ENC_*_VER` constants
364/// exactly, so a current driver is stamped byte-for-byte identically to its own SDK's constant; the
365/// `version_tests` module asserts that identity.
366fn nvenc_struct_ver(s: NvStruct, maj: u32, min: u32) -> u32 {
367    let (rev, high_bit) = s.rev((maj << 4) | (min & 0xF));
368    (maj & 0xFF) | ((min & 0xF) << 24) | (rev << 16) | (0x7 << 28) | (u32::from(high_bit) << 31)
369}
370
371/// The process's effective NVENC API version `(major, minor)`: the negotiated value once
372/// `nvenc_negotiate` has run, otherwise the pinned `NVENCAPI_VERSION` decomposed (major in the low
373/// byte, minor at bit 24) as the pre-negotiation fallback.
374#[inline]
375fn nvenc_cur_ver() -> (u32, u32) {
376    NVENC_NEG_VER
377        .get()
378        .copied()
379        .unwrap_or((NVENCAPI_VERSION & 0xFF, (NVENCAPI_VERSION >> 24) & 0xFF))
380}
381
382/// The struct-version word for `s` tagged with the process's negotiated API version — the
383/// value every NVENCAPI struct literal assigns to its `version` field.
384#[inline]
385fn sv(s: NvStruct) -> u32 {
386    let (m, n) = nvenc_cur_ver();
387    nvenc_struct_ver(s, m, n)
388}
389
390/// The negotiated session `apiVersion` word (`major | minor<<24`) passed to
391/// `NvEncOpenEncodeSessionEx` — note the minor sits at bit 24 here, unlike the `(major<<4)|minor`
392/// packing that `NvStruct::rev` matches on.
393#[inline]
394fn neg_api() -> u32 {
395    let (m, n) = nvenc_cur_ver();
396    m | (n << 24)
397}
398
399/// A struct as long as the negotiated API's layout of it, which can run eight bytes past the
400/// pinned header's: `NV_ENC_INITIALIZE_PARAMS` below 12.2, and so the reconfigure params that
401/// embed it, and `NV_ENC_LOCK_BITSTREAM` at 12.1. The tail is zeroed and pixelflux's, so the
402/// driver reads and writes inside memory this owns.
403#[repr(C)]
404struct Negotiated<T> {
405    value: T,
406    tail: [u32; 2],
407}
408
409impl<T> Negotiated<T> {
410    fn new(value: T) -> Self {
411        Self {
412            value,
413            tail: [0; 2],
414        }
415    }
416}
417
418/// The reconfigure params re-initializing with `init` at API `(maj, min)`, resetting the encoder
419/// and forcing an IDR as asked; below 12.2 the driver reads the two flags from the tail.
420fn reconfigure_params(
421    init: &NV_ENC_INITIALIZE_PARAMS,
422    (maj, min): (u32, u32),
423    reset: bool,
424    force_idr: bool,
425) -> Negotiated<NV_ENC_RECONFIGURE_PARAMS> {
426    let mut p = Negotiated::new(NV_ENC_RECONFIGURE_PARAMS {
427        version: nvenc_struct_ver(NvStruct::ReconfigureParams, maj, min),
428        reInitEncodeParams: *init,
429        ..Default::default()
430    });
431    if (maj << 4) | min < 0xC2 {
432        p.tail[0] = u32::from(reset) | u32::from(force_idr) << 1;
433    } else {
434        p.value.set_resetEncoder(u32::from(reset));
435        p.value.set_forceIDR(u32::from(force_idr));
436    }
437    p
438}
439
440/// Resolve the process-wide NVENC API version once, by probing the driver newest-first and
441/// remembering the highest version it accepts.
442///
443/// The bundled nv-codec-headers are NVENC 13.0 (`pinned`), so a current driver negotiates 13.0
444/// natively while older drivers down-negotiate through 12.x / 11.x to the 10.0 floor (~R445). The
445/// compiled struct *layouts* are the 13.0 ones, handed to older drivers with the tail their own
446/// layouts run longer (`Negotiated`); only the version *words* change per negotiated version
447/// (via the `NvStruct::rev` table), so each struct is stamped with the exact word the negotiated
448/// SDK defined. Steps:
449///
450/// 1. **Cap the search** by the driver's max: query `get_max_version` when present, then optionally
451///    lower it further from `PIXELFLUX_NVENC_MAX_API` (e.g. `"11.0"`) for testing / pinning. A cap
452///    of 0 means unknown, and probing then relies on `create_instance` acceptance alone.
453/// 2. **Probe candidates** newest-first (`pinned`, 12.1, 12.0, 11.1, 11.0, 10.0), skipping any
454///    above the cap. Each probe stamps an `NV_ENCODE_API_FUNCTION_LIST` with that version's word and
455///    calls `create_instance`.
456/// 3. **Require the whole encode path**, not just a success code: the session opener plus
457///    `nvEncInitializeEncoder`, `nvEncGetEncodePresetConfigEx`, `nvEncEncodePicture`, and
458///    `nvEncLockBitstream` must all be non-null, because a driver can accept the function-list word
459///    yet leave newer entry points null. The first fully-populated version wins.
460/// 4. **Fall back** to `pinned` if nothing qualifies. Stored in `NVENC_NEG_VER`, set-once.
461fn nvenc_negotiate(lib: &NvencLibrary) {
462    NVENC_NEG_VER.get_or_init(|| {
463        let pinned = (NVENCAPI_VERSION & 0xFF, (NVENCAPI_VERSION >> 24) & 0xFF);
464        let mut drv_max: u32 = 0;
465        if let Some(get_max) = lib.get_max_version {
466            let mut m: u32 = 0;
467            if unsafe { get_max(&mut m) } == NVENCSTATUS::NV_ENC_SUCCESS {
468                drv_max = m;
469            }
470        }
471        if let Ok(cap) = std::env::var("PIXELFLUX_NVENC_MAX_API") {
472            let mut it = cap.split('.');
473            if let (Some(a), Some(b)) = (it.next(), it.next())
474                && let (Ok(cm), Ok(cn)) = (a.parse::<u32>(), b.parse::<u32>())
475            {
476                let capv = (cm << 4) | (cn & 0xF);
477                if capv != 0 && (drv_max == 0 || capv < drv_max) {
478                    drv_max = capv;
479                }
480            }
481        }
482        let candidates = [pinned, (12, 1), (12, 0), (11, 1), (11, 0), (10, 0)];
483        for (maj, min) in candidates {
484            let vv = (maj << 4) | min;
485            if drv_max != 0 && vv > drv_max {
486                continue;
487            }
488            let mut probe = NV_ENCODE_API_FUNCTION_LIST {
489                version: nvenc_struct_ver(NvStruct::FunctionList, maj, min),
490                ..Default::default()
491            };
492            let st = unsafe { (lib.create_instance)(&mut probe) };
493            if st == NVENCSTATUS::NV_ENC_SUCCESS
494                && probe.nvEncOpenEncodeSessionEx.is_some()
495                && probe.nvEncInitializeEncoder.is_some()
496                && probe.nvEncGetEncodePresetConfigEx.is_some()
497                && probe.nvEncEncodePicture.is_some()
498                && probe.nvEncLockBitstream.is_some()
499            {
500                crate::log::debug!("[pixelflux] NVENC API version negotiated: {}.{}", maj, min);
501                return (maj, min);
502            }
503        }
504        pinned
505    });
506}
507
508/// Cached CUDA import of a dmabuf, keyed by fd so a recurring capture buffer is imported
509/// once: the `EGLImageKHR`, the CUDA graphics resource it registers as, the mapped `CUeglFrame`,
510/// and how that frame reaches NVENC. Torn down on drop / reconfigure, or evicted when the fd it is
511/// keyed by no longer names the same buffer (see `DmaBufIdentity`).
512struct CachedDmaBuf {
513    identity: DmaBufIdentity,
514    /// The frame number that last encoded from the import, for evicting the stalest.
515    last_use: u64,
516    egl_image: EGLImageKHR,
517    cuda_resource: CUgraphicsResource,
518    egl_frame: CUeglFrame,
519    input: DmaBufInput,
520    /// The texture the chroma convert reads an array-typed import through, so its RGB is never
521    /// copied into linear memory. Zero where the import is linear or carries no convert.
522    tex: CUtexObject,
523}
524
525/// External input registrations an encoder keeps at once (`register_external_input`).
526pub(crate) const EXTERNAL_INPUTS: usize = 3;
527
528/// Dmabuf imports a session keeps at once: a capture's own targets and the swapchain of the
529/// client whose buffers it encodes in place come to a handful, and past this the one longest
530/// unused is released, so a client allocating buffers freely never grows the cache.
531const DMABUF_IMPORTS: usize = 16;
532
533/// An input surface the session does not own: a device pointer produced by another component on
534/// the same CUDA context — the NvFBC capture buffer — registered and mapped with NVENC in place
535/// so the encoder reads the captured frame where it was composited.
536///
537/// The registration is kept for as long as the geometry holds, because a capture source hands
538/// back the same buffer, or the same few in turn, every frame; it is released when the geometry
539/// changes, and always before the component that owns the memory tears it down.
540#[derive(Clone, Copy)]
541struct ExternalInput {
542    device_ptr: CUdeviceptr,
543    pitch: usize,
544    width: u32,
545    height: u32,
546    format: NV_ENC_BUFFER_FORMAT,
547    registered: NV_ENC_REGISTERED_PTR,
548    mapped: NV_ENC_INPUT_PTR,
549}
550
551/// How a cached dmabuf import feeds the encoder.
552///
553/// `Direct` is the zero-copy case: the mapped frame's first plane is itself registered and mapped
554/// as an NVENC input — as a pitch-linear device pointer or as a CUDA array, per `DirectPlane` — so
555/// encoding reads the capture buffer in place. `Copy` covers the rest: a plane `direct_plane`
556/// rules out, one the driver declined to register, or the direct path switched off; the plane is
557/// then copied into the session's packed input surface each frame.
558#[derive(Clone, Copy)]
559enum DmaBufInput {
560    Direct {
561        registered: NV_ENC_REGISTERED_PTR,
562        mapped: NV_ENC_INPUT_PTR,
563        format: NV_ENC_BUFFER_FORMAT,
564    },
565    Copy,
566}
567
568/// The NVENC packed input format whose byte order is a dmabuf's: the XR24 / AR24 family is
569/// B,G,R,A in memory (NVENC's word-ordered `ARGB`), the XB24 / AB24 family R,G,B,A (`ABGR`).
570/// `None` for any other fourcc — nothing NVENC reads as packed 8-bit RGB.
571/// The fourcc a dmabuf is described with for CUDA's EGL import: the alpha-carrying twin of an
572/// X-format, since the NVIDIA driver refuses XRGB/XBGR images there while the bytes lie identically
573/// and NVENC ignores the alpha channel of ARGB/ABGR input.
574fn egl_import_fourcc(code: Fourcc) -> Fourcc {
575    match code {
576        Fourcc::Xrgb8888 => Fourcc::Argb8888,
577        Fourcc::Xbgr8888 => Fourcc::Abgr8888,
578        other => other,
579    }
580}
581
582fn fourcc_nvenc_format(code: Fourcc) -> Option<NV_ENC_BUFFER_FORMAT> {
583    match code {
584        Fourcc::Argb8888 | Fourcc::Xrgb8888 => {
585            Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB)
586        }
587        Fourcc::Abgr8888 | Fourcc::Xbgr8888 => {
588            Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR)
589        }
590        _ => None,
591    }
592}
593
594/// How the first plane of a mapped `CUeglFrame` registers with NVENC in place: the
595/// `NV_ENC_REGISTER_RESOURCE` resource type and the `pitch` word that type expects.
596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
597enum DirectPlane {
598    /// Pitch-linear device memory, registered as a CUDA device pointer at this row pitch.
599    Pitch(u32),
600    /// A two-dimensional CUDA array of four 8-bit channels, registered as a CUDA array; the
601    /// value is the array's row width in bytes (`Width × NumChannels`), which is what NVENC
602    /// takes as the pitch of an array resource.
603    Array(u32),
604}
605
606/// Whether NVENC can read a mapped `CUeglFrame` in place, and how.
607///
608/// Either frame kind has to be usable as the session's `width × height` input: a first plane
609/// present and non-null, and a geometry of at least the session's. A pitch-linear plane also needs
610/// a row pitch covering `width * 4` bytes at the 4-byte alignment `NV_ENC_REGISTER_RESOURCE`
611/// requires; a CUDA-array plane has to be four 8-bit channels, the layout NVENC's packed formats
612/// describe. `None` sends the frame down the per-frame copy into the session's own input surface.
613fn direct_plane(frame: &CUeglFrame, width: u32, height: u32) -> Option<DirectPlane> {
614    if frame.plane_count < 1 || width == 0 || height == 0 {
615        return None;
616    }
617    if frame.width < width || frame.height < height {
618        return None;
619    }
620    match frame.frame_type {
621        CU_EGL_FRAME_TYPE_PITCH => {
622            let plane = unsafe { frame.frame.p_pitch[0] };
623            let pitch_ok = frame.pitch >= width.saturating_mul(4) && frame.pitch.is_multiple_of(4);
624            (!plane.is_null() && pitch_ok).then_some(DirectPlane::Pitch(frame.pitch))
625        }
626        CU_EGL_FRAME_TYPE_ARRAY => {
627            let array = unsafe { frame.frame.p_array[0] };
628            let packed_8bit = frame.cu_format == CU_AD_FORMAT_U8 && frame.num_channels == 4;
629            (!array.is_null() && packed_8bit).then_some(DirectPlane::Array(frame.width * 4))
630        }
631        _ => None,
632    }
633}
634
635/// The chroma format and dimensional feasibility a session settles on given the driver's
636/// reported capabilities, so init degrades cleanly instead of failing opaquely.
637///
638/// - `fullcolor` is the chroma actually used: 4:4:4 only when it was requested and the GPU carries
639///   it, otherwise 4:2:0.
640/// - `downgraded_color` records that a 4:4:4 request was met with 4:2:0, so the caller says so once.
641/// - `too_large` carries the driver's `(max_w, max_h)` when the requested geometry exceeds it; the
642///   caller then declines NVENC and falls back to software rather than failing to initialize.
643///
644/// A capability that could not be queried is `None` and does not gate: 4:4:4 stays as requested and
645/// the dimension test is skipped, so an unavailable answer never forces a false downgrade or refusal.
646#[derive(Debug, Clone, Copy, PartialEq, Eq)]
647struct CapsDecision {
648    fullcolor: bool,
649    downgraded_color: bool,
650    too_large: Option<(i32, i32)>,
651}
652
653/// Resolve the requested chroma and geometry against the driver caps (`None` = unknown, ungated).
654fn decide_caps(
655    req_fullcolor: bool,
656    req_w: i32,
657    req_h: i32,
658    cap_yuv444: Option<i32>,
659    cap_width_max: Option<i32>,
660    cap_height_max: Option<i32>,
661) -> CapsDecision {
662    let downgraded_color = req_fullcolor && cap_yuv444 == Some(0);
663    let fullcolor = req_fullcolor && !downgraded_color;
664    let exceeds = |req: i32, cap: Option<i32>| cap.is_some_and(|m| m > 0 && req > m);
665    let too_large = if exceeds(req_w, cap_width_max) || exceeds(req_h, cap_height_max) {
666        Some((cap_width_max.unwrap_or(0), cap_height_max.unwrap_or(0)))
667    } else {
668        None
669    };
670    CapsDecision {
671        fullcolor,
672        downgraded_color,
673        too_large,
674    }
675}
676
677/// The geometry every session is initialized to hold, so `reconfigure_resolution` can grow into
678/// it in place. It spans both UHD and DCI 4K and is the largest picture the 5.x levels of all
679/// three codecs admit, which is what lets AV1 -- pinned to this level -- stay inside a level a
680/// decoder accepts; a taller resize rebuilds the session instead.
681const HEADROOM_WIDTH: u32 = 4096;
682const HEADROOM_HEIGHT: u32 = 2160;
683
684/// The frame rate the level is read at when the session's is lower, so a live change between
685/// the common rates keeps the level, as the geometry headroom keeps it across a resize.
686const HEADROOM_FPS: u32 = 60;
687
688/// The frames a reference window holds for a decoded picture buffer of `dpb`: an H.265 session
689/// keeping anchors still reaches, on Pascal's NVENC, the frame the next frame's reference picture
690/// set lets go, one past the buffer it declares. Turing's, Ampere's and Ada's do not, and
691/// predict from the anchor before that frame where the window names it: a dependency the window
692/// states no older than the real one, so a client holding the frame named holds the one used.
693fn window_frames(codec: Codec, dpb: u32, anchored: bool) -> u32 {
694    dpb + u32::from(codec == Codec::H265 && anchored)
695}
696
697/// Persistent anchors only where the negotiated API exposes them, the device can invalidate
698/// references and the DPB leaves two recent pictures beside them. AV1 gained the LTR fields in
699/// SDK 13; drivers negotiated down to an older API keep the unanchored path. AV1 keeps one, a
700/// frame every forty-eight, unless the consumer `acknowledged` what its clients hold and the
701/// device has room for two: then the newest every client holds is pinned beside the one the
702/// schedule marks. H.264 keeps one whatever the consumer says: OpenH264's decoder, Firefox's for
703/// WebRTC H.264 where it has no FFmpeg that decodes it, finds a long-term picture of a CABAC
704/// stream by a number no marking sets, so of two it reaches only the first, and decodes a frame
705/// naming the second against another picture without an error; and a key frame marked
706/// long-term leaves a second index past `MaxLongTermFrameIdx` besides.
707fn anchor_count(
708    codec: Codec,
709    api_major: u32,
710    invalidation: bool,
711    ltr: Option<i32>,
712    dpb: u32,
713    acknowledged: bool,
714) -> usize {
715    let fits =
716        |n: usize| invalidation && ltr.is_some_and(|l| l >= n as i32) && dpb as usize >= n + 2;
717    let count = match codec {
718        Codec::Av1 if api_major < 13 => 0,
719        Codec::H265 => ANCHORS,
720        Codec::Av1 if acknowledged && fits(ANCHORS) => ANCHORS,
721        _ => 1,
722    };
723    if fits(count) { count } else { 0 }
724}
725
726/// The in-place resize headroom for one axis: the requested size lifted to `floor` but never past
727/// the driver's reported maximum, so initializing with headroom cannot itself exceed what the GPU
728/// supports.
729fn nvenc_headroom(size: u32, floor: u32, cap: Option<i32>) -> u32 {
730    let want = size.max(floor);
731    match cap {
732        Some(m) if m > 0 => want.min(m as u32),
733        _ => want,
734    }
735}
736
737/// The share of an AV1 level's Annex A MaxBitrate the strictest driver lets a session declare
738/// at it, as a fraction: two thirds on 595.71.05, where 595.91.07 and 615.71.09 take the whole.
739const NVENC_AV1_RATE: (u64, u64) = (2, 3);
740
741/// Whether the process's driver holds an AV1 level to `NVENC_AV1_RATE` of its Annex A rate: set
742/// where it refused a session or a rate change at the Annex A level and took the weighted one,
743/// so later sessions declare the weighted level without the refusal.
744static NVENC_AV1_WEIGHTED: AtomicBool = AtomicBool::new(false);
745
746/// Whether the process's driver refused a CBR session's quarter-resolution first pass, as
747/// Kepler's does, taking only a full-resolution one: later sessions code CBR in a single pass
748/// without the refusal. On a Tesla K80 the full-resolution pass encodes a 1080p H.264 frame in
749/// 12.7 ms where the single pass takes 7.3 ms and overshoots its target by 4%.
750static NVENC_SINGLE_PASS: AtomicBool = AtomicBool::new(false);
751
752/// The rate a `codec` level has to admit for a peak of `bps`: AV1's weighed by `NVENC_AV1_RATE`
753/// where the driver holds a level to that share.
754fn level_rate(codec: Codec, bps: u64, weighted: bool) -> u64 {
755    if codec == Codec::Av1 && weighted {
756        (bps * NVENC_AV1_RATE.1).div_ceil(NVENC_AV1_RATE.0)
757    } else {
758        bps
759    }
760}
761
762/// The highest CBR target an NVENC session of `codec` opens at, its top level's bitrate ceiling:
763/// H.264 6.2's 1 Gbit/s, HEVC 6.2's 800 Mbit/s at the High tier production sessions declare,
764/// and `NVENC_AV1_RATE` of AV1 6.3's Main-tier 160 Mbit/s. No level admits a rate past it, so
765/// the driver would refuse the session, or the change, as an invalid level.
766fn nvenc_rate_ceiling(codec: Codec) -> u32 {
767    match codec {
768        Codec::Av1 => (160_000_000 * NVENC_AV1_RATE.0 / NVENC_AV1_RATE.1) as u32,
769        Codec::H265 => 800_000_000,
770        _ => 1_000_000_000,
771    }
772}
773
774/// The split-frame mode a `codec` session asks for at `width` x `height` on a device of `engines`
775/// NVENC engines under the negotiated `api`, as `gpu_bench_tuning_on_content` measured it. AV1
776/// splits across the engines whatever the picture: a 1080p frame encodes in 1.36 ms rather than
777/// 2.07 on an RTX 4090, a 1440p one in 2.27 rather than 3.58, and both code as well or better,
778/// where the driver's own choice splits only from 4K. HEVC splits from a 4K picture, which the
779/// driver's choice splits on Ada but not on Pascal: a GTX 1080 encodes the 2160p frame in 10.3 ms
780/// rather than 16.1, more than 5% of the 81 ms such a session measures glass to glass, for 2.5 dB
781/// of PSNR on scrolling text. Below 4K a split saves HEVC about a millisecond for up to 2.3 dB,
782/// so the driver decides there, and H.264 never splits. The forced mode cuts two strips on three
783/// engines as on two, so AV1 asks for three there: an L40S encodes a 1080p frame in 1.45 ms
784/// rather than 1.62 and a 2160p one in 4.2 rather than 5.3, coding better in five of six rows
785/// (up to 1.5 dB on text) and 0.5 dB worse in one, where HEVC's third strip saves nothing at 4K
786/// and costs 0.1 to 0.2 dB. Before API 12.1 the field's bits are another flag's, so an older
787/// session leaves them clear.
788fn split_mode(
789    codec: Codec,
790    width: u32,
791    height: u32,
792    engines: Option<i32>,
793    api: (u32, u32),
794) -> NV_ENC_SPLIT_ENCODE_MODE {
795    let forced = api >= (12, 1)
796        && engines.is_some_and(|n| n > 1)
797        && match codec {
798            Codec::Av1 => true,
799            Codec::H265 => u64::from(width) * u64::from(height) >= 3840 * 2160,
800            _ => false,
801        };
802    match (forced, codec, engines) {
803        (true, Codec::Av1, Some(3)) => NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_THREE_FORCED_MODE,
804        (true, ..) => NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_AUTO_FORCED_MODE,
805        _ => NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_AUTO_MODE,
806    }
807}
808
809/// The CBR target of a session at `settings`, in bits per second, held to the codec's
810/// `nvenc_rate_ceiling`.
811fn cbr_bps(settings: &CaptureSettings) -> u32 {
812    (settings.video_bitrate_kbps.max(0) as u32)
813        .saturating_mul(1000)
814        .min(nvenc_rate_ceiling(settings.codec))
815}
816
817/// Say so where `cbr_bps` held the target `settings` asks for to the codec's ceiling.
818fn log_held_rate(settings: &CaptureSettings, bps: u32) {
819    let asked = settings.video_bitrate_kbps.max(0) as u64 * 1000;
820    if u64::from(bps) < asked {
821        eprintln!(
822            "[NVENC] {} opens at {} kbps at most, its top level's ceiling: {} kbps is held there",
823            settings.codec.display(),
824            bps / 1000,
825            asked / 1000
826        );
827    }
828}
829
830/// The VBV of a CBR session at `bps`, from the session's frame rate, key-frame interval, and
831/// explicit multiplier.
832fn cbr_vbv(settings: &CaptureSettings, bps: u32) -> u32 {
833    crate::encoders::vbv_bits(
834        bps,
835        settings.target_fps,
836        settings.keyframe_interval_s,
837        settings.video_vbv_multiplier,
838    )
839}
840
841/// Write a CBR target into `rc`: the average and peak rate, the VBV, and an initial delay of the
842/// whole buffer. That delay is the driver's default (`gpu_bench_cbr_policy` finds the two
843/// identical), stated so the HRD's starting point is explicit and a rate change restates it
844/// with the buffer it belongs to.
845fn set_cbr_rate(rc: &mut NV_ENC_RC_PARAMS, bps: u32, vbv: u32) {
846    rc.averageBitRate = bps;
847    rc.maxBitRate = bps;
848    rc.vbvBufferSize = vbv;
849    rc.vbvInitialDelay = vbv;
850}
851
852/// The peak a capped constant quantizer runs under (`RateSettings::capped`), as NVENC's
853/// constant rate takes it: the target held to the codec's ceiling, and its buffer.
854///
855/// NVENC's constant quantizer takes no cap, and its quality-targeting VBR codes a picture of its
856/// own rather than the constant quantizer's, so the session switches (`PeakBucket`): it codes at
857/// its constant quantizer until its output overflows the peak, then through the constant rate at
858/// the peak with the quantizer as its `minQP`, and back. The rate-control mode is one of the
859/// parameters `nvEncReconfigureEncoder` takes without a reset (`hold_rate`), so neither switch
860/// costs a key frame. A peak past the rate the level the session declared carries leaves it at
861/// its constant quantizer, since a raised level is a key frame.
862fn peak_of(settings: &CaptureSettings) -> Option<PeakBucket> {
863    RateSettings::new(settings).capped.then(|| {
864        let bps = cbr_bps(settings);
865        PeakBucket::new(bps, cbr_vbv(settings, bps), settings.target_fps)
866    })
867}
868
869/// Quality index steps finer than the paint-over one a refined block is coded at
870/// (`FrameEncoder::refines`): what brings it to the quality NVENC's own cleanup of a still screen
871/// reaches. 1080p text beside a video, H.264 on an RTX 3060, against that cleanup's 45.2 dB at
872/// 8 Mbit/s, 47.6 at 16, and 47.0 at a constant quality: 44.4, 45.5, and 46.8 at the paint-over
873/// quantizer, 45.9, 47.0, and 48.1 three steps finer, 48.2, 48.7, and 50.6 six steps finer,
874/// spending on the text what the video would have had.
875pub const REFINE_FINER: u32 = 3;
876
877/// The share of its cap a held band taken back for coming out past it is coded again to, at the
878/// bytes a block of it came to (`NvencEncoder::cap`): the next rows of a page can be denser.
879const REFIT_SHARE: f64 = 0.8;
880
881/// Recent frames whose held bands the session keeps (`forget_bands`): past how far back an
882/// invalidation of its own before an anchor reaches (`reference::HOLD_MAX`, 384).
883const BAND_LOG_FRAMES: usize = 512;
884
885/// The band a frame codes once the frames from timestamp `pts` on are left out of every
886/// prediction, their bands taken out of `log`: the one it holds (`held`) reaching back over theirs;
887/// and whether theirs are lost, a frame that holds no quantizer (`holds`) taking none of them back.
888/// A frame held whole codes them anyway.
889fn reach_back(
890    log: &mut VecDeque<(u64, f64, f64)>,
891    pts: u64,
892    held: Option<(f64, f64)>,
893    holds: bool,
894) -> (Option<(f64, f64)>, bool) {
895    let from = log
896        .iter()
897        .filter(|b| b.0 >= pts)
898        .map(|b| b.1)
899        .reduce(f64::min);
900    log.retain(|b| b.0 < pts);
901    match (from, held) {
902        (Some(from), Some((a, b))) => (Some((a.min(from), b)), false),
903        (Some(_), None) => (None, !holds),
904        (None, held) => (held, false),
905    }
906}
907
908/// Fill `map`, the QP delta map of a `width` x `height` picture of `codec` (one entry a 16x16
909/// macroblock for H.264, a 32x32 coding tree block for HEVC, a 64x64 superblock for AV1, in
910/// raster order), with `delta` over the blocks the share of the picture `from..to` covers,
911/// rounded outward, and 0 elsewhere; returns the band's first block.
912fn fill_band_map(
913    map: &mut Vec<i8>,
914    codec: Codec,
915    width: u32,
916    height: u32,
917    (from, to): (f64, f64),
918    delta: i32,
919) -> usize {
920    let block = match codec {
921        Codec::H264 => 16,
922        Codec::H265 => 32,
923        _ => 64,
924    };
925    let blocks = (width.div_ceil(block) * height.div_ceil(block)) as usize;
926    let first = ((from.clamp(0.0, 1.0) * blocks as f64).floor() as usize).min(blocks);
927    let last = ((to.clamp(0.0, 1.0) * blocks as f64).ceil() as usize).clamp(first, blocks);
928    map.clear();
929    map.resize(blocks, 0);
930    map[first..last].fill(delta.clamp(-(codec.quantizer_max().min(128) as i32), 0) as i8);
931    first
932}
933
934/// Hand `pic` the QP delta map `map` at API `api`. Below 12.0 the codec union runs eight bytes
935/// shorter and the driver reads the map's pointer and size where the pinned layout keeps
936/// `reserved5`.
937fn point_at_qp_map(pic: &mut NV_ENC_PIC_PARAMS, map: &mut [i8], api: (u32, u32)) {
938    if api < (12, 0) {
939        pic.reserved5 = [
940            map.as_mut_ptr().cast(),
941            ptr::without_provenance_mut(map.len()),
942        ];
943    } else {
944        pic.qpDeltaMap = map.as_mut_ptr();
945        pic.qpDeltaMapSize = map.len() as u32;
946    }
947}
948
949/// The driver's message for the last failure on `session`, or a stand-in when it has none: NVENC
950/// clears the string once a session is torn down, so it is only meaningful read straight after
951/// the call that failed.
952unsafe fn last_error(funcs: &NV_ENCODE_API_FUNCTION_LIST, session: *mut c_void) -> String {
953    funcs
954        .nvEncGetLastErrorString
955        .map(|f| f(session))
956        .filter(|p| !p.is_null())
957        .map(|p| CStr::from_ptr(p).to_string_lossy().into_owned())
958        .filter(|s| !s.is_empty())
959        .unwrap_or_else(|| "no error string".to_string())
960}
961
962/// Query one NVENC capability of `codec` on an open session, returning the driver's integer
963/// answer or `None` when the entry point is absent or the query fails — `decide_caps` reads
964/// `None` as "do not gate", so a query failure never becomes a false refusal.
965unsafe fn query_cap(
966    funcs: &NV_ENCODE_API_FUNCTION_LIST,
967    session: *mut c_void,
968    codec: GUID,
969    cap: NV_ENC_CAPS,
970) -> Option<i32> {
971    let get = funcs.nvEncGetEncodeCaps?;
972    let mut param = NV_ENC_CAPS_PARAM {
973        version: sv(NvStruct::CapsParam),
974        capsToQuery: cap,
975        reserved: [0u32; 62],
976    };
977    let mut val: i32 = 0;
978    if get(session, codec, &mut param, &mut val) == NVENCSTATUS::NV_ENC_SUCCESS {
979        Some(val)
980    } else {
981        None
982    }
983}
984
985/// GUID selecting the H.264 **High** profile (4:2:0) for `NV_ENC_CONFIG::profileGUID`.
986const NV_ENC_H264_PROFILE_HIGH_GUID: GUID = GUID {
987    Data1: 0x205b553d,
988    Data2: 0x5f01,
989    Data3: 0x4d9e,
990    Data4: [0x91, 0x84, 0xda, 0x32, 0x77, 0x5b, 0x55, 0x9b],
991};
992
993/// GUID selecting the H.264 **High 4:4:4 Predictive** profile for full-color encoding.
994const NV_ENC_H264_PROFILE_HIGH_444_GUID: GUID = GUID {
995    Data1: 0x7ac663cb,
996    Data2: 0xa598,
997    Data3: 0x4960,
998    Data4: [0xb8, 0x44, 0x33, 0x9b, 0x26, 0x1a, 0x7d, 0x5c],
999};
1000
1001/// Whether two GUIDs are the same.
1002fn guid_eq(a: &GUID, b: &GUID) -> bool {
1003    a.Data1 == b.Data1 && a.Data2 == b.Data2 && a.Data3 == b.Data3 && a.Data4 == b.Data4
1004}
1005
1006/// The NVENC codec GUID of a codec the encoder serves, or `None` for one NVENC has no engine
1007/// for.
1008fn codec_guid(codec: Codec) -> Option<GUID> {
1009    match codec {
1010        Codec::H264 => Some(NV_ENC_CODEC_H264_GUID),
1011        Codec::H265 => Some(NV_ENC_CODEC_HEVC_GUID),
1012        Codec::Av1 => Some(NV_ENC_CODEC_AV1_GUID),
1013        Codec::Jpeg | Codec::Vp8 | Codec::Vp9 => None,
1014    }
1015}
1016
1017/// The video codecs the NVENC of the GPU behind `encode_node_index` has an engine for, read
1018/// from a bare session's GUID list the way a real session reads it (`device_encodes`): the
1019/// driver is negotiated, the device bound by the render node's PCI bus id (the first CUDA
1020/// device where the node names none), and the session opened on its primary context with no
1021/// input buffers, EGL, or encoder initialization. An error names the step that failed: no
1022/// driver, no device, or a session that would not open, each of which a real session would
1023/// fail on too. The CUDA and NVENC libraries stay loaded like a session's, since the driver
1024/// does not promise to survive `libcuda` being unloaded after `cuInit`.
1025pub(crate) fn probe_codecs(encode_node_index: i32) -> Result<Vec<(Codec, super::Formats)>, String> {
1026    let cuda = std::mem::ManuallyDrop::new(NvencEncoder::load_cuda()?);
1027    let nvenc_lib = std::mem::ManuallyDrop::new(NvencEncoder::load_nvenc()?);
1028    nvenc_negotiate(&nvenc_lib);
1029    crate::nvgpufilter::install();
1030    unsafe {
1031        let res = (cuda.cuInit)(0);
1032        if res != CUresult::CUDA_SUCCESS {
1033            return Err(format!(
1034                "Init CUDA failed: {}",
1035                NvencEncoder::get_error_string(&cuda, res)
1036            ));
1037        }
1038        let mut cu_device: CUdevice = 0;
1039        let bound = NvencEncoder::get_pci_bus_id(encode_node_index.max(0))
1040            .and_then(|id| CString::new(id).ok())
1041            .is_some_and(|id| {
1042                (cuda.cuDeviceGetByPCIBusId)(&mut cu_device, id.as_ptr()) == CUresult::CUDA_SUCCESS
1043            });
1044        if !bound && (cuda.cuDeviceGet)(&mut cu_device, 0) != CUresult::CUDA_SUCCESS {
1045            return Err("Failed to get default CUDA device".into());
1046        }
1047        let mut cu_context: CUcontext = ptr::null_mut();
1048        if (cuda.cuDevicePrimaryCtxRetain)(&mut cu_context, cu_device) != CUresult::CUDA_SUCCESS {
1049            return Err("Failed to retain the device's primary CUDA context".into());
1050        }
1051        if (cuda.cuCtxPushCurrent_v2)(cu_context) != CUresult::CUDA_SUCCESS {
1052            (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1053            return Err("Failed to make the primary CUDA context current".into());
1054        }
1055        let result = probe_session_codecs(&nvenc_lib, cu_context);
1056        (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1057        (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1058        result
1059    }
1060}
1061
1062/// The probe's answer where the device refuses a session for want of one to spare (the
1063/// session cap of consumer boards, or its memory): nothing lasting, unlike its other refusals.
1064pub(crate) const SESSIONS_TAKEN: &str = "the device has no NVENC session to spare";
1065
1066/// Why the driver refused to open a session: `SESSIONS_TAKEN` for the statuses it answers when
1067/// none is left, `NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY` at the cap of a consumer board and
1068/// `NV_ENC_ERR_OUT_OF_MEMORY` when its memory runs out.
1069fn session_refusal(status: NVENCSTATUS) -> String {
1070    match status {
1071        NVENCSTATUS::NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY | NVENCSTATUS::NV_ENC_ERR_OUT_OF_MEMORY => {
1072            SESSIONS_TAKEN.into()
1073        }
1074        _ => "Failed to open NVENC session".into(),
1075    }
1076}
1077
1078/// Whether a session of `codec` codes 10 bits: HEVC and AV1 where the device reports it, at
1079/// whichever API the session negotiated (`set_bit_depth`). The session converts its 8-bit RGB to
1080/// 10-bit samples itself (`ConvertLayout`). H.264 stays 8-bit, which only the newest engines code
1081/// at 10.
1082unsafe fn codes_ten_bit(
1083    function_list: &NV_ENCODE_API_FUNCTION_LIST,
1084    session: *mut c_void,
1085    codec: Codec,
1086    guid: GUID,
1087) -> bool {
1088    matches!(codec, Codec::H265 | Codec::Av1)
1089        && query_cap(
1090            function_list,
1091            session,
1092            guid,
1093            NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_10BIT_ENCODE,
1094        ) == Some(1)
1095}
1096
1097/// Stamp an HEVC or AV1 `config` with `bit_depth` as API `api` reads it. From 12.2 the session
1098/// names its input and output depths; before it HEVC carries the output's minus eight in the
1099/// bitfield the pinned layout keeps as `reserved3`, its input following the buffer format, and
1100/// AV1 (12.0 and 12.1) the input's and the output's in the bits 13.0 gave its LTR, SVC and HDR
1101/// flags. The offsets count from the codec config's first bitfield, at byte 16.
1102fn set_bit_depth(config: &mut NV_ENC_CONFIG, codec: Codec, bit_depth: u32, api: (u32, u32)) {
1103    let depth = if bit_depth > 8 {
1104        NV_ENC_BIT_DEPTH::NV_ENC_BIT_DEPTH_10
1105    } else {
1106        NV_ENC_BIT_DEPTH::NV_ENC_BIT_DEPTH_8
1107    };
1108    let minus8 = u64::from(bit_depth.saturating_sub(8));
1109    unsafe {
1110        match codec {
1111            Codec::H265 if api >= (12, 2) => {
1112                let c = &mut config.encodeCodecConfig.hevcConfig;
1113                c.inputBitDepth = depth;
1114                c.outputBitDepth = depth;
1115            }
1116            Codec::H265 => config
1117                .encodeCodecConfig
1118                .hevcConfig
1119                ._bitfield_1
1120                .set(11, 3, minus8),
1121            Codec::Av1 if api >= (12, 2) => {
1122                let c = &mut config.encodeCodecConfig.av1Config;
1123                c.inputBitDepth = depth;
1124                c.outputBitDepth = depth;
1125            }
1126            Codec::Av1 => {
1127                let bits = &mut config.encodeCodecConfig.av1Config._bitfield_1;
1128                bits.set(12, 3, minus8);
1129                bits.set(15, 3, minus8);
1130            }
1131            _ => {}
1132        }
1133    }
1134}
1135
1136/// Open a bare NVENC session on a current CUDA context, list the codecs its device encodes
1137/// and the formats past 8-bit 4:2:0 it encodes each in, and close it.
1138unsafe fn probe_session_codecs(
1139    nvenc_lib: &NvencLibrary,
1140    cu_context: CUcontext,
1141) -> Result<Vec<(Codec, super::Formats)>, String> {
1142    let mut function_list = NV_ENCODE_API_FUNCTION_LIST {
1143        version: sv(NvStruct::FunctionList),
1144        ..Default::default()
1145    };
1146    if (nvenc_lib.create_instance)(&mut function_list) != NVENCSTATUS::NV_ENC_SUCCESS {
1147        return Err("NvEncodeAPICreateInstance failed".into());
1148    }
1149    let (Some(open_fn), Some(destroy_fn)) = (
1150        function_list.nvEncOpenEncodeSessionEx,
1151        function_list.nvEncDestroyEncoder,
1152    ) else {
1153        return Err("the driver's NVENC function list has no session entry points".into());
1154    };
1155    let mut session_params = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
1156        version: sv(NvStruct::OpenSessionExParams),
1157        deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
1158        device: cu_context as *mut c_void,
1159        apiVersion: neg_api(),
1160        ..Default::default()
1161    };
1162    let mut session: *mut c_void = ptr::null_mut();
1163    match open_fn(&mut session_params, &mut session) {
1164        NVENCSTATUS::NV_ENC_SUCCESS => {}
1165        status => return Err(session_refusal(status)),
1166    }
1167    let codecs = Codec::VIDEO
1168        .into_iter()
1169        .filter_map(|codec| {
1170            let guid = codec_guid(codec)
1171                .filter(|guid| NvencEncoder::device_encodes(&function_list, session, guid))?;
1172            let fullcolor = codec.fullcolor()
1173                && query_cap(
1174                    &function_list,
1175                    session,
1176                    guid,
1177                    NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE,
1178                ) == Some(1);
1179            let ten_bit = codes_ten_bit(&function_list, session, codec, guid);
1180            Some((
1181                codec,
1182                super::Formats {
1183                    fullcolor,
1184                    ten_bit: [ten_bit, ten_bit && fullcolor],
1185                },
1186            ))
1187        })
1188        .collect();
1189    destroy_fn(session);
1190    Ok(codecs)
1191}
1192
1193/// `sliceMode = 3`: `sliceModeData` is the number of slices in the picture, which the driver
1194/// divides evenly; the other modes count macroblocks, bytes, or rows and drift with geometry.
1195const SLICE_MODE_COUNT: u32 = 3;
1196
1197/// Slices per H.264 frame, the count the VA-API (`SLICES`; one for H.264 on AMD's VCE) and
1198/// OpenH264 (`SM_FIXEDSLCNUM_SLICE`) sessions emit too: a client decoding in software threads a
1199/// frame across its slices, and more than four upsets some Chromium decoders. What the slices cost
1200/// at a fixed quantizer is measured by `gpu_bench_slices`. An HEVC frame is one slice, since no
1201/// decoder a client has threads HEVC's plain slices (FFmpeg threads its wavefronts and tiles), and
1202/// its band frames take theirs where a band starts (`set_band_layout`). AV1 partitions by tiles
1203/// instead, asked for as 1x1 in `configure_codec`.
1204const SLICES_PER_FRAME: u32 = 4;
1205
1206/// The most slices an HEVC band frame takes to start one at its band (`set_band_layout`). NVENC
1207/// sizes a frame's slices evenly, so a band starting within the first eighth of the picture would
1208/// take more, each a header a low rate's frame cannot spare: at 60 fps and 0.5 Mbit/s, 40 to 60 of
1209/// them took most of a band frame's bytes and left its band coarse.
1210const BAND_SLICES_MAX: usize = 8;
1211
1212/// The frames an AV1 session keeps, as many as a frame predicts from, LAST, LAST2, LAST3, and
1213/// GOLDEN (`numFwdRefs` tops out at four). The device keeps eight as well, predicting past a loss
1214/// from further back, but takes 112 MiB more a session at the resize headroom. With SDK 13
1215/// and LTR support, the anchors among them let an invalidation reach further back.
1216const AV1_REFERENCES: u32 = 4;
1217
1218/// Output bitstream buffers per session: one, because `submit_frame` locks, copies, and unlocks
1219/// each frame's bitstream before it returns, so no second buffer is ever outstanding (the lock
1220/// blocks; a `doNotWait` lock on Linux answers an unfinished encode with an empty bitstream
1221/// rather than `NV_ENC_ERR_LOCK_BUSY`). The ring stays, so a pipelined depth is one constant
1222/// away.
1223const BITSTREAM_BUFFERS: usize = 1;
1224
1225/// The encoder-side quality knobs of a session: the preset, the rate-control passes of a CBR
1226/// session, and adaptive quantization. Production sessions take the default, which the tuning
1227/// bench (`gpu_bench_tuning`) measures against the alternatives: P3 encodes a 1080p H.264
1228/// frame in 3.7 ms where P4 takes 5.2 ms on a V100, for 0.001 of SSIM at the same bitrate,
1229/// and the presets above P4 buy nothing; a single pass saves half a millisecond but overshoots
1230/// a CBR target by five to twelve percent; adaptive quantization moves neither time nor
1231/// quality measurably. The quarter-resolution first pass needs the 1.5-frame VBV `vbv_bits`
1232/// gives it: on a one-frame buffer its miss on a scene cut runs an 8 Mbit/s H.264 session at
1233/// 13.7 Mbit/s (`gpu_bench_cbr_rate_control`). P3 holds on real content as well
1234/// (`gpu_bench_tuning_on_content`, scrolling text and a panned texture at 1080p to 2160p): on a
1235/// GTX 1080 and an RTX 4090 P1 encodes 0.5 to 2.4 ms sooner and P2 less, at most about 5% of the
1236/// glass to glass latency a session of that size measures (25, 41, and 81 ms over WebSockets),
1237/// and both code the text worse, by up to 0.8 dB in H.264 and 2.4 dB in AV1.
1238#[derive(Clone, Copy, Debug)]
1239pub(crate) struct NvencTuning {
1240    pub preset: GUID,
1241    pub multipass: NV_ENC_MULTI_PASS,
1242    pub spatial_aq: bool,
1243    /// Temporal adaptive quantization, refused at open where the driver reports no support for
1244    /// it: a Volta HEVC session given it faults inside its first encode instead of failing to
1245    /// open.
1246    pub temporal_aq: bool,
1247    /// Whether an HEVC session declares the tier `h265_tier` names for its level; off opens the
1248    /// same session at Main tier, the ceiling `gpu_hevc_high_tier_opens_above_the_main_tier_ceiling`
1249    /// measures.
1250    pub hevc_high_tier: bool,
1251    /// Slices per H.264 and HEVC frame outside a band frame's: `None` in production, which is
1252    /// `SLICES_PER_FRAME` for H.264 and one for HEVC; a count to measure what slices cost
1253    /// (`gpu_bench_slices`).
1254    pub slices: Option<u32>,
1255    /// Leave the chroma convert out, so the session encodes through NVENC's own conversion —
1256    /// the path a driver refusing the kernel takes, which
1257    /// `gpu_hardware_conversion_matches_the_declared_matrix` measures.
1258    #[cfg(test)]
1259    pub hardware_csc: bool,
1260    /// The L0 references of an H.264 or HEVC frame, AV1's forward references: the preset's
1261    /// choice in production, where one reference saves no time, set to measure.
1262    #[cfg(test)]
1263    pub ref_l0: Option<NV_ENC_NUM_REF_FRAMES>,
1264    /// A split-frame mode in place of `split_mode`'s, to measure.
1265    #[cfg(test)]
1266    pub split: Option<NV_ENC_SPLIT_ENCODE_MODE>,
1267}
1268
1269impl Default for NvencTuning {
1270    fn default() -> Self {
1271        Self {
1272            preset: NV_ENC_PRESET_P3_GUID,
1273            multipass: NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
1274            spatial_aq: false,
1275            temporal_aq: false,
1276            hevc_high_tier: true,
1277            slices: None,
1278            #[cfg(test)]
1279            hardware_csc: false,
1280            #[cfg(test)]
1281            ref_l0: None,
1282            #[cfg(test)]
1283            split: None,
1284        }
1285    }
1286}
1287
1288/// The profile of a session: the 4:2:0 profile of the codec at its depth, or its 4:4:4 one
1289/// where the codec has it. HEVC's range extensions carry 4:4:4 at either depth, and AV1's main
1290/// profile both depths.
1291fn profile_guid(codec: Codec, fullcolor: bool, bit_depth: u32) -> GUID {
1292    match (codec, fullcolor) {
1293        (Codec::H264, true) => NV_ENC_H264_PROFILE_HIGH_444_GUID,
1294        (Codec::H264, false) => NV_ENC_H264_PROFILE_HIGH_GUID,
1295        (Codec::H265, true) => NV_ENC_HEVC_PROFILE_FREXT_GUID,
1296        (Codec::H265, false) if bit_depth > 8 => NV_ENC_HEVC_PROFILE_MAIN10_GUID,
1297        (Codec::H265, false) => NV_ENC_HEVC_PROFILE_MAIN_GUID,
1298        _ => NV_ENC_AV1_PROFILE_MAIN_GUID,
1299    }
1300}
1301
1302/// The 4:2:0 convert that replaces NVENC's own, because the hardware's fixed-function
1303/// conversion weights the two columns of a block 3:1 instead of averaging them, leaving half the
1304/// color of a subpixel-antialiased glyph edge in the chroma plane where the software and VA-API
1305/// converts leave none. It follows the matrix the session declares
1306/// (`gpu_hardware_conversion_matches_the_declared_matrix`), so only the siting is at stake, and
1307/// 4:4:4 — which subsamples nothing — keeps it.
1308///
1309/// The kernels ship as PTX the driver JIT-compiles (`cuModuleLoadData`), so the only library
1310/// involved is the `libcuda` NVENC already needs — nothing to install, and no runtime compiler.
1311/// They read the session's packed ARGB surface, or a texture over an array-typed dmabuf import,
1312/// and write the NV12 surface NVENC then encodes, averaging each block's RGB before the matrix
1313/// as the host convert does.
1314/// The surface a `ChromaConvert` writes for NVENC: NV12 for an 8-bit 4:2:0 session, and for a
1315/// 10-bit one P010 or planar 4:4:4, whose samples the kernel computes from the 8-bit RGB at
1316/// 10 bits. NVENC upconverts an 8-bit surface itself, but from samples already rounded to
1317/// eight: a 4:4:4 HEVC session coded that way measured 41.6 dB against its 8-bit 45.5 at one
1318/// quantizer on an RTX 3060, its luma half a level low.
1319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1320enum ConvertLayout {
1321    Nv12,
1322    P010,
1323    Yuv444P10,
1324}
1325
1326impl ConvertLayout {
1327    /// The layout a session of this chroma and depth converts into; None for 8-bit 4:4:4,
1328    /// which subsamples nothing and keeps NVENC's own conversion.
1329    fn of(fullcolor: bool, bit_depth: u32) -> Option<Self> {
1330        match (fullcolor, bit_depth > 8) {
1331            (false, false) => Some(Self::Nv12),
1332            (false, true) => Some(Self::P010),
1333            (true, true) => Some(Self::Yuv444P10),
1334            (true, false) => None,
1335        }
1336    }
1337
1338    /// The kernels reading a linear surface and a texture.
1339    fn kernels(self) -> (&'static CStr, &'static CStr) {
1340        match self {
1341            Self::Nv12 => (c"argb_to_nv12", c"argb_tex_to_nv12"),
1342            Self::P010 => (c"argb_to_p010", c"argb_tex_to_p010"),
1343            Self::Yuv444P10 => (c"argb_to_yuv444p10", c"argb_tex_to_yuv444p10"),
1344        }
1345    }
1346
1347    /// The bytes a row and the rows a `width`x`height` picture take: a luma plane and half as
1348    /// many rows of interleaved chroma, or three whole planes.
1349    fn extent(self, width: u32, height: u32) -> (usize, usize) {
1350        let (width, height) = (width as usize, height as usize);
1351        match self {
1352            Self::Nv12 => (width, height + height.div_ceil(2)),
1353            Self::P010 => (2 * width, height + height.div_ceil(2)),
1354            Self::Yuv444P10 => (2 * width, 3 * height),
1355        }
1356    }
1357
1358    fn format(self) -> NV_ENC_BUFFER_FORMAT {
1359        match self {
1360            Self::Nv12 => NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_NV12,
1361            Self::P010 => NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV420_10BIT,
1362            Self::Yuv444P10 => NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444_10BIT,
1363        }
1364    }
1365}
1366
1367struct ChromaConvert {
1368    module: CUmodule,
1369    kernel: CUfunction,
1370    kernel_tex: CUfunction,
1371    layout: ConvertLayout,
1372    surface: CUdeviceptr,
1373    pitch: usize,
1374    registered: NV_ENC_REGISTERED_PTR,
1375    mapped: NV_ENC_INPUT_PTR,
1376}
1377
1378impl ChromaConvert {
1379    /// JIT the module, allocate the `width`x`height` surface of `layout`, and register it with
1380    /// the session. `None` where any step refuses: the session then encodes the packed RGB itself,
1381    /// which sites chroma at the left of each block but converts with the matrix the session
1382    /// declares all the same.
1383    unsafe fn new(
1384        cuda: &CudaFunctions,
1385        funcs: &NV_ENCODE_API_FUNCTION_LIST,
1386        session: *mut c_void,
1387        width: u32,
1388        height: u32,
1389        layout: ConvertLayout,
1390    ) -> Option<Self> {
1391        let module = load_kernels(cuda)?;
1392        let mut kernel: CUfunction = ptr::null_mut();
1393        let mut kernel_tex: CUfunction = ptr::null_mut();
1394        let (linear, texture) = layout.kernels();
1395        if (cuda.cuModuleGetFunction)(&mut kernel, module, linear.as_ptr())
1396            != CUresult::CUDA_SUCCESS
1397            || (cuda.cuModuleGetFunction)(&mut kernel_tex, module, texture.as_ptr())
1398                != CUresult::CUDA_SUCCESS
1399        {
1400            (cuda.cuModuleUnload)(module);
1401            return None;
1402        }
1403        let (mut nv12, mut pitch): (CUdeviceptr, usize) = (0, 0);
1404        // The planes follow one another at the same pitch, which is the one allocation NVENC
1405        // reads all of. The chroma rows of a 4:2:0 layout round up, so the kernel's last row
1406        // is inside the allocation even at an odd height, which the capture paths do not
1407        // produce for a video codec but nothing here relies on.
1408        let (row_bytes, rows) = layout.extent(width, height);
1409        if (cuda.cuMemAllocPitch_v2)(&mut nv12, &mut pitch, row_bytes, rows, 4)
1410            != CUresult::CUDA_SUCCESS
1411        {
1412            (cuda.cuModuleUnload)(module);
1413            return None;
1414        }
1415        let mut reg = NV_ENC_REGISTER_RESOURCE {
1416            version: sv(NvStruct::RegisterResource),
1417            resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
1418            width,
1419            height,
1420            resourceToRegister: nv12 as *mut c_void,
1421            pitch: pitch as u32,
1422            bufferFormat: layout.format(),
1423            bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
1424            ..Default::default()
1425        };
1426        if (funcs.nvEncRegisterResource.unwrap())(session, &mut reg) != NVENCSTATUS::NV_ENC_SUCCESS
1427        {
1428            (cuda.cuMemFree_v2)(nv12);
1429            (cuda.cuModuleUnload)(module);
1430            return None;
1431        }
1432        let mut map = NV_ENC_MAP_INPUT_RESOURCE {
1433            version: sv(NvStruct::MapInputResource),
1434            registeredResource: reg.registeredResource,
1435            ..Default::default()
1436        };
1437        if (funcs.nvEncMapInputResource.unwrap())(session, &mut map) != NVENCSTATUS::NV_ENC_SUCCESS
1438        {
1439            (funcs.nvEncUnregisterResource.unwrap())(session, reg.registeredResource);
1440            (cuda.cuMemFree_v2)(nv12);
1441            (cuda.cuModuleUnload)(module);
1442            return None;
1443        }
1444        Some(ChromaConvert {
1445            module,
1446            kernel,
1447            kernel_tex,
1448            layout,
1449            surface: nv12,
1450            pitch,
1451            registered: reg.registeredResource,
1452            mapped: map.mappedResource,
1453        })
1454    }
1455
1456    /// The registered surface and its format, as NVENC is handed them.
1457    fn input(&self) -> (NV_ENC_INPUT_PTR, NV_ENC_BUFFER_FORMAT) {
1458        (self.mapped, self.layout.format())
1459    }
1460
1461    /// Convert the `width`x`height` packed surface at `src`/`src_pitch` into the surface,
1462    /// on the default stream so it is ordered behind the upload and ahead of the encode.
1463    /// `swap_rb` marks an RGBA byte order rather than BGRA.
1464    unsafe fn run(
1465        &self,
1466        cuda: &CudaFunctions,
1467        src: CUdeviceptr,
1468        src_pitch: usize,
1469        width: u32,
1470        height: u32,
1471        swap_rb: bool,
1472    ) -> Result<(), String> {
1473        let (mut src, mut sp) = (src, src_pitch as i32);
1474        let (mut dst, mut dp) = (self.surface, self.pitch as i32);
1475        let (mut w, mut h, mut swap) = (width as i32, height as i32, i32::from(swap_rb));
1476        let mut params: [*mut c_void; 7] = [
1477            &mut src as *mut _ as *mut c_void,
1478            &mut sp as *mut _ as *mut c_void,
1479            &mut dst as *mut _ as *mut c_void,
1480            &mut dp as *mut _ as *mut c_void,
1481            &mut w as *mut _ as *mut c_void,
1482            &mut h as *mut _ as *mut c_void,
1483            &mut swap as *mut _ as *mut c_void,
1484        ];
1485        self.launch(cuda, self.kernel, &mut params, width, height)
1486    }
1487
1488    /// The same convert reading an array-typed import through `tex`, which leaves its RGB where
1489    /// the compositor put it instead of copying it into linear memory first.
1490    unsafe fn run_texture(
1491        &self,
1492        cuda: &CudaFunctions,
1493        tex: CUtexObject,
1494        width: u32,
1495        height: u32,
1496        swap_rb: bool,
1497    ) -> Result<(), String> {
1498        let mut tex = tex;
1499        let (mut dst, mut dp) = (self.surface, self.pitch as i32);
1500        let (mut w, mut h, mut swap) = (width as i32, height as i32, i32::from(swap_rb));
1501        let mut params: [*mut c_void; 6] = [
1502            &mut tex as *mut _ as *mut c_void,
1503            &mut dst as *mut _ as *mut c_void,
1504            &mut dp as *mut _ as *mut c_void,
1505            &mut w as *mut _ as *mut c_void,
1506            &mut h as *mut _ as *mut c_void,
1507            &mut swap as *mut _ as *mut c_void,
1508        ];
1509        self.launch(cuda, self.kernel_tex, &mut params, width, height)
1510    }
1511
1512    /// One chroma sample per thread, on the default stream so the convert is ordered behind
1513    /// whatever produced the source and ahead of the encode.
1514    unsafe fn launch(
1515        &self,
1516        cuda: &CudaFunctions,
1517        kernel: CUfunction,
1518        params: &mut [*mut c_void],
1519        width: u32,
1520        height: u32,
1521    ) -> Result<(), String> {
1522        const BLOCK: u32 = 16;
1523        let grid = (
1524            width.div_ceil(2).div_ceil(BLOCK),
1525            height.div_ceil(2).div_ceil(BLOCK),
1526        );
1527        if (cuda.cuLaunchKernel)(
1528            kernel,
1529            grid.0,
1530            grid.1,
1531            1,
1532            BLOCK,
1533            BLOCK,
1534            1,
1535            0,
1536            ptr::null_mut(),
1537            params.as_mut_ptr(),
1538            ptr::null_mut(),
1539        ) != CUresult::CUDA_SUCCESS
1540        {
1541            return Err("the chroma convert kernel failed to launch".into());
1542        }
1543        Ok(())
1544    }
1545
1546    /// A point-sampled, byte-valued texture over an array-typed import, or zero where the driver
1547    /// refuses one — the frame then stays on NVENC's own conversion.
1548    unsafe fn texture_for(cuda: &CudaFunctions, array: CUarray) -> CUtexObject {
1549        let mut res: CUDA_RESOURCE_DESC = std::mem::zeroed();
1550        res.resType = CUresourcetype::CU_RESOURCE_TYPE_ARRAY;
1551        res.res.array.hArray = array;
1552        let mut sampling: CUDA_TEXTURE_DESC = std::mem::zeroed();
1553        sampling.addressMode = [CUaddress_mode::CU_TR_ADDRESS_MODE_CLAMP; 3];
1554        sampling.filterMode = CUfilter_mode::CU_TR_FILTER_MODE_POINT;
1555        sampling.flags = CU_TRSF_READ_AS_INTEGER;
1556        let mut tex: CUtexObject = 0;
1557        if (cuda.cuTexObjectCreate)(&mut tex, &res, &sampling, ptr::null())
1558            != CUresult::CUDA_SUCCESS
1559        {
1560            return 0;
1561        }
1562        tex
1563    }
1564
1565    /// Inner handle before outer, as the rest of the teardown does.
1566    unsafe fn release(
1567        &self,
1568        cuda: &CudaFunctions,
1569        funcs: &NV_ENCODE_API_FUNCTION_LIST,
1570        session: *mut c_void,
1571    ) {
1572        (funcs.nvEncUnmapInputResource.unwrap())(session, self.mapped);
1573        (funcs.nvEncUnregisterResource.unwrap())(session, self.registered);
1574        (cuda.cuMemFree_v2)(self.surface);
1575        (cuda.cuModuleUnload)(self.module);
1576    }
1577}
1578
1579/// A hash per band of rows of a packed 32-bit frame in video memory (`band_hash`), computed
1580/// where the frame lies and read back as one word a band and column segment.
1581struct BandHash {
1582    module: CUmodule,
1583    kernel: CUfunction,
1584    sums: CUdeviceptr,
1585    capacity: usize,
1586}
1587
1588/// The threads of a `band_hash` block, as the kernel sizes its reduction, and the blocks a band
1589/// is split into across its columns, so a frame's few bands fill the GPU: a P100 hashed 1080p in
1590/// 0.08 ms this way and in 0.12 with a block a band, its 34 bands leaving 22 of the 56
1591/// multiprocessors idle.
1592const HASH_THREADS: u32 = 256;
1593const HASH_SEGMENTS: u32 = 4;
1594
1595impl BandHash {
1596    unsafe fn new(cuda: &CudaFunctions) -> Option<Self> {
1597        let module = load_kernels(cuda)?;
1598        let mut kernel: CUfunction = ptr::null_mut();
1599        if (cuda.cuModuleGetFunction)(&mut kernel, module, c"band_hash".as_ptr())
1600            != CUresult::CUDA_SUCCESS
1601        {
1602            (cuda.cuModuleUnload)(module);
1603            return None;
1604        }
1605        Some(Self {
1606            module,
1607            kernel,
1608            sums: 0,
1609            capacity: 0,
1610        })
1611    }
1612
1613    /// On the default stream so the hash is ordered behind whatever produced the frame; the copy
1614    /// back waits for it.
1615    unsafe fn run(
1616        &mut self,
1617        cuda: &CudaFunctions,
1618        src: CUdeviceptr,
1619        pitch: usize,
1620        width: u32,
1621        height: u32,
1622        rows: u32,
1623    ) -> Option<Vec<u64>> {
1624        let words = self.launch(cuda, src, pitch, width, height, rows)?;
1625        self.read(cuda, words)
1626    }
1627
1628    /// The kernel alone, queued on the default stream behind whatever produced the frame: the
1629    /// words `read` copies back, or None where it did not launch.
1630    unsafe fn launch(
1631        &mut self,
1632        cuda: &CudaFunctions,
1633        src: CUdeviceptr,
1634        pitch: usize,
1635        width: u32,
1636        height: u32,
1637        rows: u32,
1638    ) -> Option<usize> {
1639        let bands = height.div_ceil(rows.max(1)) as usize;
1640        let words = bands * HASH_SEGMENTS as usize;
1641        if words > self.capacity {
1642            if self.sums != 0 {
1643                (cuda.cuMemFree_v2)(self.sums);
1644            }
1645            self.capacity = 0;
1646            if (cuda.cuMemAlloc_v2)(&mut self.sums, words * 8) != CUresult::CUDA_SUCCESS {
1647                self.sums = 0;
1648                return None;
1649            }
1650            self.capacity = words;
1651        }
1652        let (mut src, mut sp, mut out) = (src, pitch as i32, self.sums);
1653        let (mut w, mut h, mut r) = (width as i32, height as i32, rows.max(1) as i32);
1654        let mut params: [*mut c_void; 6] = [
1655            &mut src as *mut _ as *mut c_void,
1656            &mut sp as *mut _ as *mut c_void,
1657            &mut w as *mut _ as *mut c_void,
1658            &mut h as *mut _ as *mut c_void,
1659            &mut r as *mut _ as *mut c_void,
1660            &mut out as *mut _ as *mut c_void,
1661        ];
1662        ((cuda.cuLaunchKernel)(
1663            self.kernel,
1664            bands as u32,
1665            HASH_SEGMENTS,
1666            1,
1667            HASH_THREADS,
1668            1,
1669            1,
1670            0,
1671            ptr::null_mut(),
1672            params.as_mut_ptr(),
1673            ptr::null_mut(),
1674        ) == CUresult::CUDA_SUCCESS)
1675            .then_some(words)
1676    }
1677
1678    /// The hashes a `launch` of `words` left, a word a band: the copy back waits for the kernel.
1679    unsafe fn read(&mut self, cuda: &CudaFunctions, words: usize) -> Option<Vec<u64>> {
1680        let mut sums = vec![0u64; words];
1681        ((cuda.cuMemcpyDtoH_v2)(sums.as_mut_ptr() as *mut c_void, self.sums, words * 8)
1682            == CUresult::CUDA_SUCCESS)
1683            .then(|| {
1684                sums.chunks(HASH_SEGMENTS as usize)
1685                    .map(|band| band.iter().fold(0u64, |a, &b| a.wrapping_add(b)))
1686                    .collect()
1687            })
1688    }
1689
1690    unsafe fn release(self, cuda: &CudaFunctions) {
1691        if self.sums != 0 {
1692            (cuda.cuMemFree_v2)(self.sums);
1693        }
1694        (cuda.cuModuleUnload)(self.module);
1695    }
1696}
1697
1698/// A live NVENC encoder session with its CUDA context and interop resources.
1699///
1700/// One instance owns a CUDA context bound to a specific GPU plus an NVENC session and everything
1701/// the two input paths need:
1702///
1703/// - **Packed path**: a pitched device buffer (`input_device_ptr` / `input_pitch`) registered and
1704///   mapped as the NVENC input (`registered_input_resource` / `mapped_input_buffer`) in the byte
1705///   order `input_format` names (re-registered in place when a source of the other order arrives),
1706///   fed either by a host→device upload or by the copy arm of the dmabuf path.
1707/// - **Zero-copy dmabuf path**: `dmabuf_cache` memoizes each fd's EGLImage → CUDA import, keyed by
1708///   fd but validated against the buffer's `DmaBufIdentity` so a recycled fd re-imports; an import
1709///   whose plane NVENC can take as it is — pitch-linear memory or a packed 8-bit CUDA array — is
1710///   registered in place (`DmaBufInput::Direct`) unless `direct_dmabuf` was switched off, anything
1711///   else is copied into the packed input each frame.
1712///
1713/// `bitstream_buffers` is a ring of `BITSTREAM_BUFFERS` output buffers (`current_buffer_idx`
1714/// cycles it).
1715/// `pinned_hosts` maps each page-locked host upload source's base pointer to its registered length,
1716/// with a `0` length recording a failed registration so that address is never re-pinned.
1717/// `codec` and `fullcolor` name the session's codec and negotiated chroma; `current_qp` tracks the
1718/// live ConstQP so a paint-over reconfigure is skipped when unchanged. `encode_config` and
1719/// `init_params` are retained so in-place reconfigure can resubmit them.
1720/// `omit_stripe_headers` drops the wire
1721/// header, and `node_index` is the effective CUDA device this session is bound to — a reuse across
1722/// captures that now targets a different device must rebuild rather than reconfigure.
1723pub struct NvencEncoder {
1724    encoder_session: *mut c_void,
1725    cuda_context: CUcontext,
1726    cuda_device: CUdevice,
1727    /// The GPU's marketing name, for the one line that says which device encodes.
1728    device_name: String,
1729    egl_display: EGLDisplay,
1730    codec: Codec,
1731    fullcolor: bool,
1732    /// The bits per sample the session codes.
1733    bit_depth: u32,
1734    /// Whether the session was opened asking for 4:4:4 and for 10 bits, which a device without
1735    /// them codes at 4:2:0 and 8: a resize keeps the session for that ask or for what it codes,
1736    /// either of which opens the same session again.
1737    asked: (bool, bool),
1738    width: u32,
1739    height: u32,
1740    current_qp: u32,
1741    /// A capped constant quantizer's peak (`peak_of`), and the rate control it opened with,
1742    /// which a release restores.
1743    peak: Option<PeakBucket>,
1744    open_rc: NV_ENC_RC_PARAMS,
1745    /// The mean quantizer, in the device's own domain, the last frame was coded at; 0 unknown.
1746    last_avg_qp: u32,
1747    /// Whether the last frame was coded bound to the peak (`coded_capped`), and the blocks the
1748    /// next frame holds at the coarsest quantizer (`defer_blocks`).
1749    last_capped: bool,
1750    deferred: Option<crate::ledger::BlockPlan>,
1751    /// The peak's rate, in bit/s, a bind to it was last refused at (`bind_refused`).
1752    refused_bind: Option<u32>,
1753    /// The quality index the next frame is held at whatever the rate control
1754    /// (`hold_quantizer`), the band of it that quantizer covers, and whether the driver has
1755    /// refused one, said once.
1756    held_qp: Option<u32>,
1757    held_band: Option<(f64, f64)>,
1758    /// The `(sliceMode, sliceModeData)` of an HEVC session's frames outside a band frame's,
1759    /// which `set_band_layout` puts back.
1760    slice_layout: (u32, u32),
1761    hold_refused: bool,
1762    /// The bands held in recent frames, by the timestamp the device knows each frame by, and
1763    /// whether a frame that held none left some of them out of every prediction since the
1764    /// cleanup last asked (`take_lost_cleanup`).
1765    band_log: VecDeque<(u64, f64, f64)>,
1766    lost_cleanup: bool,
1767    /// The bytes the next frame may come to (`cap`), and where the last was taken back for coming
1768    /// out past them and coded again, how far its held band then reached, or 0 (`refit`).
1769    cap: Option<usize>,
1770    refit: Option<f64>,
1771    /// Bytes of the last held frame, and the QP delta map a held band is coded through.
1772    held_bytes: usize,
1773    qp_map: Vec<i8>,
1774    /// The blocks the next frame of the rate control's refines (`refine_blocks`), whether the
1775    /// session's rate control takes a QP delta map, and whether the driver refused one, said
1776    /// once, which ends refinement on the session.
1777    refinement: Option<crate::ledger::Refinement>,
1778    delta_mapped: bool,
1779    refine_refused: bool,
1780    /// The quality index the rate control last coded a frame at (held frames aside), from the
1781    /// driver's average quantizer.
1782    last_quality: Option<u32>,
1783    /// Bytes of the last frame the rate control coded, held frames aside.
1784    last_bytes: Option<usize>,
1785    encode_config: NV_ENC_CONFIG,
1786    init_params: NV_ENC_INITIALIZE_PARAMS,
1787    input_device_ptr: CUdeviceptr,
1788    input_pitch: usize,
1789    input_format: NV_ENC_BUFFER_FORMAT,
1790    registered_input_resource: NV_ENC_REGISTERED_PTR,
1791    mapped_input_buffer: NV_ENC_INPUT_PTR,
1792    bitstream_buffers: Vec<NV_ENC_OUTPUT_PTR>,
1793    current_buffer_idx: usize,
1794    dmabuf_cache: HashMap<i32, CachedDmaBuf>,
1795    external_inputs: Vec<ExternalInput>,
1796    pinned_hosts: HashMap<usize, usize>,
1797    cuda: Arc<CudaFunctions>,
1798    /// The EGL entry points a dmabuf import takes, loaded for a session given an EGL display.
1799    egl: Option<Arc<EglFunctions>>,
1800    _nvenc_lib: Arc<NvencLibrary>,
1801    nvenc_funcs: NV_ENCODE_API_FUNCTION_LIST,
1802    omit_stripe_headers: bool,
1803    node_index: i32,
1804    /// Resolved once at init from `PIXELFLUX_NVENC_PIN`: page-lock the host upload sources so the
1805    /// copy is a direct pinned DMA rather than a pageable copy staged through a bounce buffer.
1806    pin_uploads: bool,
1807    /// Resolved once at init from `PIXELFLUX_NVENC_DIRECT`: register pitch-linear dmabuf imports
1808    /// with NVENC in place instead of copying them into the packed input each frame.
1809    direct_dmabuf: bool,
1810    /// The 4:2:0 chroma convert, where the driver took the kernel and the session is not 4:4:4.
1811    /// `None` leaves NVENC's own conversion in place.
1812    csc: Option<ChromaConvert>,
1813    /// The decoded picture buffer the session declares, in frames, which a resize lowers where
1814    /// the new level admits fewer and never raises.
1815    dpb: u32,
1816    /// The device's NVENC engines, which `split_mode` reads at every geometry.
1817    engines: Option<i32>,
1818    /// The frames the decoder holds, so a lost one can be left out of the predictions and each
1819    /// frame can name what it predicts from; None where the device cannot invalidate a
1820    /// reference, and nothing is tracked.
1821    references: Option<ReferenceWindow>,
1822    last_reference: Reference,
1823    /// The H.264 stream's `frame_num` a byte wider, where the session tracks its references: the
1824    /// frame where the counter wraps, which a loss covering it costs a key frame for, comes once
1825    /// in 65536 frames instead of 256.
1826    frame_num: WideFrameNum,
1827    /// The X server's blit semaphore imported into this context (`blit_semaphore_fd`), which
1828    /// `encode_after_blit` queues each frame's wait on; null until one is made.
1829    blit_semaphore: CUexternalSemaphore,
1830    /// The band hash (`band_hashes`), loaded by the first frame that asks for one; `Some(None)`
1831    /// where the driver refused it.
1832    band_hash: Option<Option<BandHash>>,
1833    /// The bands of rows the next host upload is hashed in on the device (`hash_next_upload`),
1834    /// the words that hash left for the frame being coded, and its hashes once read back
1835    /// (`take_upload_hashes`).
1836    upload_hash: Option<u32>,
1837    upload_words: Option<usize>,
1838    upload_hashes: Option<Vec<u64>>,
1839}
1840
1841unsafe impl Send for NvencEncoder {}
1842
1843/// Release every GPU resource the session holds, in the one teardown order the drivers
1844/// tolerate, so nothing leaks and no still-referenced handle is ever freed out from under the
1845/// driver.
1846///
1847/// The whole sequence runs with the owning CUDA context pushed current, because the `cuMemFree` /
1848/// `cuGraphicsUnregisterResource` / `cuMemHostUnregister` calls each act on the *current* context —
1849/// pop it first and the frees silently do nothing, leaking device memory. Within that, resources
1850/// go inner-handle before the outer handle that owns it, since freeing an owner first orphans or
1851/// faults on what still points into it: unmap the packed input before unregistering it, free its
1852/// device buffer, destroy the bitstream buffers, and release every cached dmabuf
1853/// import (`release_dmabuf_import`: its NVENC mapping and registration, then the CUDA resource and
1854/// the EGLImage) — all session-owned — before the encoder session itself, and destroy that
1855/// session before releasing the device's primary CUDA context it was opened against (the retain is
1856/// refcounted, so the context lives until the last session on that device releases it). The
1857/// page-locked host sources are unpinned in the same pass, each only when its recorded length is
1858/// non-zero (a `0` marks a registration that failed and so was never pinned).
1859impl Drop for NvencEncoder {
1860    fn drop(&mut self) {
1861        unsafe {
1862            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
1863
1864            self.unmap_external_inputs();
1865            if let Some(csc) = self.csc.take() {
1866                csc.release(&self.cuda, &self.nvenc_funcs, self.encoder_session);
1867            }
1868            if let Some(Some(hash)) = self.band_hash.take() {
1869                hash.release(&self.cuda);
1870            }
1871            if !self.mapped_input_buffer.is_null() {
1872                (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
1873                    self.encoder_session,
1874                    self.mapped_input_buffer,
1875                );
1876            }
1877            if !self.registered_input_resource.is_null() {
1878                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1879                    self.encoder_session,
1880                    self.registered_input_resource,
1881                );
1882            }
1883            if self.input_device_ptr != 0 {
1884                (self.cuda.cuMemFree_v2)(self.input_device_ptr);
1885            }
1886
1887            for &bs in &self.bitstream_buffers {
1888                (self.nvenc_funcs.nvEncDestroyBitstreamBuffer.unwrap())(self.encoder_session, bs);
1889            }
1890
1891            let imports: Vec<CachedDmaBuf> = self.dmabuf_cache.drain().map(|(_, c)| c).collect();
1892            for cache in imports {
1893                self.release_dmabuf_import(cache);
1894            }
1895
1896            for (base, len) in &self.pinned_hosts {
1897                if *len > 0 {
1898                    (self.cuda.cuMemHostUnregister)(*base as *mut c_void);
1899                }
1900            }
1901
1902            if !self.blit_semaphore.is_null()
1903                && let Some(destroy) = self.cuda.cuDestroyExternalSemaphore
1904            {
1905                destroy(self.blit_semaphore);
1906            }
1907
1908            if !self.encoder_session.is_null() {
1909                (self.nvenc_funcs.nvEncDestroyEncoder.unwrap())(self.encoder_session);
1910            }
1911
1912            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1913            (self.cuda.cuDevicePrimaryCtxRelease_v2)(self.cuda_device);
1914        }
1915    }
1916}
1917
1918/// Hold one reference on `device`'s primary CUDA context for the life of the process, once a
1919/// session on it has opened. A capture restarted while a game fills the GPU's memory then
1920/// retains the context it had instead of creating one, which can fail there and leave the
1921/// stream on the CPU. The cost is the idle context's 106 MiB of VRAM on a device that has
1922/// encoded once, kept also while no session runs. Its own copy of the driver's entry points
1923/// keeps `libcuda` loaded after the last session's copy goes.
1924fn keep_primary_context(device: CUdevice) {
1925    static KEPT: std::sync::Mutex<Vec<CUdevice>> = std::sync::Mutex::new(Vec::new());
1926    static CUDA: std::sync::OnceLock<Option<CudaFunctions>> = std::sync::OnceLock::new();
1927    let mut kept = KEPT.lock().unwrap_or_else(|e| e.into_inner());
1928    if kept.contains(&device) {
1929        return;
1930    }
1931    let Some(cuda) = CUDA.get_or_init(|| NvencEncoder::load_cuda().ok()) else {
1932        return;
1933    };
1934    let mut context: CUcontext = ptr::null_mut();
1935    if unsafe { (cuda.cuDevicePrimaryCtxRetain)(&mut context, device) } == CUresult::CUDA_SUCCESS {
1936        kept.push(device);
1937    }
1938}
1939
1940/// The level an NVENC session advertises for a `width` x `height` stream at `fps` carrying up
1941/// to `bitrate_bps` (`NV_ENC_LEVEL` shares each codec's own numbering: level_idc for H.264,
1942/// general_level_idc for HEVC, seq_level_idx for AV1).
1943///
1944/// H.264 and HEVC carry the current geometry's level, the lowest a decoder is asked to accept,
1945/// so a hardware decoder that gates on it -- older Apple and Intel fixed-function parts refuse
1946/// a stream whose SPS names a level above their ceiling even for a picture they could hold --
1947/// takes the stream. `reconfigure_resolution` re-declares the level with a forced IDR on every
1948/// resize; the frame rate is floored at `HEADROOM_FPS`, so a live rate change between the
1949/// common rates, which carries no IDR, never moves it. AV1 instead holds the resize headroom's
1950/// level: NVENC validates an AV1 session's level against `maxEncodeWidth` x `maxEncodeHeight`
1951/// at init and refuses one that cannot hold it, and an AV1 hardware decoder is recent enough to
1952/// take that level whatever the picture. The driver holds every codec's level to its bitrate
1953/// ceiling as well, refusing a CBR target past it as an invalid level, so a declared rate
1954/// raises the level to the first that admits it; `hevc_high_tier` names the HEVC tier the
1955/// session declares, whose ceiling is the one that applies. Driver 595.71.05 holds an AV1
1956/// level to two thirds of its Annex A MaxBitrate, `NVENC_AV1_RATE` (5.1 26.7 Mbit/s rather
1957/// than 40, on an RTX 4090 and an L4, whatever the frame rate or buffer), where 595.91.07 and
1958/// 615.71.09 hold Annex A's, so a session asks for the Annex A level first and, refused, for
1959/// the level `level_rate` weighs the rate to (`NVENC_AV1_WEIGHTED`). An HEVC picture is counted
1960/// in whole 32-pixel coding tree blocks, NVENC's, as the driver counts it: 1280x720 at 144 fps
1961/// is inside 4.1 by its own samples, and the driver refuses it as 4.1.
1962fn nvenc_level(
1963    codec: Codec,
1964    width: u32,
1965    height: u32,
1966    fps: u32,
1967    bitrate_bps: u64,
1968    hevc_high_tier: bool,
1969) -> u32 {
1970    let fps = fps.max(HEADROOM_FPS);
1971    match codec {
1972        Codec::Av1 => av1_level(
1973            width.max(HEADROOM_WIDTH),
1974            height.max(HEADROOM_HEIGHT),
1975            fps,
1976            bitrate_bps,
1977        ),
1978        Codec::H265 => h265_level(
1979            width.next_multiple_of(32),
1980            height.next_multiple_of(32),
1981            fps,
1982            bitrate_bps,
1983            hevc_high_tier,
1984        ),
1985        _ => h264_level(width, height, fps, bitrate_bps),
1986    }
1987}
1988
1989impl NvencEncoder {
1990    /// Resolve EGL at runtime rather than link against it, so one binary boots even on hosts
1991    /// without EGL — it is needed only by the zero-copy dmabuf path — and reach the
1992    /// `eglCreateImageKHR` / `eglDestroyImageKHR` entry points through `eglGetProcAddress` because
1993    /// they are KHR *extensions* the base `libEGL` is not obliged to export as plain symbols.
1994    /// Erroring when the library or either extension is missing lets the caller fall back to another
1995    /// encoder instead of crashing at the first dmabuf import.
1996    fn load_egl() -> Result<EglFunctions, String> {
1997        unsafe {
1998            let lib_name = "libEGL.so.1";
1999            let lib = Library::new(lib_name)
2000                .or_else(|_| Library::new("libEGL.so"))
2001                .map_err(|e| format!("Could not load EGL library: {}", e))?;
2002
2003            let get_proc_addr_sym: Symbol<unsafe extern "C" fn(*const c_char) -> *mut c_void> = lib
2004                .get(b"eglGetProcAddress\0")
2005                .map_err(|e| format!("Missing symbol eglGetProcAddress: {}", e))?;
2006
2007            let eglGetProcAddress = *get_proc_addr_sym;
2008
2009            let load_extension = |name: &str| -> Result<*mut c_void, String> {
2010                let c_name = CString::new(name).unwrap();
2011                let addr = eglGetProcAddress(c_name.as_ptr());
2012                if addr.is_null() {
2013                    Err(format!("EGL Extension not found: {}", name))
2014                } else {
2015                    Ok(addr)
2016                }
2017            };
2018
2019            let create_addr = load_extension("eglCreateImageKHR")?;
2020            let destroy_addr = load_extension("eglDestroyImageKHR")?;
2021
2022            Ok(EglFunctions {
2023                _lib: lib,
2024                eglGetProcAddress,
2025                eglCreateImageKHR: std::mem::transmute::<*mut c_void, EglCreateImageKhrFn>(
2026                    create_addr,
2027                ),
2028                eglDestroyImageKHR: std::mem::transmute::<*mut c_void, EglDestroyImageKhrFn>(
2029                    destroy_addr,
2030                ),
2031            })
2032        }
2033    }
2034
2035    /// Resolve the CUDA driver library (`libcuda.so.1`, or `nvcuda.dll` on Windows) at
2036    /// runtime so the crate links against no CUDA SDK and still runs wherever a driver is installed,
2037    /// binding every `cu*` entry point up front so the per-frame hot path is plain indirect calls
2038    /// with no repeated symbol lookups. A missing symbol errors with its name, turning an ABI
2039    /// mismatch into a legible message instead of a later null-pointer call.
2040    fn load_cuda() -> Result<CudaFunctions, String> {
2041        unsafe {
2042            let lib_name = if cfg!(windows) {
2043                "nvcuda.dll"
2044            } else {
2045                "libcuda.so.1"
2046            };
2047            let lib = Library::new(lib_name)
2048                .map_err(|e| format!("Could not load CUDA library ({}): {}", lib_name, e))?;
2049
2050            macro_rules! load {
2051                ($lib:expr, $name:expr) => {
2052                    *$lib.get($name).map_err(|e| {
2053                        format!(
2054                            "Missing symbol {}: {}",
2055                            std::str::from_utf8($name).unwrap(),
2056                            e
2057                        )
2058                    })?
2059                };
2060            }
2061
2062            Ok(CudaFunctions {
2063                cuInit: load!(lib, b"cuInit\0"),
2064                cuDeviceGet: load!(lib, b"cuDeviceGet\0"),
2065                cuDeviceGetByPCIBusId: load!(lib, b"cuDeviceGetByPCIBusId\0"),
2066                cuDevicePrimaryCtxRetain: load!(lib, b"cuDevicePrimaryCtxRetain\0"),
2067                cuCtxPushCurrent_v2: load!(lib, b"cuCtxPushCurrent_v2\0"),
2068                cuCtxPopCurrent_v2: load!(lib, b"cuCtxPopCurrent_v2\0"),
2069                cuDevicePrimaryCtxRelease_v2: load!(lib, b"cuDevicePrimaryCtxRelease_v2\0"),
2070                cuMemAlloc_v2: load!(lib, b"cuMemAlloc_v2\0"),
2071                cuMemAllocPitch_v2: load!(lib, b"cuMemAllocPitch_v2\0"),
2072                cuMemFree_v2: load!(lib, b"cuMemFree_v2\0"),
2073                cuMemcpyHtoD_v2: load!(lib, b"cuMemcpyHtoD_v2\0"),
2074                cuMemcpyDtoH_v2: load!(lib, b"cuMemcpyDtoH_v2\0"),
2075                cuMemcpy2D_v2: load!(lib, b"cuMemcpy2D_v2\0"),
2076                cuMemcpy2DAsync_v2: load!(lib, b"cuMemcpy2DAsync_v2\0"),
2077                cuStreamSynchronize: load!(lib, b"cuStreamSynchronize\0"),
2078                cuModuleLoadData: load!(lib, b"cuModuleLoadData\0"),
2079                cuModuleGetFunction: load!(lib, b"cuModuleGetFunction\0"),
2080                cuModuleUnload: load!(lib, b"cuModuleUnload\0"),
2081                cuTexObjectCreate: load!(lib, b"cuTexObjectCreate\0"),
2082                cuTexObjectDestroy: load!(lib, b"cuTexObjectDestroy\0"),
2083                cuLaunchKernel: load!(lib, b"cuLaunchKernel\0"),
2084                cuMemHostRegister_v2: load!(lib, b"cuMemHostRegister_v2\0"),
2085                cuMemHostUnregister: load!(lib, b"cuMemHostUnregister\0"),
2086                cuGraphicsEGLRegisterImage: load!(lib, b"cuGraphicsEGLRegisterImage\0"),
2087                cuGraphicsUnregisterResource: load!(lib, b"cuGraphicsUnregisterResource\0"),
2088                cuGraphicsResourceGetMappedEglFrame: load!(
2089                    lib,
2090                    b"cuGraphicsResourceGetMappedEglFrame\0"
2091                ),
2092                cuDeviceGetCount: load!(lib, b"cuDeviceGetCount\0"),
2093                cuDeviceGetName: load!(lib, b"cuDeviceGetName\0"),
2094                cuDeviceGetUuid: load!(lib, b"cuDeviceGetUuid\0"),
2095                cuGetErrorName: load!(lib, b"cuGetErrorName\0"),
2096                cuImportExternalSemaphore: lib.get(b"cuImportExternalSemaphore\0").ok().map(|s| *s),
2097                cuWaitExternalSemaphoresAsync: lib
2098                    .get(b"cuWaitExternalSemaphoresAsync\0")
2099                    .ok()
2100                    .map(|s| *s),
2101                cuDestroyExternalSemaphore: lib
2102                    .get(b"cuDestroyExternalSemaphore\0")
2103                    .ok()
2104                    .map(|s| *s),
2105                _lib: lib,
2106            })
2107        }
2108    }
2109
2110    /// Resolve `libnvidia-encode` at runtime for the same reason as CUDA — no SDK to link,
2111    /// runs against whatever driver ships — binding `NvEncodeAPICreateInstance` as the sole entry
2112    /// point every later encode call is reached through. `NvEncodeAPIGetMaxSupportedVersion` is kept
2113    /// optional and bound only when present, because very old drivers lack it; negotiation then falls
2114    /// back to probing `create_instance` acceptance directly rather than failing to load.
2115    fn load_nvenc() -> Result<NvencLibrary, String> {
2116        unsafe {
2117            let lib_name = NVENC_DLL_NAME;
2118            let lib = Library::new(lib_name)
2119                .map_err(|e| format!("Could not load NVENC library ({}): {}", lib_name, e))?;
2120
2121            let create_instance = *lib
2122                .get(NV_ENCODE_API_CREATE_INSTANCE_FN_NAME)
2123                .map_err(|e| e.to_string())?;
2124            let get_max_version = lib
2125                .get::<NvEncodeApiGetMaxSupportedVersionFn>(
2126                    NV_ENCODE_API_GET_MAX_SUPPORTED_VERSION_FN_NAME,
2127                )
2128                .map(|s| *s)
2129                .ok();
2130            Ok(NvencLibrary {
2131                create_instance,
2132                get_max_version,
2133                _lib: lib,
2134            })
2135        }
2136    }
2137
2138    /// Turn a `CUresult` into the driver's own error name via `cuGetErrorName` so a failure
2139    /// logs something diagnosable (e.g. `CUDA_ERROR_OUT_OF_MEMORY`) instead of a bare integer,
2140    /// falling back to the numeric code only when the name is unavailable.
2141    unsafe fn get_error_string(cuda: &CudaFunctions, err: CUresult) -> String {
2142        let mut p_str: *const c_char = ptr::null();
2143        if (cuda.cuGetErrorName)(err, &mut p_str) == CUresult::CUDA_SUCCESS && !p_str.is_null() {
2144            CStr::from_ptr(p_str).to_string_lossy().into_owned()
2145        } else {
2146            format!("Unknown CUDA Error ({})", err.0)
2147        }
2148    }
2149
2150    /// Log the CUDA devices CUDA can enumerate — a debug aid when session init fails to find
2151    /// or bind the expected GPU.
2152    unsafe fn probe_devices(cuda: &CudaFunctions) {
2153        let mut count = 0;
2154        if (cuda.cuDeviceGetCount)(&mut count) != CUresult::CUDA_SUCCESS {
2155            return;
2156        }
2157        crate::log::debug!("[NVENC] Found {} CUDA devices:", count);
2158        for i in 0..count {
2159            let mut dev = 0;
2160            (cuda.cuDeviceGet)(&mut dev, i);
2161            crate::log::debug!(
2162                "[NVENC]   Device {}: {}",
2163                i,
2164                Self::device_name_of(cuda, dev)
2165            );
2166        }
2167    }
2168
2169    /// The name CUDA gives a device, or a placeholder when it will not say.
2170    unsafe fn device_name_of(cuda: &CudaFunctions, dev: CUdevice) -> String {
2171        let mut name_buf = [0 as c_char; 256];
2172        if (cuda.cuDeviceGetName)(name_buf.as_mut_ptr(), 256, dev) != CUresult::CUDA_SUCCESS {
2173            return format!("CUDA device {dev}");
2174        }
2175        CStr::from_ptr(name_buf.as_ptr())
2176            .to_string_lossy()
2177            .into_owned()
2178    }
2179
2180    /// The PCI bus ID of the GPU behind `/dev/dri/renderD<128+index>`, read from the sysfs
2181    /// device symlink, so CUDA can bind to the same physical GPU the capture render node lives on.
2182    fn get_pci_bus_id(render_index: i32) -> Option<String> {
2183        let path = format!("/sys/class/drm/renderD{}/device", 128 + render_index);
2184        if let Ok(target) = std::fs::read_link(&path)
2185            && let Some(name) = target.file_name()
2186            && let Some(name_str) = name.to_str()
2187        {
2188            return Some(name_str.to_string());
2189        }
2190        None
2191    }
2192
2193    /// Build a live NVENC session for the settings' codec: bind CUDA to the target GPU, open and
2194    /// configure the encoder, and allocate its input and output buffers.
2195    ///
2196    /// The sequence:
2197    ///
2198    /// 1. **Load and negotiate**: dlopen EGL / CUDA / NVENC, then `nvenc_negotiate` resolves the API
2199    ///    version against the driver (set-once) before any struct is version-tagged. The multi-GPU
2200    ///    `GET_ATTACHED_IDS` ioctl filter is installed after the NVIDIA libraries are loaded — so
2201    ///    their GOTs can be patched — and before `cuInit` enumerates devices (a no-op unless a host
2202    ///    GPU is hidden from this container). The three library `Arc`s are leaked once per process so
2203    ///    the resolved function pointers stay valid for the program's life.
2204    /// 2. **Bind the device**: `cuInit`, then bind by the render node's PCI bus ID
2205    ///    (`encode_node_index`, with auto `<0` meaning device 0), falling back to CUDA device 0, and
2206    ///    retain the device's primary CUDA context — shared and refcounted across every session on
2207    ///    that device rather than a fresh 100-300 MiB context each — pushing it current.
2208    /// 3. **Allocate input**: a pitched ARGB device buffer (`cuMemAllocPitch`, 16-byte element
2209    ///    alignment) that the chroma convert, or hardware CSC, turns into YUV.
2210    /// 4. **Open the session and query caps**: create the function-list instance, open the session
2211    ///    with the negotiated `apiVersion`, refuse a codec the device lists no engine for, and
2212    ///    query `nvEncGetEncodeCaps` so init degrades rather than fails — a 4:4:4 request on a GPU
2213    ///    or codec without it drops to 4:2:0, and a capture beyond the encoder's max dimensions
2214    ///    returns `Err` so the caller falls back to software. Then pull a preset config (P3,
2215    ///    ultra-low-latency); a failed preset lookup logs the driver's error string and proceeds
2216    ///    with the zeroed default rather than aborting.
2217    /// 5. **Configure the stream** (mutating the returned preset config, whose `version` word is
2218    ///    re-stamped while its embedded `rcParams` keeps the version the preset fill set): the
2219    ///    codec's 4:2:0 or 4:4:4 profile; CBR (two-pass quarter-resolution for tighter per-frame
2220    ///    rate adherence, VBV sizing, optional min/max QP clamps in the codec's quantizer domain)
2221    ///    or ConstQP; infinite GOP (`gopLength` / `idrPeriod` = `0xFFFFFFFF`); `zeroReorderDelay`
2222    ///    and, for H.264, a bitstream-restriction VUI (`max_num_reorder_frames=0`) so no-reorder
2223    ///    decoders don't buffer; an explicit level from `nvenc_level` pinned from frame 1 so the
2224    ///    level never bumps mid-stream; BT.709 primaries and transfer for the sRGB source, at
2225    ///    limited range, with the matrix whichever conversion the session uses produces;
2226    ///    a decoded picture buffer of as many frames as the level admits, so a frame a client
2227    ///    lost can be left out of the predictions (`invalidate_reference`) with earlier frames
2228    ///    still there to predict from;
2229    ///    repeated parameter sets on every key frame; H.264 CABAC; no AUD; one AV1 tile; strict
2230    ///    GOP target; and lookahead disabled for real-time latency.
2231    /// 6. **Initialize with resize headroom**: `maxEncodeWidth` / `maxEncodeHeight` are raised to
2232    ///    at least `HEADROOM_WIDTH` x `HEADROOM_HEIGHT` so `reconfigure_resolution` can grow in
2233    ///    place, but never past the driver's reported maximum; this costs a few hundred MiB of
2234    ///    device memory, so a failed init reopens the session and retries at the exact size
2235    ///    (in-place resize then falls back to a rebuild).
2236    /// 7. **Register, map, and buffer**: register and map the packed input surface (as `ARGB`;
2237    ///    `set_input_format` re-registers it for an RGBA source), and create a
2238    ///    4-deep ring of bitstream output buffers.
2239    ///
2240    /// Every failure after the CUDA allocation unwinds the resources created so far — buffers,
2241    /// session, context — before returning `Err`. EGL is only needed by the zero-copy dmabuf path,
2242    /// so callers on the host-ARGB path pass a null `egl_display`. The retained
2243    /// `init_params.encodeConfig` raw pointer is nulled before the struct is returned (it points at
2244    /// a local `config` about to move); the reconfigure paths repoint it at `self.encode_config`
2245    /// when they resubmit.
2246    pub fn new(settings: &CaptureSettings, egl_display: *const c_void) -> Result<Self, String> {
2247        Self::new_tuned(settings, egl_display, NvencTuning::default())
2248    }
2249
2250    /// `new` with the preset, rate-control passes, and adaptive quantization named, for the
2251    /// tuning bench; production sessions take `NvencTuning::default`.
2252    pub(crate) fn new_tuned(
2253        settings: &CaptureSettings,
2254        egl_display: *const c_void,
2255        tuning: NvencTuning,
2256    ) -> Result<Self, String> {
2257        let codec = settings.codec;
2258        let codec_guid =
2259            codec_guid(codec).ok_or_else(|| format!("NVENC has no {} encoder", codec.display()))?;
2260        crate::log::debug!("[NVENC] Initializing {}...", codec.display());
2261
2262        let egl = if egl_display.is_null() {
2263            None
2264        } else {
2265            Some(Arc::new(Self::load_egl()?))
2266        };
2267        let cuda = Arc::new(Self::load_cuda()?);
2268        let nvenc_lib = Arc::new(Self::load_nvenc()?);
2269        nvenc_negotiate(&nvenc_lib);
2270
2271        crate::nvgpufilter::install();
2272
2273        static LEAK_ONCE: std::sync::Once = std::sync::Once::new();
2274        LEAK_ONCE.call_once(|| {
2275            std::mem::forget(egl.clone());
2276            std::mem::forget(cuda.clone());
2277            std::mem::forget(nvenc_lib.clone());
2278        });
2279
2280        unsafe {
2281            let res = (cuda.cuInit)(0);
2282            if res != CUresult::CUDA_SUCCESS {
2283                return Err(format!(
2284                    "Init CUDA failed: {}",
2285                    Self::get_error_string(&cuda, res)
2286                ));
2287            }
2288
2289            Self::probe_devices(&cuda);
2290
2291            let mut cu_device: CUdevice = 0;
2292            let mut device_found = false;
2293
2294            if let Some(pci_bus_id) = Self::get_pci_bus_id(settings.encode_node_index.max(0)) {
2295                let c_pci_bus_id = CString::new(pci_bus_id.clone()).unwrap();
2296                if (cuda.cuDeviceGetByPCIBusId)(&mut cu_device, c_pci_bus_id.as_ptr())
2297                    == CUresult::CUDA_SUCCESS
2298                {
2299                    crate::log::debug!(
2300                        "[NVENC] Bound to CUDA device via PCI Bus ID: {}",
2301                        pci_bus_id
2302                    );
2303                    device_found = true;
2304                }
2305            }
2306
2307            if !device_found {
2308                let res = (cuda.cuDeviceGet)(&mut cu_device, 0);
2309                if res != CUresult::CUDA_SUCCESS {
2310                    return Err("Failed to get default CUDA device".into());
2311                }
2312            }
2313            let device_name = Self::device_name_of(&cuda, cu_device);
2314
2315            // One primary context per device, shared and refcounted across every session on that
2316            // device, rather than a fresh 100-300 MiB context each: a second display or a rebuild
2317            // retains the same context instead of allocating another. Retain does not make it
2318            // current, so it is pushed here to run the allocations below and left current for the
2319            // encode paths — matching the current-context state the removed cuCtxCreate produced.
2320            let mut cu_context: CUcontext = ptr::null_mut();
2321            let res = (cuda.cuDevicePrimaryCtxRetain)(&mut cu_context, cu_device);
2322            if res != CUresult::CUDA_SUCCESS {
2323                return Err("Failed to retain the device's primary CUDA context".into());
2324            }
2325            if (cuda.cuCtxPushCurrent_v2)(cu_context) != CUresult::CUDA_SUCCESS {
2326                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2327                return Err("Failed to make the primary CUDA context current".into());
2328            }
2329
2330            let width = settings.width as u32;
2331            let height = settings.height as u32;
2332            let mut input_device_ptr: CUdeviceptr = 0;
2333            let mut input_pitch: usize = 0;
2334
2335            let res = (cuda.cuMemAllocPitch_v2)(
2336                &mut input_device_ptr,
2337                &mut input_pitch,
2338                (width * 4) as usize,
2339                height as usize,
2340                16,
2341            );
2342            if res != CUresult::CUDA_SUCCESS {
2343                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2344                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2345                return Err("Failed to allocate ARGB input buffer on GPU".into());
2346            }
2347
2348            let mut function_list = NV_ENCODE_API_FUNCTION_LIST {
2349                version: sv(NvStruct::FunctionList),
2350                ..Default::default()
2351            };
2352            if (nvenc_lib.create_instance)(&mut function_list) != NVENCSTATUS::NV_ENC_SUCCESS {
2353                (cuda.cuMemFree_v2)(input_device_ptr);
2354                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2355                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2356                return Err("NvEncodeAPICreateInstance failed".into());
2357            }
2358
2359            let mut session_params = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
2360                version: sv(NvStruct::OpenSessionExParams),
2361                deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
2362                device: cu_context as *mut c_void,
2363                apiVersion: neg_api(),
2364                ..Default::default()
2365            };
2366
2367            let mut encoder_session: *mut c_void = ptr::null_mut();
2368            let open_fn = function_list.nvEncOpenEncodeSessionEx.unwrap();
2369            let status = open_fn(&mut session_params, &mut encoder_session);
2370            if status != NVENCSTATUS::NV_ENC_SUCCESS {
2371                (cuda.cuMemFree_v2)(input_device_ptr);
2372                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2373                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2374                return Err(session_refusal(status));
2375            }
2376
2377            // The device's engine list decides whether the codec exists here at all (AV1
2378            // arrived with Ada), and a refusal has to name that rather than fail an init.
2379            if !Self::device_encodes(&function_list, encoder_session, &codec_guid) {
2380                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2381                (cuda.cuMemFree_v2)(input_device_ptr);
2382                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2383                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2384                return Err(format!(
2385                    "this GPU's NVENC has no {} engine",
2386                    codec.display()
2387                ));
2388            }
2389
2390            // Query caps so init degrades instead of failing opaquely: a 4:4:4 request on a GPU
2391            // or codec without it drops to 4:2:0, and a capture beyond the encoder's max
2392            // dimensions declines NVENC so the caller falls back to software.
2393            let caps_444 = if codec.fullcolor() {
2394                query_cap(
2395                    &function_list,
2396                    encoder_session,
2397                    codec_guid,
2398                    NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE,
2399                )
2400            } else {
2401                Some(0)
2402            };
2403            let caps_wmax = query_cap(
2404                &function_list,
2405                encoder_session,
2406                codec_guid,
2407                NV_ENC_CAPS::NV_ENC_CAPS_WIDTH_MAX,
2408            );
2409            let caps_hmax = query_cap(
2410                &function_list,
2411                encoder_session,
2412                codec_guid,
2413                NV_ENC_CAPS::NV_ENC_CAPS_HEIGHT_MAX,
2414            );
2415            let caps = decide_caps(
2416                settings.video_fullcolor,
2417                width as i32,
2418                height as i32,
2419                caps_444,
2420                caps_wmax,
2421                caps_hmax,
2422            );
2423            if let Some((mw, mh)) = caps.too_large {
2424                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2425                (cuda.cuMemFree_v2)(input_device_ptr);
2426                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2427                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2428                return Err(format!(
2429                    "NVENC maximum encode size {mw}x{mh} exceeded by {width}x{height}; using software"
2430                ));
2431            }
2432            if caps.downgraded_color && super::software_fullcolor(codec) {
2433                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2434                (cuda.cuMemFree_v2)(input_device_ptr);
2435                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2436                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2437                return Err(format!(
2438                    "this GPU's NVENC engine encodes no {} 4:4:4, which the software encoder does",
2439                    codec.display()
2440                ));
2441            }
2442            if caps.downgraded_color {
2443                eprintln!(
2444                    "[NVENC] {} 4:4:4 (YUV444) encoding unsupported on this GPU; encoding 4:2:0.",
2445                    codec.display()
2446                );
2447            }
2448            if tuning.temporal_aq
2449                && query_cap(
2450                    &function_list,
2451                    encoder_session,
2452                    codec_guid,
2453                    NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_TEMPORAL_AQ,
2454                ) == Some(0)
2455            {
2456                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2457                (cuda.cuMemFree_v2)(input_device_ptr);
2458                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2459                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2460                return Err(format!(
2461                    "{} temporal AQ is not supported by this GPU",
2462                    codec.display()
2463                ));
2464            }
2465
2466            let is_444 = caps.fullcolor;
2467            let bit_depth = if settings.video_bit_depth >= 10
2468                && codes_ten_bit(&function_list, encoder_session, codec, codec_guid)
2469            {
2470                10
2471            } else {
2472                8
2473            };
2474            let invalidation = query_cap(
2475                &function_list,
2476                encoder_session,
2477                codec_guid,
2478                NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_REF_PIC_INVALIDATION,
2479            ) == Some(1);
2480
2481            let mut config = NV_ENC_CONFIG {
2482                version: sv(NvStruct::Config),
2483                ..Default::default()
2484            };
2485            let mut preset_config = NV_ENC_PRESET_CONFIG {
2486                version: sv(NvStruct::PresetConfig),
2487                presetCfg: config,
2488                ..Default::default()
2489            };
2490
2491            let get_preset_ex = function_list.nvEncGetEncodePresetConfigEx.unwrap();
2492            let preset_status = get_preset_ex(
2493                encoder_session,
2494                codec_guid,
2495                tuning.preset,
2496                NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
2497                &mut preset_config,
2498            );
2499            if preset_status != NVENCSTATUS::NV_ENC_SUCCESS {
2500                eprintln!(
2501                    "[NVENC] nvEncGetEncodePresetConfigEx failed ({preset_status:?}): {}",
2502                    last_error(&function_list, encoder_session)
2503                );
2504            }
2505
2506            config = preset_config.presetCfg;
2507            config.version = sv(NvStruct::Config);
2508            config.profileGUID = profile_guid(codec, is_444, bit_depth);
2509            if settings.video_cbr_mode {
2510                let bps = cbr_bps(settings);
2511                log_held_rate(settings, bps);
2512                config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
2513                config.rcParams.multiPass = if tuning.multipass
2514                    == NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION
2515                    && NVENC_SINGLE_PASS.load(Ordering::Relaxed)
2516                {
2517                    NV_ENC_MULTI_PASS::NV_ENC_MULTI_PASS_DISABLED
2518                } else {
2519                    tuning.multipass
2520                };
2521                set_cbr_rate(&mut config.rcParams, bps, cbr_vbv(settings, bps));
2522                let lo = codec.hardware_quantizer_bound(Hardware::Nvenc, settings.video_min_qp);
2523                if lo > 0 {
2524                    config.rcParams.set_enableMinQP(1);
2525                    config.rcParams.minQP.qpInterP = lo;
2526                    config.rcParams.minQP.qpInterB = lo;
2527                    config.rcParams.minQP.qpIntra = lo;
2528                }
2529                let hi = codec.hardware_quantizer_bound(Hardware::Nvenc, settings.video_max_qp);
2530                if hi > 0 {
2531                    config.rcParams.set_enableMaxQP(1);
2532                    config.rcParams.maxQP.qpInterP = hi;
2533                    config.rcParams.maxQP.qpInterB = hi;
2534                    config.rcParams.maxQP.qpIntra = hi;
2535                }
2536            } else {
2537                let q = codec.hardware_quantizer(Hardware::Nvenc, settings.video_crf);
2538                config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
2539                config.rcParams.constQP.qpInterP = q;
2540                config.rcParams.constQP.qpInterB = q;
2541                config.rcParams.constQP.qpIntra = q;
2542            }
2543            config.rcParams.set_enableAQ(u32::from(tuning.spatial_aq));
2544            config
2545                .rcParams
2546                .set_enableTemporalAQ(u32::from(tuning.temporal_aq));
2547            config.frameIntervalP = 1;
2548            config.gopLength = 0xFFFFFFFF;
2549            config.rcParams.set_zeroReorderDelay(1);
2550            config.rcParams.set_strictGOPTarget(1);
2551            config.rcParams.set_enableLookahead(0);
2552            config.rcParams.lookaheadDepth = 0;
2553            let rate = FrameRate::of(settings.target_fps);
2554            let level_at = |weighted| {
2555                nvenc_level(
2556                    codec,
2557                    width,
2558                    height,
2559                    rate.ceil(),
2560                    level_rate(codec, u64::from(config.rcParams.maxBitRate), weighted),
2561                    tuning.hevc_high_tier,
2562                )
2563            };
2564            let level = level_at(NVENC_AV1_WEIGHTED.load(Ordering::Relaxed));
2565            let weighted_level = level_at(true);
2566            let dpb = match codec {
2567                Codec::H265 => h265_dpb_frames(level, width, height),
2568                Codec::Av1 => AV1_REFERENCES,
2569                _ => h264_dpb_frames(level, width, height),
2570            };
2571            let dpb = if codec == Codec::Av1 {
2572                dpb
2573            } else {
2574                dpb.min(crate::encoders::reference_frames(settings))
2575            };
2576            let ltr = query_cap(
2577                &function_list,
2578                encoder_session,
2579                codec_guid,
2580                NV_ENC_CAPS::NV_ENC_CAPS_NUM_MAX_LTR_FRAMES,
2581            );
2582            let anchors = anchor_count(
2583                codec,
2584                nvenc_cur_ver().0,
2585                invalidation,
2586                ltr,
2587                dpb,
2588                settings.acknowledge_references,
2589            );
2590            Self::configure_codec(
2591                &mut config,
2592                codec,
2593                is_444,
2594                bit_depth,
2595                level,
2596                dpb,
2597                anchors,
2598                &tuning,
2599            );
2600            #[cfg(test)]
2601            if let Some(refs) = tuning.ref_l0 {
2602                match codec {
2603                    Codec::H265 => config.encodeCodecConfig.hevcConfig.numRefL0 = refs,
2604                    Codec::Av1 => config.encodeCodecConfig.av1Config.numFwdRefs = refs,
2605                    _ => config.encodeCodecConfig.h264Config.numRefL0 = refs,
2606                }
2607            }
2608            let engines = query_cap(
2609                &function_list,
2610                encoder_session,
2611                codec_guid,
2612                NV_ENC_CAPS::NV_ENC_CAPS_NUM_ENCODER_ENGINES,
2613            );
2614            let split = split_mode(codec, width, height, engines, nvenc_cur_ver());
2615            #[cfg(test)]
2616            let split = tuning.split.unwrap_or(split);
2617
2618            let mut init_params = NV_ENC_INITIALIZE_PARAMS {
2619                version: sv(NvStruct::InitializeParams),
2620                encodeGUID: codec_guid,
2621                presetGUID: tuning.preset,
2622                tuningInfo: NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
2623                encodeWidth: width,
2624                encodeHeight: height,
2625                darWidth: width,
2626                darHeight: height,
2627                frameRateNum: rate.num,
2628                frameRateDen: rate.den,
2629                enablePTD: 1,
2630                encodeConfig: &mut config,
2631                maxEncodeWidth: nvenc_headroom(width, HEADROOM_WIDTH, caps_wmax),
2632                maxEncodeHeight: nvenc_headroom(height, HEADROOM_HEIGHT, caps_hmax),
2633                ..Default::default()
2634            };
2635            init_params.set_splitEncodeMode(split as u32);
2636
2637            let init_fn = function_list.nvEncInitializeEncoder.unwrap();
2638            let mut headroom_status =
2639                init_fn(encoder_session, &mut Negotiated::new(init_params).value);
2640            if headroom_status != NVENCSTATUS::NV_ENC_SUCCESS && weighted_level != level {
2641                // A driver holding AV1 to `NVENC_AV1_RATE` refuses the Annex A level as invalid,
2642                // and a refused encoder stays refused: the weighted level opens a session of its
2643                // own.
2644                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2645                encoder_session = ptr::null_mut();
2646                if open_fn(&mut session_params, &mut encoder_session) != NVENCSTATUS::NV_ENC_SUCCESS
2647                {
2648                    (cuda.cuMemFree_v2)(input_device_ptr);
2649                    (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2650                    (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2651                    return Err("Failed to reopen the AV1 session at the weighted level".into());
2652                }
2653                config.encodeCodecConfig.av1Config.level = weighted_level;
2654                init_params.encodeConfig = &mut config;
2655                headroom_status = init_fn(encoder_session, &mut Negotiated::new(init_params).value);
2656                if headroom_status == NVENCSTATUS::NV_ENC_SUCCESS {
2657                    NVENC_AV1_WEIGHTED.store(true, Ordering::Relaxed);
2658                    eprintln!(
2659                        "[NVENC] The driver refused AV1 level {level} at {} kbps; declaring {weighted_level}",
2660                        config.rcParams.maxBitRate / 1000
2661                    );
2662                }
2663            }
2664            if headroom_status == NVENCSTATUS::NV_ENC_ERR_INVALID_PARAM
2665                && config.rcParams.multiPass
2666                    == NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION
2667            {
2668                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2669                encoder_session = ptr::null_mut();
2670                if open_fn(&mut session_params, &mut encoder_session) != NVENCSTATUS::NV_ENC_SUCCESS
2671                {
2672                    (cuda.cuMemFree_v2)(input_device_ptr);
2673                    (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2674                    (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2675                    return Err("Failed to reopen the session for a single pass".into());
2676                }
2677                config.rcParams.multiPass = NV_ENC_MULTI_PASS::NV_ENC_MULTI_PASS_DISABLED;
2678                init_params.encodeConfig = &mut config;
2679                headroom_status = init_fn(encoder_session, &mut Negotiated::new(init_params).value);
2680                if headroom_status == NVENCSTATUS::NV_ENC_SUCCESS
2681                    && !NVENC_SINGLE_PASS.swap(true, Ordering::Relaxed)
2682                {
2683                    eprintln!(
2684                        "[NVENC] The driver refused the quarter-resolution first pass; coding CBR in a single pass"
2685                    );
2686                }
2687            }
2688            if headroom_status != NVENCSTATUS::NV_ENC_SUCCESS {
2689                eprintln!(
2690                    "[NVENC] Init with {}x{} resize headroom failed ({headroom_status:?}): {}",
2691                    init_params.maxEncodeWidth,
2692                    init_params.maxEncodeHeight,
2693                    last_error(&function_list, encoder_session)
2694                );
2695                // An encoder the driver refused to initialize stays refused, so the retry at the
2696                // exact capture size needs a session of its own rather than this one.
2697                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2698                encoder_session = ptr::null_mut();
2699                let reopened = open_fn(&mut session_params, &mut encoder_session)
2700                    == NVENCSTATUS::NV_ENC_SUCCESS;
2701                init_params.maxEncodeWidth = width;
2702                init_params.maxEncodeHeight = height;
2703                init_params.encodeConfig = &mut config;
2704                let exact_status = if reopened {
2705                    init_fn(encoder_session, &mut Negotiated::new(init_params).value)
2706                } else {
2707                    NVENCSTATUS::NV_ENC_ERR_NO_ENCODE_DEVICE
2708                };
2709                if exact_status != NVENCSTATUS::NV_ENC_SUCCESS {
2710                    let detail = if reopened {
2711                        let d = last_error(&function_list, encoder_session);
2712                        (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2713                        d
2714                    } else {
2715                        "could not reopen the session".to_string()
2716                    };
2717                    (cuda.cuMemFree_v2)(input_device_ptr);
2718                    (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2719                    (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2720                    return Err(format!(
2721                        "Failed to initialize {} encoder at {width}x{height} ({exact_status:?}): {detail}",
2722                        codec.display()
2723                    ));
2724                }
2725                eprintln!("[NVENC] Running without resize headroom.");
2726            }
2727
2728            init_params.encodeConfig = ptr::null_mut();
2729
2730            let mut reg_res = NV_ENC_REGISTER_RESOURCE {
2731                version: sv(NvStruct::RegisterResource),
2732                resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
2733                width,
2734                height,
2735                resourceToRegister: input_device_ptr as *mut c_void,
2736                pitch: input_pitch as u32,
2737                bufferFormat: NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB,
2738                bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
2739                ..Default::default()
2740            };
2741
2742            let register_fn = function_list.nvEncRegisterResource.unwrap();
2743            if register_fn(encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
2744                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2745                (cuda.cuMemFree_v2)(input_device_ptr);
2746                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2747                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2748                return Err("Failed to register input buffer".into());
2749            }
2750
2751            let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
2752                version: sv(NvStruct::MapInputResource),
2753                registeredResource: reg_res.registeredResource,
2754                ..Default::default()
2755            };
2756            let map_fn = function_list.nvEncMapInputResource.unwrap();
2757            if map_fn(encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
2758                (function_list.nvEncUnregisterResource.unwrap())(
2759                    encoder_session,
2760                    reg_res.registeredResource,
2761                );
2762                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2763                (cuda.cuMemFree_v2)(input_device_ptr);
2764                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2765                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2766                return Err("Failed to map input buffer".into());
2767            }
2768
2769            let mut bitstream_buffers = Vec::new();
2770            let create_bs_fn = function_list.nvEncCreateBitstreamBuffer.unwrap();
2771            for _ in 0..BITSTREAM_BUFFERS {
2772                let mut bitstream_params = NV_ENC_CREATE_BITSTREAM_BUFFER {
2773                    version: sv(NvStruct::CreateBitstreamBuffer),
2774                    ..Default::default()
2775                };
2776                if create_bs_fn(encoder_session, &mut bitstream_params)
2777                    != NVENCSTATUS::NV_ENC_SUCCESS
2778                {
2779                    for &bs in &bitstream_buffers {
2780                        (function_list.nvEncDestroyBitstreamBuffer.unwrap())(encoder_session, bs);
2781                    }
2782                    (function_list.nvEncUnmapInputResource.unwrap())(
2783                        encoder_session,
2784                        map_params.mappedResource,
2785                    );
2786                    (function_list.nvEncUnregisterResource.unwrap())(
2787                        encoder_session,
2788                        reg_res.registeredResource,
2789                    );
2790                    (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2791                    (cuda.cuMemFree_v2)(input_device_ptr);
2792                    (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2793                    (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2794                    return Err("Failed to create bitstream buffer".into());
2795                }
2796                bitstream_buffers.push(bitstream_params.bitstreamBuffer);
2797            }
2798
2799            // 8-bit 4:4:4 subsamples nothing and needs no kernel; a driver that refuses the
2800            // 4:2:0 one keeps NVENC's own conversion, which follows the declared matrix either
2801            // way. A 10-bit session was initialized for the 10-bit surface only the kernel
2802            // writes.
2803            let layout = ConvertLayout::of(is_444, bit_depth);
2804            #[cfg(test)]
2805            let layout = layout.filter(|_| !tuning.hardware_csc);
2806            let csc = layout.and_then(|layout| {
2807                ChromaConvert::new(
2808                    &cuda,
2809                    &function_list,
2810                    encoder_session,
2811                    width,
2812                    height,
2813                    layout,
2814                )
2815            });
2816            if bit_depth > 8 && csc.is_none() {
2817                for buffer in &bitstream_buffers {
2818                    (function_list.nvEncDestroyBitstreamBuffer.unwrap())(encoder_session, *buffer);
2819                }
2820                (function_list.nvEncUnmapInputResource.unwrap())(
2821                    encoder_session,
2822                    map_params.mappedResource,
2823                );
2824                (function_list.nvEncUnregisterResource.unwrap())(
2825                    encoder_session,
2826                    reg_res.registeredResource,
2827                );
2828                (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2829                (cuda.cuMemFree_v2)(input_device_ptr);
2830                (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2831                (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2832                return Err("the driver took no 10-bit convert kernel".into());
2833            }
2834            keep_primary_context(cu_device);
2835            crate::log::debug!(
2836                "[NVENC] {} initialized (4:4:4 mode: {}, chroma convert: {}).",
2837                codec.display(),
2838                is_444,
2839                if csc.is_some() { "kernel" } else { "hardware" }
2840            );
2841
2842            Ok(Self {
2843                encoder_session,
2844                cuda_context: cu_context,
2845                cuda_device: cu_device,
2846                device_name,
2847                egl_display: egl_display as EGLDisplay,
2848                codec,
2849                fullcolor: is_444,
2850                bit_depth,
2851                asked: (
2852                    settings.video_fullcolor && codec.fullcolor(),
2853                    settings.video_bit_depth >= 10,
2854                ),
2855                width,
2856                height,
2857                current_qp: codec.hardware_quantizer(Hardware::Nvenc, settings.video_crf),
2858                peak: peak_of(settings),
2859                open_rc: config.rcParams,
2860                last_avg_qp: 0,
2861                last_capped: false,
2862                deferred: None,
2863                refused_bind: None,
2864                held_qp: None,
2865                held_band: None,
2866                slice_layout: tuning.slices.map_or((0, 0), |n| (SLICE_MODE_COUNT, n)),
2867                cap: None,
2868                refit: None,
2869                hold_refused: false,
2870                band_log: VecDeque::new(),
2871                lost_cleanup: false,
2872                held_bytes: 0,
2873                qp_map: Vec::new(),
2874                refinement: None,
2875                delta_mapped: false,
2876                refine_refused: false,
2877                last_quality: None,
2878                last_bytes: None,
2879                encode_config: config,
2880                init_params,
2881                input_device_ptr,
2882                input_pitch,
2883                input_format: NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB,
2884                registered_input_resource: reg_res.registeredResource,
2885                mapped_input_buffer: map_params.mappedResource,
2886                bitstream_buffers,
2887                current_buffer_idx: 0,
2888                dmabuf_cache: HashMap::new(),
2889                external_inputs: Vec::new(),
2890                pinned_hosts: HashMap::new(),
2891                cuda,
2892                egl,
2893                _nvenc_lib: nvenc_lib,
2894                nvenc_funcs: function_list,
2895                omit_stripe_headers: settings.omit_stripe_headers,
2896                node_index: settings.encode_node_index.max(0),
2897                pin_uploads: std::env::var("PIXELFLUX_NVENC_PIN").as_deref() != Ok("0"),
2898                direct_dmabuf: std::env::var("PIXELFLUX_NVENC_DIRECT").as_deref() != Ok("0"),
2899                csc,
2900                dpb,
2901                engines,
2902                references: invalidation.then(|| {
2903                    if anchors > 0 {
2904                        let mut w =
2905                            ReferenceWindow::with_anchors(window_frames(codec, dpb, true), anchors);
2906                        if settings.acknowledge_references {
2907                            w.set_acknowledged();
2908                        }
2909                        if codec == Codec::Av1 {
2910                            w.lend_free_anchors();
2911                        }
2912                        w
2913                    } else {
2914                        ReferenceWindow::new(dpb)
2915                    }
2916                }),
2917                last_reference: Reference::Untracked,
2918                frame_num: WideFrameNum::default(),
2919                blit_semaphore: ptr::null_mut(),
2920                band_hash: None,
2921                upload_hash: None,
2922                upload_words: None,
2923                upload_hashes: None,
2924            })
2925        }
2926    }
2927
2928    /// Whether the open session's device lists an encode engine for `codec`.
2929    unsafe fn device_encodes(
2930        funcs: &NV_ENCODE_API_FUNCTION_LIST,
2931        session: *mut c_void,
2932        codec: &GUID,
2933    ) -> bool {
2934        let (Some(count_fn), Some(list_fn)) =
2935            (funcs.nvEncGetEncodeGUIDCount, funcs.nvEncGetEncodeGUIDs)
2936        else {
2937            return true;
2938        };
2939        let mut count = 0u32;
2940        if count_fn(session, &mut count) != NVENCSTATUS::NV_ENC_SUCCESS || count == 0 {
2941            return false;
2942        }
2943        let mut guids = vec![GUID::default(); count as usize];
2944        let mut listed = 0u32;
2945        if list_fn(session, guids.as_mut_ptr(), count, &mut listed) != NVENCSTATUS::NV_ENC_SUCCESS {
2946            return false;
2947        }
2948        guids
2949            .iter()
2950            .take(listed as usize)
2951            .any(|g| guid_eq(g, codec))
2952    }
2953
2954    /// Program the codec-specific arm of `config`: `level`, an infinite IDR period, the chroma
2955    /// format, 8-bit input and output, parameter sets repeated on every key frame, and the
2956    /// color description every session converts with.
2957    ///
2958    /// The whole description is BT.709 at limited range. Primaries and transfer describe the
2959    /// source, sRGB desktop pixels, which shares both with BT.709; the matrix is what
2960    /// `ChromaConvert` produces, and NVENC's own conversion — the fallback, and the 4:4:4
2961    /// sessions — follows the matrix declared here at the limited range it emits, which
2962    /// `gpu_hardware_conversion_matches_the_declared_matrix` holds it to. H.264 additionally
2963    /// restricts reordering in its VUI so no-reorder decoders don't buffer, and codes CABAC.
2964    /// H.264 frames carry `SLICES_PER_FRAME` slices and HEVC frames one, an HEVC band frame its
2965    /// own (`set_band_layout`), and an HEVC session filters across no slice boundary, which is
2966    /// what keeps a band's slice from softening the rows refined before it; AV1 asks for one tile, since tiles
2967    /// cost bitrate and buy no quality, and tier 0, the only tier NVENC takes for it. The driver
2968    /// honors a 1x1 request below 1986 pixels of picture height; above that it forces a second
2969    /// tile row whatever is asked for, in the low-latency presets this one is among but not from
2970    /// P5 up, so a 4K AV1 session codes two tile rows.
2971    ///
2972    /// HEVC declares the tier `h265_tier` names for its level, High: NVENC validates a CBR target
2973    /// against the MaxBR of the pinned level, and the Main-tier ceiling of the 5.x levels
2974    /// (40 Mbit/s at 5.1) is one a 4K desktop session reaches, where a Main-tier open is refused
2975    /// and a live rate change past it is declined. The tier is a signaled cap, not a coding
2976    /// tool; it does change the codec string a client derives from the SPS (`H153` for `L153`).
2977    #[allow(clippy::too_many_arguments)]
2978    fn configure_codec(
2979        config: &mut NV_ENC_CONFIG,
2980        codec: Codec,
2981        fullcolor: bool,
2982        bit_depth: u32,
2983        level: u32,
2984        dpb: u32,
2985        anchors: usize,
2986        tuning: &NvencTuning,
2987    ) {
2988        let primaries = NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709;
2989        let transfer = NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709;
2990        let matrix = NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709;
2991        let vui = |vui: &mut NV_ENC_CONFIG_H264_VUI_PARAMETERS| {
2992            vui.videoSignalTypePresentFlag = 1;
2993            vui.videoFormat = NV_ENC_VUI_VIDEO_FORMAT::NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED;
2994            vui.colourDescriptionPresentFlag = 1;
2995            vui.colourPrimaries = primaries;
2996            vui.transferCharacteristics = transfer;
2997            vui.colourMatrix = matrix;
2998            vui.videoFullRangeFlag = 0;
2999        };
3000        unsafe {
3001            match codec {
3002                Codec::H265 => {
3003                    let c = &mut config.encodeCodecConfig.hevcConfig;
3004                    c.level = level;
3005                    c.maxNumRefFramesInDPB = dpb;
3006                    if anchors > 0 {
3007                        c.set_enableLTR(1);
3008                        c.ltrTrustMode = 0;
3009                        c.ltrNumFrames = anchors as u32;
3010                    }
3011                    c.tier = if tuning.hevc_high_tier {
3012                        h265_tier(level)
3013                    } else {
3014                        0
3015                    };
3016                    c.set_disableDeblockAcrossSliceBoundary(1);
3017                    (c.sliceMode, c.sliceModeData) =
3018                        tuning.slices.map_or((0, 0), |n| (SLICE_MODE_COUNT, n));
3019                    c.idrPeriod = 0xFFFFFFFF;
3020                    c.set_chromaFormatIDC(if fullcolor { 3 } else { 1 });
3021                    c.set_repeatSPSPPS(1);
3022                    c.set_outputAUD(0);
3023                    vui(&mut c.hevcVUIParameters);
3024                }
3025                Codec::Av1 => {
3026                    let c = &mut config.encodeCodecConfig.av1Config;
3027                    c.level = level;
3028                    c.tier = 0;
3029                    c.idrPeriod = 0xFFFFFFFF;
3030                    c.set_chromaFormatIDC(1);
3031                    c.maxNumRefFramesInDPB = dpb;
3032                    if anchors > 0 {
3033                        c.set_enableLTR(1);
3034                        c.ltrNumFrames = anchors as u32;
3035                    }
3036                    c.set_repeatSeqHdr(1);
3037                    c.set_outputAnnexBFormat(0);
3038                    c.set_enableBitstreamPadding(0);
3039                    c.numTileColumns = 1;
3040                    c.numTileRows = 1;
3041                    c.colorPrimaries = primaries;
3042                    c.transferCharacteristics = transfer;
3043                    c.matrixCoefficients = matrix;
3044                    c.colorRange = 0;
3045                }
3046                _ => {
3047                    let c = &mut config.encodeCodecConfig.h264Config;
3048                    c.level = level;
3049                    c.maxNumRefFrames = dpb;
3050                    if anchors > 0 {
3051                        c.set_enableLTR(1);
3052                        c.ltrTrustMode = 0;
3053                        c.ltrNumFrames = anchors as u32;
3054                    }
3055                    c.sliceMode = SLICE_MODE_COUNT;
3056                    c.sliceModeData = tuning.slices.unwrap_or(SLICES_PER_FRAME);
3057                    c.idrPeriod = 0xFFFFFFFF;
3058                    c.chromaFormatIDC = if fullcolor { 3 } else { 1 };
3059                    c.set_repeatSPSPPS(1);
3060                    c.entropyCodingMode =
3061                        NV_ENC_H264_ENTROPY_CODING_MODE::NV_ENC_H264_ENTROPY_CODING_MODE_CABAC;
3062                    c.set_outputAUD(0);
3063                    c.h264VUIParameters.bitstreamRestrictionFlag = 1;
3064                    vui(&mut c.h264VUIParameters);
3065                }
3066            }
3067        }
3068        set_bit_depth(config, codec, bit_depth, nvenc_cur_ver());
3069    }
3070
3071    /// The level the live config declares, read from its codec arm.
3072    fn declared_level(&self) -> u32 {
3073        let config = &self.encode_config.encodeCodecConfig;
3074        unsafe {
3075            match self.codec {
3076                Codec::H265 => config.hevcConfig.level,
3077                Codec::Av1 => config.av1Config.level,
3078                _ => config.h264Config.level,
3079            }
3080        }
3081    }
3082
3083    /// The level a `width` x `height` stream at `fps` takes at the live config's peak bitrate
3084    /// and HEVC tier, and the driver's AV1 share (`NVENC_AV1_WEIGHTED`).
3085    fn level_for(&self, width: u32, height: u32, fps: u32) -> u32 {
3086        let high_tier = unsafe { self.encode_config.encodeCodecConfig.hevcConfig.tier == 1 };
3087        nvenc_level(
3088            self.codec,
3089            width,
3090            height,
3091            fps,
3092            level_rate(
3093                self.codec,
3094                u64::from(self.encode_config.rcParams.maxBitRate),
3095                NVENC_AV1_WEIGHTED.load(Ordering::Relaxed),
3096            ),
3097            high_tier,
3098        )
3099    }
3100
3101    /// Write the level for a new geometry or frame rate into the live config's codec arm.
3102    fn set_level(&mut self, width: u32, height: u32, fps: u32) {
3103        let level = self.level_for(width, height, fps);
3104        match self.codec {
3105            Codec::H265 => self.encode_config.encodeCodecConfig.hevcConfig.level = level,
3106            Codec::Av1 => self.encode_config.encodeCodecConfig.av1Config.level = level,
3107            _ => self.encode_config.encodeCodecConfig.h264Config.level = level,
3108        }
3109    }
3110
3111    /// The decoded picture buffer, in frames, the level a `width` x `height` stream at `fps`
3112    /// takes admits for this codec.
3113    fn dpb_frames_at(&self, width: u32, height: u32, fps: u32) -> u32 {
3114        let level = self.level_for(width, height, fps);
3115        match self.codec {
3116            Codec::H265 => h265_dpb_frames(level, width, height),
3117            Codec::Av1 => AV1_REFERENCES,
3118            _ => h264_dpb_frames(level, width, height),
3119        }
3120    }
3121
3122    /// The frame the last encoded frame predicted from.
3123    pub fn last_reference(&self) -> Reference {
3124        self.last_reference
3125    }
3126
3127    /// Leave frame `frame_id` and every frame after it out of the predictions. False when the
3128    /// device cannot or refuses, and the caller codes a key frame instead.
3129    pub fn invalidate_reference(&mut self, frame_id: u16) -> bool {
3130        let Some(references) = &mut self.references else {
3131            return false;
3132        };
3133        match references.invalidate(frame_id) {
3134            Invalidation::Forget(pts) => unsafe {
3135                (self.nvenc_funcs.nvEncInvalidateRefFrames.unwrap())(self.encoder_session, pts)
3136                    == NVENCSTATUS::NV_ENC_SUCCESS
3137            },
3138            Invalidation::KeyFrame | Invalidation::Ignored => true,
3139        }
3140    }
3141
3142    /// Every consumer holds frame `frame_id`, or where not `held` was sent it
3143    /// (`ReferenceWindow::acknowledge`).
3144    pub fn acknowledge_reference(&mut self, frame_id: u16, held: bool) {
3145        if let Some(references) = &mut self.references {
3146            references.acknowledge(frame_id, held);
3147        }
3148    }
3149
3150    /// Apply what the consumers say of a frame (`FrameEncoder::take_report`); false where a key
3151    /// frame is coded instead.
3152    pub fn take_report(&mut self, report: ReferenceReport) -> bool {
3153        match report {
3154            ReferenceReport::Lost(frame_id) | ReferenceReport::Withheld(frame_id) => {
3155                // The cleanup starts again for a loss (`StripeState::lost_frame`).
3156                self.band_log.clear();
3157                self.invalidate_reference(frame_id)
3158            }
3159            ReferenceReport::Held(frame_id) => {
3160                self.acknowledge_reference(frame_id, true);
3161                true
3162            }
3163            ReferenceReport::Sent(frame_id) => {
3164                self.acknowledge_reference(frame_id, false);
3165                true
3166            }
3167        }
3168    }
3169
3170    /// Leave the frame just encoded out of the predictions as a frame the client lost is, for a
3171    /// frame coded again in its place (`ReferenceWindow::retract`); false where the window or
3172    /// the device cannot.
3173    fn retract_last(&mut self) -> bool {
3174        let (funcs, session) = (&self.nvenc_funcs, self.encoder_session);
3175        self.references.as_mut().is_some_and(|r| {
3176            r.retract(|pts| unsafe {
3177                (funcs.nvEncInvalidateRefFrames.unwrap())(session, pts)
3178                    == NVENCSTATUS::NV_ENC_SUCCESS
3179            })
3180        })
3181    }
3182
3183    /// How the session splits a frame across the device's encode engines (`split_mode`), for the
3184    /// line that says which device encodes.
3185    pub fn split_summary(&self) -> String {
3186        let forced = [
3187            NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_AUTO_FORCED_MODE,
3188            NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_THREE_FORCED_MODE,
3189        ]
3190        .map(|m| m as u32)
3191        .contains(&self.init_params.splitEncodeMode());
3192        match self.engines {
3193            Some(n) if n > 1 && forced => format!("split across {n} engines"),
3194            Some(n) if n > 1 && self.codec != Codec::H264 => {
3195                format!("split as the driver picks, {n} engines")
3196            }
3197            Some(n) => format!("{n} engine{}", if n == 1 { "" } else { "s" }),
3198            None => "engines unreported".into(),
3199        }
3200    }
3201
3202    /// The codec the session emits.
3203    /// The name CUDA gives the GPU this session encodes on.
3204    pub fn device_name(&self) -> &str {
3205        &self.device_name
3206    }
3207
3208    pub fn codec(&self) -> Codec {
3209        self.codec
3210    }
3211
3212    /// Every NVENC session converts to and declares limited range.
3213    pub fn is_full_range(&self) -> bool {
3214        false
3215    }
3216
3217    /// Whether the session negotiated 4:4:4 chroma.
3218    pub fn is_fullcolor(&self) -> bool {
3219        self.fullcolor
3220    }
3221
3222    /// The bits per sample the session codes.
3223    pub fn bit_depth(&self) -> u32 {
3224        self.bit_depth
3225    }
3226
3227    /// Follow a capture restart on the live session, folding in the current rate / QP / fps,
3228    /// without tearing it down.
3229    ///
3230    /// The NVENC session, CUDA context, and bitstream buffers survive, so a restart costs a few
3231    /// milliseconds instead of a full rebuild. Flow:
3232    ///
3233    /// 1. **Reject the unchangeable**: a different encode device or codec, a flip of the chroma
3234    ///    format or depth asked for (4:4:4, 10-bit; not of what a device without them codes), an
3235    ///    RC-mode flip, or dimensions of zero or beyond the init-time `maxEncode` headroom all
3236    ///    return `Err` so the caller rebuilds.
3237    /// 2. **Keep the stream at unchanged dimensions**: the reference chain, the input surface, and
3238    ///    the dmabuf imports stay as they are, so the restart costs no IDR and no reset. Only the
3239    ///    pinned hosts are dropped -- the restart recreates the source buffers, often at the same
3240    ///    addresses -- and the rate, frame rate, and wire framing the restart carries are folded
3241    ///    in, as `reconfigure_rate` does. Returns `Ok(false)`.
3242    /// 3. **Release geometry-dependent state** under the pushed CUDA context: unmap / unregister /
3243    ///    free the packed input surface, every cached dmabuf import (with the NVENC registration a
3244    ///    direct import holds), and every pinned host. The dmabuf imports are re-created lazily by
3245    ///    the encode path; pinned hosts are dropped because the source shm segments are recreated
3246    ///    on resize and may reuse the same base addresses.
3247    /// 4. **Reconfigure the session**: update the level for the new size, the decoded picture
3248    ///    buffer where that level admits fewer frames (the driver takes a smaller one at the
3249    ///    forced IDR and refuses to raise it again), the CBR bitrate + VBV or the ConstQP, and the
3250    ///    new dimensions / DAR / frame rate, then `NvEncReconfigureEncoder` with `resetEncoder`
3251    ///    and `forceIDR` so the stream restarts cleanly at the new size. Driver rejection returns
3252    ///    `Err`.
3253    /// 5. **Reallocate the packed input** at the new size and register + map it as init does, in
3254    ///    the byte order the session was last fed.
3255    ///
3256    /// On a resize the next encoded frame is a reset-RC IDR and `Ok(true)` is returned.
3257    pub fn reconfigure_resolution(&mut self, settings: &CaptureSettings) -> Result<bool, String> {
3258        let new_w = settings.width as u32;
3259        let new_h = settings.height as u32;
3260        let is_cbr = self.encode_config.rcParams.rateControlMode
3261            == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
3262        if settings.encode_node_index.max(0) != self.node_index {
3263            return Err("encode device changed".into());
3264        }
3265        if settings.codec != self.codec {
3266            return Err("codec changed".into());
3267        }
3268        let fullcolor = settings.video_fullcolor && self.codec.fullcolor();
3269        if fullcolor != self.asked.0 && fullcolor != self.fullcolor {
3270            return Err("chroma format changed".into());
3271        }
3272        let ten_bit = settings.video_bit_depth >= 10;
3273        if ten_bit != self.asked.1
3274            && ten_bit != (self.bit_depth > 8)
3275            && matches!(self.codec, Codec::H265 | Codec::Av1)
3276        {
3277            return Err("bit depth changed".into());
3278        }
3279        if settings.video_cbr_mode != is_cbr {
3280            return Err("rate-control mode changed".into());
3281        }
3282        if new_w == 0
3283            || new_h == 0
3284            || new_w > self.init_params.maxEncodeWidth
3285            || new_h > self.init_params.maxEncodeHeight
3286        {
3287            return Err(format!(
3288                "{}x{} outside reconfigure headroom {}x{}",
3289                new_w, new_h, self.init_params.maxEncodeWidth, self.init_params.maxEncodeHeight
3290            ));
3291        }
3292        if (new_w, new_h) == (self.width, self.height) {
3293            self.release_pinned_hosts();
3294            self.reconfigure_rate(settings);
3295            self.omit_stripe_headers = settings.omit_stripe_headers;
3296            return Ok(false);
3297        }
3298        let rate = FrameRate::of(settings.target_fps);
3299        let dpb = self.dpb_frames_at(new_w, new_h, rate.ceil()).min(self.dpb);
3300
3301        unsafe {
3302            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
3303            self.unmap_external_inputs();
3304            if let Some(csc) = self.csc.take() {
3305                csc.release(&self.cuda, &self.nvenc_funcs, self.encoder_session);
3306            }
3307            if !self.mapped_input_buffer.is_null() {
3308                (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
3309                    self.encoder_session,
3310                    self.mapped_input_buffer,
3311                );
3312                self.mapped_input_buffer = ptr::null_mut();
3313            }
3314            if !self.registered_input_resource.is_null() {
3315                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3316                    self.encoder_session,
3317                    self.registered_input_resource,
3318                );
3319                self.registered_input_resource = ptr::null_mut();
3320            }
3321            if self.input_device_ptr != 0 {
3322                (self.cuda.cuMemFree_v2)(self.input_device_ptr);
3323                self.input_device_ptr = 0;
3324            }
3325            let imports: Vec<CachedDmaBuf> = self.dmabuf_cache.drain().map(|(_, c)| c).collect();
3326            for cache in imports {
3327                self.release_dmabuf_import(cache);
3328            }
3329            for (base, len) in self.pinned_hosts.drain() {
3330                if len > 0 {
3331                    (self.cuda.cuMemHostUnregister)(base as *mut c_void);
3332                }
3333            }
3334
3335            if is_cbr {
3336                let bps = cbr_bps(settings);
3337                set_cbr_rate(
3338                    &mut self.encode_config.rcParams,
3339                    bps,
3340                    cbr_vbv(settings, bps),
3341                );
3342            } else {
3343                let qp = self
3344                    .codec
3345                    .hardware_quantizer(Hardware::Nvenc, settings.video_crf);
3346                self.encode_config.rcParams.constQP.qpInterP = qp;
3347                self.encode_config.rcParams.constQP.qpInterB = qp;
3348                self.encode_config.rcParams.constQP.qpIntra = qp;
3349                self.current_qp = qp;
3350            }
3351            self.set_level(new_w, new_h, rate.ceil());
3352            match self.codec {
3353                Codec::H265 => {
3354                    self.encode_config
3355                        .encodeCodecConfig
3356                        .hevcConfig
3357                        .maxNumRefFramesInDPB = dpb
3358                }
3359                Codec::Av1 => {
3360                    self.encode_config
3361                        .encodeCodecConfig
3362                        .av1Config
3363                        .maxNumRefFramesInDPB = dpb
3364                }
3365                _ => {
3366                    self.encode_config
3367                        .encodeCodecConfig
3368                        .h264Config
3369                        .maxNumRefFrames = dpb
3370                }
3371            }
3372            self.init_params.encodeWidth = new_w;
3373            self.init_params.encodeHeight = new_h;
3374            self.init_params.darWidth = new_w;
3375            self.init_params.darHeight = new_h;
3376            self.init_params.frameRateNum = rate.num;
3377            self.init_params.frameRateDen = rate.den;
3378            self.init_params.set_splitEncodeMode(split_mode(
3379                self.codec,
3380                new_w,
3381                new_h,
3382                self.engines,
3383                nvenc_cur_ver(),
3384            ) as u32);
3385            if self.reconfigure(true, true) != NVENCSTATUS::NV_ENC_SUCCESS {
3386                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3387                return Err("NvEncReconfigureEncoder rejected the resolution change".into());
3388            }
3389            self.width = new_w;
3390            self.height = new_h;
3391            self.dpb = dpb;
3392            if let Some(references) = &mut self.references {
3393                references.set_capacity(window_frames(self.codec, dpb, references.anchored()));
3394                references.reset();
3395            }
3396
3397            let mut input_device_ptr: CUdeviceptr = 0;
3398            let mut input_pitch: usize = 0;
3399            let res = (self.cuda.cuMemAllocPitch_v2)(
3400                &mut input_device_ptr,
3401                &mut input_pitch,
3402                (new_w * 4) as usize,
3403                new_h as usize,
3404                16,
3405            );
3406            if res != CUresult::CUDA_SUCCESS {
3407                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3408                return Err("Failed to allocate ARGB input buffer on GPU".into());
3409            }
3410            let mut reg_res = NV_ENC_REGISTER_RESOURCE {
3411                version: sv(NvStruct::RegisterResource),
3412                resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
3413                width: new_w,
3414                height: new_h,
3415                resourceToRegister: input_device_ptr as *mut c_void,
3416                pitch: input_pitch as u32,
3417                bufferFormat: self.input_format,
3418                bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
3419                ..Default::default()
3420            };
3421            let register_fn = self.nvenc_funcs.nvEncRegisterResource.unwrap();
3422            if register_fn(self.encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
3423                (self.cuda.cuMemFree_v2)(input_device_ptr);
3424                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3425                return Err("Failed to register input buffer".into());
3426            }
3427            let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
3428                version: sv(NvStruct::MapInputResource),
3429                registeredResource: reg_res.registeredResource,
3430                ..Default::default()
3431            };
3432            let map_fn = self.nvenc_funcs.nvEncMapInputResource.unwrap();
3433            if map_fn(self.encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
3434                (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3435                    self.encoder_session,
3436                    reg_res.registeredResource,
3437                );
3438                (self.cuda.cuMemFree_v2)(input_device_ptr);
3439                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3440                return Err("Failed to map input buffer".into());
3441            }
3442            self.input_device_ptr = input_device_ptr;
3443            self.input_pitch = input_pitch;
3444            self.registered_input_resource = reg_res.registeredResource;
3445            self.mapped_input_buffer = map_params.mappedResource;
3446            if let Some(layout) = ConvertLayout::of(self.fullcolor, self.bit_depth) {
3447                self.csc = ChromaConvert::new(
3448                    &self.cuda,
3449                    &self.nvenc_funcs,
3450                    self.encoder_session,
3451                    new_w,
3452                    new_h,
3453                    layout,
3454                );
3455                if self.bit_depth > 8 && self.csc.is_none() {
3456                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3457                    return Err("the driver took no 10-bit convert kernel".into());
3458                }
3459            }
3460            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3461        }
3462        self.omit_stripe_headers = settings.omit_stripe_headers;
3463        Ok(true)
3464    }
3465
3466    /// Page-lock one host upload source's base address once, under the already-current CUDA
3467    /// context, so the copy is a direct pinned DMA instead of a pageable copy staged through a driver
3468    /// bounce buffer. A `0`-length entry records a failed registration so the address is never
3469    /// re-probed; the persistent, bounded shm / reused planar sources make this a one-time cost.
3470    unsafe fn pin_host_source(&mut self, base: usize, len: usize) {
3471        if let std::collections::hash_map::Entry::Vacant(e) = self.pinned_hosts.entry(base) {
3472            let st = (self.cuda.cuMemHostRegister_v2)(base as *mut c_void, len, 0);
3473            e.insert(if st == CUresult::CUDA_SUCCESS { len } else { 0 });
3474        }
3475    }
3476
3477    /// Drop every page-locked host registration, under the pushed CUDA context.
3478    ///
3479    /// Called when the capture's shm segments are recreated at unchanged dimensions, and when a
3480    /// Wayland encode thread hands its session on, before its pool is freed: new buffers often
3481    /// reuse the old base addresses, so a stale registration would alias fresh memory, and one
3482    /// freed while registered leaves CUDA the old length at that address. Subsequent uploads
3483    /// re-pin lazily. A `0`-length entry marks a registration that failed and so
3484    /// is not unregistered.
3485    pub fn release_pinned_hosts(&mut self) {
3486        if self.pinned_hosts.is_empty() {
3487            return;
3488        }
3489        unsafe {
3490            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
3491            for (base, len) in self.pinned_hosts.drain() {
3492                if len > 0 {
3493                    (self.cuda.cuMemHostUnregister)(base as *mut c_void);
3494                }
3495            }
3496            let _ = (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3497        }
3498    }
3499
3500    /// Tear down one cached dmabuf import under the already-current CUDA context, inner handle
3501    /// first: the NVENC mapping and registration a direct import holds, then the CUDA graphics
3502    /// resource, then the EGLImage it was built from. The encode that last read the import has
3503    /// completed (`submit_frame` waits for the bitstream), so nothing is still in flight on it.
3504    unsafe fn release_dmabuf_import(&self, cache: CachedDmaBuf) {
3505        if cache.tex != 0 {
3506            (self.cuda.cuTexObjectDestroy)(cache.tex);
3507        }
3508        if let DmaBufInput::Direct {
3509            registered, mapped, ..
3510        } = cache.input
3511        {
3512            (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(self.encoder_session, mapped);
3513            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(self.encoder_session, registered);
3514        }
3515        (self.cuda.cuGraphicsUnregisterResource)(cache.cuda_resource);
3516        if let Some(egl) = &self.egl {
3517            (egl.eglDestroyImageKHR)(self.egl_display, cache.egl_image);
3518        }
3519    }
3520
3521    /// Register the packed input surface with NVENC in the byte order `format` names, when it is
3522    /// not already: the surface memory is unchanged, only its registration (and mapping) is
3523    /// replaced, so a session fed first from one source order and then the other keeps one surface.
3524    /// Runs under the already-current CUDA context; a failed re-registration leaves the surface
3525    /// unregistered and returns `Err`, so the caller's encode fails visibly instead of encoding
3526    /// swapped channels.
3527    unsafe fn set_input_format(&mut self, format: NV_ENC_BUFFER_FORMAT) -> Result<(), String> {
3528        if self.input_format == format && !self.registered_input_resource.is_null() {
3529            return Ok(());
3530        }
3531        if !self.mapped_input_buffer.is_null() {
3532            (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
3533                self.encoder_session,
3534                self.mapped_input_buffer,
3535            );
3536            self.mapped_input_buffer = ptr::null_mut();
3537        }
3538        if !self.registered_input_resource.is_null() {
3539            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3540                self.encoder_session,
3541                self.registered_input_resource,
3542            );
3543            self.registered_input_resource = ptr::null_mut();
3544        }
3545        let mut reg_res = NV_ENC_REGISTER_RESOURCE {
3546            version: sv(NvStruct::RegisterResource),
3547            resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
3548            width: self.width,
3549            height: self.height,
3550            resourceToRegister: self.input_device_ptr as *mut c_void,
3551            pitch: self.input_pitch as u32,
3552            bufferFormat: format,
3553            bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
3554            ..Default::default()
3555        };
3556        if (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res)
3557            != NVENCSTATUS::NV_ENC_SUCCESS
3558        {
3559            return Err(format!("Failed to register input buffer as {format:?}"));
3560        }
3561        let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
3562            version: sv(NvStruct::MapInputResource),
3563            registeredResource: reg_res.registeredResource,
3564            ..Default::default()
3565        };
3566        if (self.nvenc_funcs.nvEncMapInputResource.unwrap())(self.encoder_session, &mut map_params)
3567            != NVENCSTATUS::NV_ENC_SUCCESS
3568        {
3569            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3570                self.encoder_session,
3571                reg_res.registeredResource,
3572            );
3573            return Err(format!("Failed to map input buffer as {format:?}"));
3574        }
3575        self.registered_input_resource = reg_res.registeredResource;
3576        self.mapped_input_buffer = map_params.mappedResource;
3577        self.input_format = format;
3578        Ok(())
3579    }
3580
3581    /// Hand the live config to `nvEncReconfigureEncoder`, resetting the encoder and forcing an IDR
3582    /// as asked.
3583    unsafe fn reconfigure(&mut self, reset: bool, force_idr: bool) -> NVENCSTATUS {
3584        self.init_params.encodeConfig = &mut self.encode_config;
3585        let mut params = reconfigure_params(&self.init_params, nvenc_cur_ver(), reset, force_idr);
3586        (self.nvenc_funcs.nvEncReconfigureEncoder.unwrap())(self.encoder_session, &mut params.value)
3587    }
3588
3589    /// Reconfigure the live session's ConstQP when the quantizer the session quality index
3590    /// `crf` selects differs from the current one, returning whether a reconfigure actually
3591    /// happened.
3592    ///
3593    /// A no-op in CBR mode (bitrate-controlled, so QP-based paint-over does not apply), save a
3594    /// capped constant quantizer bound to its peak, whose floor follows (`peak_of`), and when the
3595    /// QP is unchanged. When it does apply, the three `constQP` fields are updated and the session is
3596    /// reconfigured **without** a forced IDR: a lower-QP P-frame refines the static image against the
3597    /// existing reference chain (paint-over) with no intra-frame bitrate spike, so the GOP continues
3598    /// seamlessly across the reconfigure.
3599    unsafe fn reconfigure_if_needed(&mut self, crf: u32) -> bool {
3600        let binding = self.peak.is_some_and(|p| p.binding);
3601        if self.encode_config.rcParams.rateControlMode
3602            == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
3603            && !binding
3604        {
3605            return false;
3606        }
3607        let target_qp = self.codec.hardware_quantizer(Hardware::Nvenc, crf as i32);
3608        if self.current_qp != target_qp {
3609            let rc = &mut self.encode_config.rcParams;
3610            rc.constQP.qpInterP = target_qp;
3611            rc.constQP.qpInterB = target_qp;
3612            rc.constQP.qpIntra = target_qp;
3613            if binding {
3614                rc.minQP.qpInterP = target_qp;
3615                rc.minQP.qpInterB = target_qp;
3616                rc.minQP.qpIntra = target_qp;
3617            }
3618            let status = self.reconfigure(false, false);
3619            if status == NVENCSTATUS::NV_ENC_SUCCESS {
3620                self.current_qp = target_qp;
3621                return true;
3622            }
3623            eprintln!(
3624                "[NVENC] Quantizer reconfigure refused ({status:?}): {}",
3625                last_error(&self.nvenc_funcs, self.encoder_session)
3626            );
3627        }
3628        false
3629    }
3630
3631    /// Whether the constant quantizer runs under a peak (`peak_of`).
3632    pub fn capped(&self) -> bool {
3633        self.peak.is_some()
3634    }
3635
3636    /// Whether the session is bound to that peak, coding through its constant rate.
3637    pub fn peak_bound(&self) -> bool {
3638        self.peak.is_some_and(|p| p.binding)
3639    }
3640
3641    /// Whether the last frame was coded bound to the peak.
3642    pub fn coded_capped(&self) -> bool {
3643        self.last_capped
3644    }
3645
3646    /// A tick that codes no frame (`FrameEncoder::idle`): the peak's bucket drains, and a session
3647    /// bound to it counts the tick at its quantizer, nothing being coded coarser.
3648    pub fn idle(&mut self) {
3649        let Some(peak) = self.peak.as_mut() else {
3650            return;
3651        };
3652        if let Some(bind) = peak.note(0.0, Some(true)) {
3653            unsafe { self.switch_peak(bind) };
3654        }
3655    }
3656
3657    /// Hold the blocks `plan` names at the coarsest quantizer in the next frame of the rate
3658    /// control's, through the QP delta map (`FrameEncoder::defer_blocks`): a block whose
3659    /// reference already holds it codes as a skip there.
3660    pub fn defer_blocks(&mut self, plan: Option<&crate::ledger::BlockPlan>) {
3661        self.deferred = plan.filter(|_| self.refines()).cloned();
3662    }
3663
3664    /// The quality index the rate control last coded a frame at, where the driver reports its
3665    /// quantizer; a held frame (`hold_quantizer`) is not the rate control's and leaves it.
3666    pub fn last_quality(&self) -> Option<u32> {
3667        self.last_quality
3668    }
3669
3670    /// The bytes of the last frame the rate control coded; a held frame leaves it.
3671    pub fn last_size(&self) -> Option<usize> {
3672        self.last_bytes
3673    }
3674
3675    /// Encode the next frame at the constant quantizer the quality index `crf` selects, whatever
3676    /// the rate control, and leave the session's own rate control and quantizer as they were for
3677    /// the frame after: the cleanup of a still screen. A held key frame of a constant-rate session
3678    /// that comes out past `HELD_KEY_BUDGET_S` of the target is coded again, as a key frame, at the
3679    /// coarser quantizer `held_key_retry` picks, and so is a held frame of the whole picture past
3680    /// `HELD_REFRESH_LIMIT_S`, predicted from the frame before the first attempt, which the device
3681    /// leaves out as it does a frame the client lost (`retract_last`). `band`, the share of the
3682    /// picture from and to in raster order, confines `crf` to the blocks it covers (`band_size`),
3683    /// the rest of the frame held at the coarsest quantizer, or for AV1 at the coarsest the delta
3684    /// map's 128 steps reach above the band: a still region is left as it is at any quantizer, and
3685    /// a change the frame carries before its damage is known (X11 under Turbo hashes a frame beside
3686    /// its encode) costs what the rate control's own frame would.
3687    pub fn hold_quantizer(&mut self, crf: u32, band: Option<(f64, f64)>) {
3688        self.held_qp = Some(crf);
3689        self.held_band = band.filter(|_| self.band_size().is_some());
3690    }
3691
3692    /// Whether the session refines blocks through a QP delta map under its rate control
3693    /// (`refine_blocks`): H.264, HEVC, and AV1, until the driver refuses the map.
3694    pub fn refines(&self) -> bool {
3695        matches!(self.codec, Codec::H264 | Codec::H265 | Codec::Av1) && !self.refine_refused
3696    }
3697
3698    /// Code the blocks `refinement` names in the next frame of the rate control's at its target
3699    /// quality, through the frame's QP delta map: each map unit (a 16x16 macroblock, a 32x32
3700    /// coding tree block, a 64x64 superblock) holding a refined block takes the delta from the
3701    /// quantizer the rate control last coded at to the target, the rest 0. The rate control
3702    /// is put on the delta map mode the first time, by the reconfigure that takes the
3703    /// rate-control mode without a reset, and stays there.
3704    pub fn refine_blocks(&mut self, refinement: &crate::ledger::Refinement) {
3705        self.refinement = self.refines().then(|| refinement.clone());
3706    }
3707
3708    /// Fill `qp_map` for the blocks the next frame refines (`refine_blocks`), each map unit
3709    /// holding one at the delta from the quantizer the rate control last coded at to the target,
3710    /// and for the blocks a refresh leaves for later frames (`defer_blocks`), each at the delta to
3711    /// the codec's coarsest quantizer from the session's; the rest 0. Returns whether the session
3712    /// codes through it, which a refused reconfigure ends for good.
3713    unsafe fn block_map(
3714        &mut self,
3715        refinement: Option<&crate::ledger::Refinement>,
3716        deferred: Option<&crate::ledger::BlockPlan>,
3717    ) -> bool {
3718        if !self.delta_map_mode() {
3719            return false;
3720        }
3721        let hw = |index: i32| self.codec.hardware_quantizer(Hardware::Nvenc, index) as i32;
3722        let refine = refinement.map(|r| {
3723            let delta = (hw(r.target) - hw(r.from)).clamp(i32::from(i8::MIN), 0) as i8;
3724            (&r.plan, delta)
3725        });
3726        let defer = (self.codec.quantizer_max() as i32 - self.current_qp as i32)
3727            .clamp(0, i32::from(i8::MAX)) as i8;
3728        let unit = match self.codec {
3729            Codec::H264 => 16,
3730            Codec::H265 => 32,
3731            _ => 64,
3732        };
3733        let (cols, rows) = (self.width.div_ceil(unit), self.height.div_ceil(unit));
3734        self.qp_map.clear();
3735        self.qp_map.extend((0..rows).flat_map(|row| {
3736            (0..cols).map(move |col| {
3737                let (x, y, u) = ((col * unit) as i32, (row * unit) as i32, unit as i32);
3738                match refine {
3739                    Some((plan, delta)) if plan.refines_area(x, y, u, u) => delta,
3740                    _ if deferred.is_some_and(|d| d.refines_area(x, y, u, u)) => defer,
3741                    _ => 0,
3742                }
3743            })
3744        }));
3745        true
3746    }
3747
3748    /// Put the rate control on the QP delta map mode, by the reconfigure that takes the
3749    /// rate-control mode without a reset, the first time; false where the driver refused, which
3750    /// ends refining for good.
3751    unsafe fn delta_map_mode(&mut self) -> bool {
3752        if self.delta_mapped {
3753            return true;
3754        }
3755        let saved = self.encode_config.rcParams;
3756        self.encode_config.rcParams.qpMapMode = NV_ENC_QP_MAP_MODE::NV_ENC_QP_MAP_DELTA;
3757        let status = self.reconfigure(false, false);
3758        if status != NVENCSTATUS::NV_ENC_SUCCESS {
3759            self.encode_config.rcParams = saved;
3760            self.refine_refused = true;
3761            eprintln!(
3762                "[NVENC] Refining blocks through a QP delta map refused ({status:?}): {}",
3763                last_error(&self.nvenc_funcs, self.encoder_session)
3764            );
3765            return false;
3766        }
3767        self.delta_mapped = true;
3768        true
3769    }
3770
3771    /// Take the next frame back where it comes out past `bytes` and code it again within them: a
3772    /// held band in a shorter one, to `REFIT_SHARE` of them at the bytes the band came to, and a
3773    /// frame of the rate control's at the coarsest quantizer, which on a still screen refines
3774    /// nothing. The frame taken back is left out of the predictions as a frame a client lost is
3775    /// (`retract_last`); a key frame stands as coded.
3776    pub fn cap(&mut self, bytes: usize) {
3777        self.cap = Some(bytes);
3778    }
3779
3780    /// Where the last frame was taken back and coded again (`cap`): how far its held band then
3781    /// reached, or 0 for a frame of the rate control's.
3782    pub fn refit(&self) -> Option<f64> {
3783        self.refit
3784    }
3785
3786    /// Whether a frame left a held band out of every prediction since this was last asked (an
3787    /// anchor predicting from the newest frame every client was sent, past a band coded since),
3788    /// where the frame held none to take it back into: the cleanup has to run again.
3789    pub fn take_lost_cleanup(&mut self) -> bool {
3790        std::mem::take(&mut self.lost_cleanup)
3791    }
3792
3793    /// The frames from timestamp `pts` on have just been left out of every prediction: a band one
3794    /// of them held is coded again in the band this frame holds, which then reaches back to it,
3795    /// and is lost to a frame that holds none (`take_lost_cleanup`).
3796    fn forget_bands(&mut self, pts: u64) {
3797        let (band, lost) = reach_back(
3798            &mut self.band_log,
3799            pts,
3800            self.held_band,
3801            self.held_qp.is_some(),
3802        );
3803        self.held_band = band;
3804        self.lost_cleanup |= lost;
3805    }
3806
3807    /// Keep the deblocking filter off the rows the bands before refined, on a frame holding a band
3808    /// whose first block is `band` (`None` on any other frame): at the edges a band shares with
3809    /// blocks held at the coarsest quantizer, the filter works at their mean quantizer and softens
3810    /// those rows (at 60 fps 0.5 Mbit/s a 1080p row from 44.5 dB to 33.9). H.264 turns the filter
3811    /// off for the frame, which its slice header carries. NVENC's HEVC codes the same bytes
3812    /// whatever its switch says on a reconfigure, so its band frame starts a slice at the band
3813    /// instead, across whose boundary the session never filters (`configure_codec`), where that
3814    /// takes at most `BAND_SLICES_MAX` even slices; a band starting before them keeps the frame's
3815    /// own. AV1 has neither.
3816    fn set_band_layout(&mut self, band: Option<usize>) {
3817        match self.codec {
3818            Codec::H264 => {
3819                self.encode_config
3820                    .encodeCodecConfig
3821                    .h264Config
3822                    .disableDeblockingFilterIDC = u32::from(band.is_some());
3823            }
3824            Codec::H265 => {
3825                let blocks = (self.width.div_ceil(32) * self.height.div_ceil(32)) as usize;
3826                let c = unsafe { &mut self.encode_config.encodeCodecConfig.hevcConfig };
3827                (c.sliceMode, c.sliceModeData) = match band {
3828                    Some(first) if first > 0 && blocks.div_ceil(first) <= BAND_SLICES_MAX => {
3829                        (0, first as u32)
3830                    }
3831                    _ => self.slice_layout,
3832                };
3833            }
3834            _ => {}
3835        }
3836    }
3837
3838    /// The bytes of the last held frame (0 before one), where the session holds a band: H.264,
3839    /// HEVC and AV1, whose QP delta map is one entry a macroblock, a 32x32 coding tree block, or a
3840    /// 64x64 superblock.
3841    pub fn band_size(&self) -> Option<usize> {
3842        matches!(self.codec, Codec::H264 | Codec::H265 | Codec::Av1).then_some(self.held_bytes)
3843    }
3844
3845    /// Put the session on constant quantizer `q` for the picture about to be submitted, with the
3846    /// picture's QP delta map on top where it holds a band starting at block `band`
3847    /// (`fill_band_map`), answering the rate control to restore once it is encoded; `None` where
3848    /// the session is already there or the driver refused, which leaves the picture to the rate
3849    /// control.
3850    ///
3851    /// The rate-control mode is one of the parameters `nvEncReconfigureEncoder` takes without a
3852    /// reset, so a constant-rate session keeps its HRD state across the held picture instead of
3853    /// starting over after it.
3854    unsafe fn hold_rate(&mut self, q: u32, band: Option<usize>) -> Option<NV_ENC_RC_PARAMS> {
3855        let mapped = band.is_some();
3856        let saved = self.encode_config.rcParams;
3857        let constant = saved.rateControlMode == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
3858        if constant && !mapped && saved.constQP.qpIntra == q && saved.constQP.qpInterP == q {
3859            return None;
3860        }
3861        self.set_band_layout(band);
3862        let rc = &mut self.encode_config.rcParams;
3863        if mapped {
3864            rc.qpMapMode = NV_ENC_QP_MAP_MODE::NV_ENC_QP_MAP_DELTA;
3865        }
3866        rc.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
3867        rc.constQP.qpInterP = q;
3868        rc.constQP.qpInterB = q;
3869        rc.constQP.qpIntra = q;
3870        let status = self.reconfigure(false, false);
3871        if status != NVENCSTATUS::NV_ENC_SUCCESS {
3872            self.encode_config.rcParams = saved;
3873            self.set_band_layout(None);
3874            if !std::mem::replace(&mut self.hold_refused, true) {
3875                eprintln!(
3876                    "[NVENC] Holding a frame at quantizer {q} refused ({status:?}): {}",
3877                    last_error(&self.nvenc_funcs, self.encoder_session)
3878                );
3879            }
3880            return None;
3881        }
3882        Some(saved)
3883    }
3884
3885    /// Apply a runtime rate-control / frame-rate change to the live session, and report whether
3886    /// the session carries it afterwards.
3887    ///
3888    /// In CBR mode the target bitrate, max bitrate, VBV, and its initial delay are updated (the VBV
3889    /// is ignored outside CBR); the target fps is updated in either mode. The session is
3890    /// reconfigured only when one of these actually changed — no RC reset, and a forced IDR only
3891    /// where a target past the declared level's bitrate ceiling, or a frame rate past its
3892    /// macroblock rate, raises the level, which the decoder learns from the sequence header a key
3893    /// frame carries; a level a lower target or frame rate no longer needs stays, since a level
3894    /// only ever has to be high enough and the driver refuses one below the decoded picture
3895    /// buffer the session declared — so calling it every frame is cheap. A reconfigure the driver
3896    /// refuses leaves the session encoding at its previous rate, logged with the driver's reason.
3897    pub fn reconfigure_rate(&mut self, settings: &CaptureSettings) -> bool {
3898        if let Some(peak) = self.peak.as_mut() {
3899            let bps = cbr_bps(settings);
3900            peak.follow(bps, cbr_vbv(settings, bps), settings.target_fps);
3901            if peak.binding {
3902                let rc = &mut self.encode_config.rcParams;
3903                let at = rc.maxBitRate;
3904                rc.maxBitRate = peak.bps;
3905                let past = self.level_for_peak() > self.declared_level();
3906                self.encode_config.rcParams.maxBitRate = at;
3907                if past {
3908                    unsafe { self.switch_peak(false) };
3909                }
3910            }
3911        }
3912        unsafe {
3913            let mut changed = false;
3914            let mut level_raised = false;
3915            if self.encode_config.rcParams.rateControlMode
3916                == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
3917            {
3918                let bps = cbr_bps(settings);
3919                let vbv = cbr_vbv(settings, bps);
3920                let rc = &mut self.encode_config.rcParams;
3921                if rc.averageBitRate != bps || rc.maxBitRate != bps || rc.vbvBufferSize != vbv {
3922                    log_held_rate(settings, bps);
3923                    set_cbr_rate(rc, bps, vbv);
3924                    changed = true;
3925                    let fps = self
3926                        .init_params
3927                        .frameRateNum
3928                        .div_ceil(self.init_params.frameRateDen.max(1));
3929                    let (w, h) = (self.init_params.encodeWidth, self.init_params.encodeHeight);
3930                    if self.level_for(w, h, fps) > self.declared_level() {
3931                        self.set_level(w, h, fps);
3932                        level_raised = true;
3933                    }
3934                }
3935            }
3936            let rate = FrameRate::of(settings.target_fps);
3937            if (self.init_params.frameRateNum, self.init_params.frameRateDen)
3938                != (rate.num, rate.den)
3939            {
3940                self.init_params.frameRateNum = rate.num;
3941                self.init_params.frameRateDen = rate.den;
3942                let (w, h, fps) = (
3943                    self.init_params.encodeWidth,
3944                    self.init_params.encodeHeight,
3945                    rate.ceil(),
3946                );
3947                if self.level_for(w, h, fps) > self.declared_level() {
3948                    self.set_level(w, h, fps);
3949                    level_raised = true;
3950                }
3951                changed = true;
3952            }
3953            if !changed {
3954                return true;
3955            }
3956            let mut status = self.reconfigure(false, level_raised);
3957            if status != NVENCSTATUS::NV_ENC_SUCCESS
3958                && self.codec == Codec::Av1
3959                && !NVENC_AV1_WEIGHTED.swap(true, Ordering::Relaxed)
3960            {
3961                let (w, h) = (self.init_params.encodeWidth, self.init_params.encodeHeight);
3962                let fps = self
3963                    .init_params
3964                    .frameRateNum
3965                    .div_ceil(self.init_params.frameRateDen.max(1));
3966                let refused = self.declared_level();
3967                if self.level_for(w, h, fps) > refused {
3968                    self.set_level(w, h, fps);
3969                    level_raised = true;
3970                    status = self.reconfigure(false, true);
3971                }
3972                if status == NVENCSTATUS::NV_ENC_SUCCESS {
3973                    eprintln!(
3974                        "[NVENC] The driver refused AV1 level {refused} at {} kbps; declaring {}",
3975                        self.encode_config.rcParams.maxBitRate / 1000,
3976                        self.declared_level()
3977                    );
3978                } else {
3979                    NVENC_AV1_WEIGHTED.store(false, Ordering::Relaxed);
3980                }
3981            }
3982            if status != NVENCSTATUS::NV_ENC_SUCCESS {
3983                eprintln!(
3984                    "[NVENC] Rate reconfigure refused ({status:?}): {}",
3985                    last_error(&self.nvenc_funcs, self.encoder_session)
3986                );
3987                return false;
3988            }
3989            if level_raised && let Some(references) = &mut self.references {
3990                references.reset();
3991            }
3992            true
3993        }
3994    }
3995
3996    /// Encode one mapped input picture and return its bitstream bytes behind the wire header.
3997    ///
3998    /// The shared tail of all three encode paths:
3999    ///
4000    /// 1. **Pick an output buffer** from the ring (`current_buffer_idx` advances modulo the ring
4001    ///    length) and submit the picture with `nvEncEncodePicture`; `force_idr` sets the force-IDR
4002    ///    pic flag.
4003    /// 2. **Lock the bitstream** (`nvEncLockBitstream`, blocking) to read the encoded bytes. The
4004    ///    lock has to block: on Linux a `doNotWait` lock answers an unfinished encode with
4005    ///    `NV_ENC_SUCCESS` and an empty bitstream, not `NV_ENC_ERR_LOCK_BUSY`, so polling it would
4006    ///    hand out empty frames.
4007    /// 3. **Frame the output**: unless `omit_stripe_headers` is set, prepend the wire header with
4008    ///    the frame kind derived from the *actual* encoded `pictureType` (IDR = key, I = intra,
4009    ///    else delta) rather than the `force_idr` request.
4010    /// 4. **Emit**: append the encoded bytes, unlock the bitstream, and return the framed buffer.
4011    unsafe fn submit_frame(
4012        &mut self,
4013        mapped_buffer: NV_ENC_INPUT_PTR,
4014        buffer_format: NV_ENC_BUFFER_FORMAT,
4015        frame_number: u64,
4016        force_idr: bool,
4017    ) -> Result<Vec<u8>, String> {
4018        let output_bitstream = self.bitstream_buffers[self.current_buffer_idx];
4019        self.current_buffer_idx = (self.current_buffer_idx + 1) % self.bitstream_buffers.len();
4020
4021        let asked_idr = force_idr;
4022        let mut force_idr =
4023            force_idr || self.references.as_ref().is_some_and(|r| !r.has_reference());
4024        let mut forgot = None;
4025        if !force_idr
4026            && let Some(references) = &mut self.references
4027            && references.plan_anchor(false).is_some()
4028        {
4029            match references.settle() {
4030                Some(Invalidation::Forget(pts)) => {
4031                    force_idr = (self.nvenc_funcs.nvEncInvalidateRefFrames.unwrap())(
4032                        self.encoder_session,
4033                        pts,
4034                    ) != NVENCSTATUS::NV_ENC_SUCCESS;
4035                    forgot = Some(pts);
4036                }
4037                Some(Invalidation::KeyFrame) => force_idr = true,
4038                _ => {}
4039            }
4040        }
4041        if !force_idr && let Some(references) = &mut self.references {
4042            match references.forget_stale() {
4043                Some(Invalidation::Forget(pts)) => {
4044                    force_idr = (self.nvenc_funcs.nvEncInvalidateRefFrames.unwrap())(
4045                        self.encoder_session,
4046                        pts,
4047                    ) != NVENCSTATUS::NV_ENC_SUCCESS;
4048                    forgot = Some(forgot.map_or(pts, |f: u64| f.min(pts)));
4049                }
4050                Some(Invalidation::KeyFrame) => force_idr = true,
4051                _ => {}
4052            }
4053        }
4054        if force_idr {
4055            // A key frame of the device's own codes the picture at the stream's quality, or with
4056            // a band at the coarsest outside it: the bands before are gone.
4057            if !asked_idr
4058                && !self.band_log.is_empty()
4059                && (self.held_band.is_some() || self.held_qp.is_none())
4060            {
4061                self.lost_cleanup = true;
4062            }
4063            self.band_log.clear();
4064        } else if let Some(pts) = forgot {
4065            self.forget_bands(pts);
4066        }
4067        let anchor = self
4068            .references
4069            .as_ref()
4070            .and_then(|r| r.plan_anchor(force_idr));
4071        let named = self
4072            .references
4073            .as_ref()
4074            .filter(|_| !force_idr)
4075            .and_then(ReferenceWindow::predicting_anchor);
4076        let mut pic_params = NV_ENC_PIC_PARAMS {
4077            version: sv(NvStruct::PicParams),
4078            inputWidth: self.width,
4079            inputHeight: self.height,
4080            inputBuffer: mapped_buffer,
4081            outputBitstream: output_bitstream,
4082            bufferFmt: buffer_format,
4083            pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
4084            encodePicFlags: if force_idr {
4085                NV_ENC_PIC_FLAGS::NV_ENC_PIC_FLAG_FORCEIDR as u32
4086            } else {
4087                0
4088            },
4089            inputTimeStamp: self
4090                .references
4091                .as_ref()
4092                .map_or(0, ReferenceWindow::next_pts),
4093            ..Default::default()
4094        };
4095
4096        macro_rules! long_term {
4097            ($p:expr) => {{
4098                let p = $p;
4099                if let Some(slot) = named {
4100                    p.set_ltrUseFrames(1);
4101                    p.ltrUseFrameBitmap = 1 << slot;
4102                }
4103                if let Some(slot) = anchor {
4104                    p.set_ltrMarkFrame(1);
4105                    p.ltrMarkFrameIdx = u32::from(slot);
4106                }
4107            }};
4108        }
4109        match self.codec {
4110            Codec::Av1 => long_term!(&mut pic_params.codecPicParams.av1PicParams),
4111            Codec::H265 => long_term!(&mut pic_params.codecPicParams.hevcPicParams),
4112            _ => long_term!(&mut pic_params.codecPicParams.h264PicParams),
4113        }
4114        let held_qp = self.held_qp.take();
4115        let band = self.held_band.take();
4116        let held = held_qp.and_then(|crf| {
4117            let q = self.codec.hardware_quantizer(Hardware::Nvenc, crf as i32);
4118            let rest = self
4119                .codec
4120                .quantizer_max()
4121                .min(q + u32::from(i8::MIN.unsigned_abs()));
4122            match band {
4123                Some(share) if rest > q => {
4124                    let first = fill_band_map(
4125                        &mut self.qp_map,
4126                        self.codec,
4127                        self.width,
4128                        self.height,
4129                        share,
4130                        q as i32 - rest as i32,
4131                    );
4132                    let held = self.hold_rate(rest, Some(first));
4133                    if held.is_some() {
4134                        point_at_qp_map(&mut pic_params, &mut self.qp_map, nvenc_cur_ver());
4135                    }
4136                    held
4137                }
4138                _ => self.hold_rate(q, None),
4139            }
4140        });
4141        let refinement = self
4142            .refinement
4143            .take()
4144            .filter(|_| held_qp.is_none() && !force_idr);
4145        let deferred = self
4146            .deferred
4147            .take()
4148            .filter(|_| held_qp.is_none() && !force_idr);
4149        let mapped = (refinement.is_some() || deferred.is_some())
4150            && self.block_map(refinement.as_ref(), deferred.as_ref());
4151        if mapped {
4152            point_at_qp_map(&mut pic_params, &mut self.qp_map, nvenc_cur_ver());
4153        }
4154        let mut result = self.encode_picture(
4155            &mut pic_params,
4156            output_bitstream,
4157            frame_number,
4158            held_qp.is_some(),
4159            anchor,
4160        );
4161        let retry = match (held_qp, held, &result) {
4162            (Some(crf), Some(rc), Ok(coded))
4163                if rc.rateControlMode == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
4164                    && (force_idr
4165                        || band.is_none()
4166                            && coded.len() as f64
4167                                > f64::from(rc.averageBitRate) / 8.0
4168                                    * super::HELD_REFRESH_LIMIT_S) =>
4169            {
4170                let cap = (f64::from(rc.averageBitRate) / 8.0 * super::HELD_KEY_BUDGET_S) as usize;
4171                super::held_key_retry(crf, coded.len(), cap)
4172            }
4173            _ => None,
4174        };
4175        if let Some(coarser) = retry
4176            && self
4177                .hold_rate(
4178                    self.codec
4179                        .hardware_quantizer(Hardware::Nvenc, coarser as i32),
4180                    None,
4181                )
4182                .is_some()
4183            && (force_idr || self.retract_last())
4184        {
4185            pic_params.inputTimeStamp = self
4186                .references
4187                .as_ref()
4188                .map_or(0, ReferenceWindow::next_pts);
4189            result = self.encode_picture(
4190                &mut pic_params,
4191                output_bitstream,
4192                frame_number,
4193                true,
4194                anchor,
4195            );
4196        }
4197        self.refit = None;
4198        let cap = self.cap.take();
4199        let mut held = held;
4200        if let (Some(cap), Ok(coded), None, false) = (cap, &result, retry, force_idr)
4201            && coded.len() > cap
4202        {
4203            let refit = match (held_qp, band, held) {
4204                (Some(crf), Some((from, to)), Some(_)) => {
4205                    let q = self.codec.hardware_quantizer(Hardware::Nvenc, crf as i32);
4206                    let rest = self
4207                        .codec
4208                        .quantizer_max()
4209                        .min(q + u32::from(i8::MIN.unsigned_abs()));
4210                    let end = from + (to - from) * REFIT_SHARE * cap as f64 / coded.len() as f64;
4211                    fill_band_map(
4212                        &mut self.qp_map,
4213                        self.codec,
4214                        self.width,
4215                        self.height,
4216                        (from, end),
4217                        q as i32 - rest as i32,
4218                    );
4219                    point_at_qp_map(&mut pic_params, &mut self.qp_map, nvenc_cur_ver());
4220                    Some(end)
4221                }
4222                (None, _, None) => {
4223                    held = self.hold_rate(self.codec.quantizer_max(), None);
4224                    held.map(|_| 0.0)
4225                }
4226                _ => None,
4227            };
4228            if let Some(end) = refit
4229                && self.retract_last()
4230            {
4231                pic_params.inputTimeStamp = self
4232                    .references
4233                    .as_ref()
4234                    .map_or(0, ReferenceWindow::next_pts);
4235                result = self.encode_picture(
4236                    &mut pic_params,
4237                    output_bitstream,
4238                    frame_number,
4239                    true,
4240                    anchor,
4241                );
4242                self.refit = Some(end);
4243            }
4244        }
4245        if let (Some(_), Some((from, to)), Ok(_)) = (held, band, &result) {
4246            let to = self.refit.unwrap_or(to);
4247            self.band_log
4248                .push_back((pic_params.inputTimeStamp, from, to));
4249            if self.band_log.len() > BAND_LOG_FRAMES {
4250                self.band_log.pop_front();
4251            }
4252        }
4253        if let Some(rc) = held {
4254            self.encode_config.rcParams = rc;
4255            self.set_band_layout(None);
4256            let status = self.reconfigure(false, false);
4257            if status != NVENCSTATUS::NV_ENC_SUCCESS {
4258                eprintln!(
4259                    "[NVENC] Restoring the rate control after a held frame refused ({status:?}): {}",
4260                    last_error(&self.nvenc_funcs, self.encoder_session)
4261                );
4262            }
4263        }
4264        self.last_capped = self.peak.is_some_and(|p| p.binding);
4265        if let Ok(coded) = &result {
4266            self.follow_peak(coded.len(), held_qp.is_none());
4267        }
4268        result
4269    }
4270
4271    /// Fold the frame just coded, `bytes` long, into a capped constant quantizer's peak
4272    /// (`peak_of`), and bind the session to the peak or release it where the frame says to; `own`
4273    /// is false for a held frame. A frame refining blocks through the QP map counts as the rate
4274    /// control's: bound to a peak its content overflows, NVENC's constant rate holds such a
4275    /// frame's mean quantizer over the floor (none of 140 refining every block 25 steps finer
4276    /// came out at it, on an L4), and a session refining on every bound frame would otherwise
4277    /// never count one toward its release.
4278    unsafe fn follow_peak(&mut self, bytes: usize, own: bool) {
4279        let floor = self.current_qp;
4280        let qp = Some(self.last_avg_qp).filter(|q| own && *q > 0);
4281        let Some(peak) = self.peak.as_mut() else {
4282            return;
4283        };
4284        if let Some(bind) = peak.note(bytes as f64 * 8.0, qp.map(|q| q <= floor)) {
4285            self.switch_peak(bind);
4286        }
4287    }
4288
4289    /// Whether a bind to the peak was refused at the peak's rate as it stands (`switch_peak`),
4290    /// which is not asked for again until the peak moves.
4291    fn bind_refused(&self) -> bool {
4292        self.peak.is_some_and(|p| self.refused_bind == Some(p.bps))
4293    }
4294
4295    /// Bind a capped constant quantizer to its peak, or release it to its constant quantizer
4296    /// (`peak_of`); a switch the driver refuses leaves the session as it was. A peak past the
4297    /// level the session declared releases it rather than raise the level, a key frame. A bind
4298    /// refused either way is not tried again at that peak (`bind_refused`).
4299    unsafe fn switch_peak(&mut self, bind: bool) {
4300        let Some(peak) = self.peak else {
4301            return;
4302        };
4303        if bind && self.bind_refused() {
4304            if let Some(peak) = self.peak.as_mut() {
4305                peak.binding = false;
4306            }
4307            return;
4308        }
4309        let floor = self.current_qp;
4310        let (bps, vbv) = (peak.bps, peak.vbv);
4311        let saved = self.encode_config.rcParams;
4312        let rc = &mut self.encode_config.rcParams;
4313        if bind {
4314            rc.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
4315            set_cbr_rate(rc, bps, vbv);
4316            rc.set_enableMinQP(1);
4317            rc.minQP.qpInterP = floor;
4318            rc.minQP.qpInterB = floor;
4319            rc.minQP.qpIntra = floor;
4320        } else {
4321            let open = self.open_rc;
4322            rc.rateControlMode = open.rateControlMode;
4323            rc.averageBitRate = open.averageBitRate;
4324            rc.maxBitRate = open.maxBitRate;
4325            rc.vbvBufferSize = open.vbvBufferSize;
4326            rc.vbvInitialDelay = open.vbvInitialDelay;
4327            // The driver keeps coding at the floor it last took once `enableMinQP` is cleared
4328            // (an L4), so a release lowers the floor to the lowest quantizer instead.
4329            rc.set_enableMinQP(1);
4330            rc.minQP.qpInterP = 0;
4331            rc.minQP.qpInterB = 0;
4332            rc.minQP.qpIntra = 0;
4333        }
4334        if bind && self.level_for_peak() > self.declared_level() {
4335            self.encode_config.rcParams = saved;
4336            self.refused_bind = Some(bps);
4337            if let Some(peak) = self.peak.as_mut() {
4338                peak.binding = false;
4339            }
4340            return;
4341        }
4342        let status = self.reconfigure(false, false);
4343        if status != NVENCSTATUS::NV_ENC_SUCCESS {
4344            self.encode_config.rcParams = saved;
4345            if bind {
4346                self.refused_bind = Some(bps);
4347            }
4348            if let Some(peak) = self.peak.as_mut() {
4349                peak.binding = !bind;
4350            }
4351            if !std::mem::replace(&mut self.hold_refused, true) {
4352                eprintln!(
4353                    "[NVENC] {} the peak of a capped constant quantizer refused ({status:?}): {}",
4354                    if bind { "Binding to" } else { "Releasing" },
4355                    last_error(&self.nvenc_funcs, self.encoder_session)
4356                );
4357            }
4358            return;
4359        }
4360        if let Some(peak) = self.peak.as_mut() {
4361            peak.binding = bind;
4362        }
4363    }
4364
4365    /// The level the live config takes at the session's frame rate and size.
4366    fn level_for_peak(&self) -> u32 {
4367        let (w, h) = (self.init_params.encodeWidth, self.init_params.encodeHeight);
4368        let fps = self
4369            .init_params
4370            .frameRateNum
4371            .div_ceil(self.init_params.frameRateDen.max(1));
4372        self.level_for(w, h, fps)
4373    }
4374
4375    /// Submit one picture and read its bitstream back behind the wire header: steps 1 to 4 of
4376    /// `submit_frame` past the choice of output buffer. A `held` picture leaves `last_quality` to
4377    /// the rate control's own frames.
4378    unsafe fn encode_picture(
4379        &mut self,
4380        pic_params: &mut NV_ENC_PIC_PARAMS,
4381        output_bitstream: NV_ENC_OUTPUT_PTR,
4382        frame_number: u64,
4383        held: bool,
4384        anchor: Option<u8>,
4385    ) -> Result<Vec<u8>, String> {
4386        let encode_fn = self.nvenc_funcs.nvEncEncodePicture.unwrap();
4387        let res = encode_fn(self.encoder_session, pic_params);
4388        if res != NVENCSTATUS::NV_ENC_SUCCESS {
4389            return Err(format!("Encode Picture failed: {:?}", res));
4390        }
4391        // A host upload's band hash asked for (`hash_next_upload`), queued once its encode is, so
4392        // it runs on the CUDA cores beside the encode engine and holds nothing up.
4393        if let Some(rows) = self.upload_hash.take() {
4394            let (src, pitch, width, height) = (
4395                self.input_device_ptr,
4396                self.input_pitch,
4397                self.width,
4398                self.height,
4399            );
4400            let cuda = &self.cuda;
4401            self.upload_words = self
4402                .band_hash
4403                .get_or_insert_with(|| BandHash::new(cuda))
4404                .as_mut()
4405                .and_then(|h| h.launch(cuda, src, pitch, width, height, rows));
4406        }
4407
4408        let mut negotiated = Negotiated::new(NV_ENC_LOCK_BITSTREAM {
4409            version: sv(NvStruct::LockBitstream),
4410            outputBitstream: output_bitstream,
4411            ..Default::default()
4412        });
4413        let lock_params = &mut negotiated.value;
4414        lock_params.set_doNotWait(0);
4415        let lock_fn = self.nvenc_funcs.nvEncLockBitstream.unwrap();
4416        let status = lock_fn(self.encoder_session, lock_params);
4417        if status != NVENCSTATUS::NV_ENC_SUCCESS {
4418            return Err(format!("Lock Bitstream failed: {status:?}"));
4419        }
4420
4421        let data_ptr = lock_params.bitstreamBufferPtr as *const u8;
4422        let data_size = lock_params.bitstreamSizeInBytes as usize;
4423        let header_sz = if self.omit_stripe_headers {
4424            0
4425        } else {
4426            VIDEO_HEADER_LEN
4427        };
4428        let mut output = Vec::with_capacity(header_sz + data_size);
4429        if !held {
4430            self.last_avg_qp = lock_params.frameAvgQP;
4431            self.last_quality = match lock_params.frameAvgQP {
4432                0 => None,
4433                q => Some(self.codec.hardware_quality_index(Hardware::Nvenc, q)),
4434            };
4435            self.last_bytes = Some(data_size);
4436        } else {
4437            self.held_bytes = data_size;
4438        }
4439        let frame_type = match lock_params.pictureType {
4440            NV_ENC_PIC_TYPE::NV_ENC_PIC_TYPE_IDR => FRAME_KEY,
4441            NV_ENC_PIC_TYPE::NV_ENC_PIC_TYPE_I => FRAME_INTRA,
4442            _ => FRAME_DELTA,
4443        };
4444        let mut wire_type = frame_type;
4445        if let Some(references) = &mut self.references {
4446            let shared = references.acknowledging() && references.predicts_from_shared();
4447            self.last_reference =
4448                references.record_marked(frame_number as u16, frame_type != FRAME_DELTA, anchor);
4449            if anchor.is_some() && frame_type == FRAME_DELTA && shared {
4450                wire_type |= FRAME_ANCHOR;
4451            }
4452        }
4453
4454        if !self.omit_stripe_headers {
4455            push_video_header(
4456                &mut output,
4457                self.codec,
4458                wire_type,
4459                frame_number as u16,
4460                0,
4461                self.width as u16,
4462                self.height as u16,
4463                self.last_reference,
4464            );
4465        }
4466
4467        if data_size > 0 && !data_ptr.is_null() {
4468            let slice = std::slice::from_raw_parts(data_ptr, data_size);
4469            if self.codec == Codec::H264 && self.references.is_some() {
4470                let mut widened = true;
4471                for nal in crate::encoders::codec::annexb_nals(slice) {
4472                    widened &= self.frame_num.push_unit(&[0, 0, 0, 1], nal, &mut output);
4473                }
4474                if !widened && let Some(references) = &mut self.references {
4475                    // Its slices no longer match the set they went out under: the next frame
4476                    // is a key frame.
4477                    references.reset();
4478                }
4479            } else {
4480                output.extend_from_slice(slice);
4481            }
4482        }
4483        if frame_type == FRAME_KEY
4484            && self.codec == Codec::H264
4485            && let Some(references) = &mut self.references
4486        {
4487            references.set_frame_num_range(h264_frame_num_range(&output[header_sz..]).unwrap_or(0));
4488        }
4489
4490        (self.nvenc_funcs.nvEncUnlockBitstream.unwrap())(self.encoder_session, output_bitstream);
4491        Ok(output)
4492    }
4493
4494    /// Encode a dmabuf frame zero-copy, by importing it through EGL into CUDA and, where the
4495    /// driver allows, handing the mapped plane to NVENC as its input.
4496    ///
4497    /// Applies any pending ConstQP change, then works under the pushed CUDA context:
4498    ///
4499    /// 1. **Import once, cache by fd with an identity check**: the cache is keyed by the dmabuf fd
4500    ///    but each entry stores the buffer's `DmaBufIdentity`; an entry whose identity no longer
4501    ///    matches (a recycled fd) is released first. On a miss, build an `EGLImageKHR` from the
4502    ///    dmabuf's fd / offset / pitch / modifier, register it as a CUDA graphics resource, map it to
4503    ///    a `CUeglFrame`, and settle how it feeds the encoder: a first plane that `direct_plane`
4504    ///    accepts (and `direct_dmabuf` on) is registered with NVENC in place — a pitch-linear plane
4505    ///    as a CUDA device pointer at its own pitch, a four-channel 8-bit CUDA array as a CUDA
4506    ///    array — in the byte order the dmabuf fourcc names, and mapped once (`DmaBufInput::Direct`);
4507    ///    any other plane, or a registration the driver refuses, takes `DmaBufInput::Copy`. The
4508    ///    result is memoized so a recurring capture buffer pays the import cost only once. Each
4509    ///    failure destroys what it created and pops the context.
4510    /// 2. **Feed the encoder**: a direct import is submitted as it is — no copy at all. A copy
4511    ///    import is copied with `cuMemcpy2DAsync` on the default stream — the array plane or the
4512    ///    pitch-linear plane, per `frame_type` — into the packed input surface, re-registered in the
4513    ///    dmabuf's byte order when it differs; NVENC processes its input on that same stream, so the
4514    ///    copy is ordered before the encode without a host wait.
4515    /// 3. **Submit** via `submit_frame`, then pop the context.
4516    ///
4517    /// The dmabuf fd is read out before the context is pushed so an early `?` return cannot leave the
4518    /// CUDA context stack imbalanced.
4519    pub fn encode(
4520        &mut self,
4521        dmabuf: &Dmabuf,
4522        frame_number: u64,
4523        crf: u32,
4524        force_idr: bool,
4525    ) -> Result<Vec<u8>, String> {
4526        self.encode_frame(dmabuf, frame_number, crf, force_idr, false)
4527    }
4528
4529    /// `encode`, for a buffer the X server is still blitting into: the frame's work is queued
4530    /// behind a wait on the semaphore the server signals once the blit has landed
4531    /// (`blit_semaphore_fd`), so it runs on the GPU right behind the blit instead of after the
4532    /// CPU has learnt of it. NVENC reads its input on the default stream, as the copy and the
4533    /// convert do, so one wait there orders all of it.
4534    pub fn encode_after_blit(
4535        &mut self,
4536        dmabuf: &Dmabuf,
4537        frame_number: u64,
4538        crf: u32,
4539        force_idr: bool,
4540    ) -> Result<Vec<u8>, String> {
4541        self.encode_frame(dmabuf, frame_number, crf, force_idr, true)
4542    }
4543
4544    /// A new semaphore for the X server to signal after a blit and `encode_after_blit` to wait
4545    /// on: made on this session's GPU, imported into its context in place of any earlier one,
4546    /// and returned as the fd the server imports.
4547    pub fn blit_semaphore_fd(&mut self) -> Result<std::os::fd::OwnedFd, String> {
4548        use std::os::fd::IntoRawFd;
4549        unsafe {
4550            let (Some(import), Some(_), Some(destroy)) = (
4551                self.cuda.cuImportExternalSemaphore,
4552                self.cuda.cuWaitExternalSemaphoresAsync,
4553                self.cuda.cuDestroyExternalSemaphore,
4554            ) else {
4555                return Err("the CUDA driver has no external semaphores".into());
4556            };
4557            let mut uuid = CUuuid { bytes: [0; 16] };
4558            if (self.cuda.cuDeviceGetUuid)(&mut uuid, self.cuda_device) != CUresult::CUDA_SUCCESS {
4559                return Err("the CUDA device has no UUID to find its Vulkan device by".into());
4560            }
4561            let (cuda_fd, server_fd) =
4562                super::blit_semaphore::opaque_fd_pair(uuid.bytes.map(|b| b.to_ne_bytes()[0]))?;
4563            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4564            if !self.blit_semaphore.is_null() {
4565                destroy(self.blit_semaphore);
4566                self.blit_semaphore = ptr::null_mut();
4567            }
4568            let mut desc = CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC {
4569                type_: CUexternalSemaphoreHandleType::CU_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD,
4570                ..Default::default()
4571            };
4572            desc.handle.fd = cuda_fd.as_raw_fd();
4573            let mut semaphore: CUexternalSemaphore = ptr::null_mut();
4574            let r = import(&mut semaphore, &desc);
4575            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4576            if r != CUresult::CUDA_SUCCESS {
4577                return Err(format!(
4578                    "CUDA refused the semaphore ({})",
4579                    Self::get_error_string(&self.cuda, r)
4580                ));
4581            }
4582            // A successful import owns its fd.
4583            let _ = cuda_fd.into_raw_fd();
4584            self.blit_semaphore = semaphore;
4585            Ok(server_fd)
4586        }
4587    }
4588
4589    fn encode_frame(
4590        &mut self,
4591        dmabuf: &Dmabuf,
4592        frame_number: u64,
4593        crf: u32,
4594        force_idr: bool,
4595        after_blit: bool,
4596    ) -> Result<Vec<u8>, String> {
4597        unsafe {
4598            self.reconfigure_if_needed(crf);
4599            let fd = dmabuf.handles().next().ok_or("No handles")?.as_raw_fd();
4600            let fmt = dmabuf.format();
4601            let modifier: u64 = fmt.modifier.into();
4602            let identity = DmaBufIdentity::probe(fd, modifier, self.width, self.height);
4603            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4604
4605            // First, before anything can fail: each signal the server was asked for has exactly
4606            // one wait, or the binary semaphore would be signalled again while still signalled.
4607            if after_blit {
4608                let wait = self.cuda.cuWaitExternalSemaphoresAsync;
4609                let queued = match wait {
4610                    Some(wait) if !self.blit_semaphore.is_null() => {
4611                        let params = CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS::default();
4612                        wait(&self.blit_semaphore, &params, 1, ptr::null_mut())
4613                    }
4614                    _ => CUresult::CUDA_ERROR_NOT_INITIALIZED,
4615                };
4616                if queued != CUresult::CUDA_SUCCESS {
4617                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4618                    return Err(format!(
4619                        "the wait for the server's blit could not be queued ({})",
4620                        Self::get_error_string(&self.cuda, queued)
4621                    ));
4622                }
4623            }
4624
4625            // A raw fd number is not an identity: the host recycles fd numbers across slot
4626            // renegotiations, so an entry whose stored identity no longer matches is torn down and
4627            // re-imported rather than returning a stale EGLImage for a buffer the fd no longer names.
4628            if self
4629                .dmabuf_cache
4630                .get(&fd)
4631                .is_some_and(|c| c.identity != identity)
4632                && let Some(stale) = self.dmabuf_cache.remove(&fd)
4633            {
4634                self.release_dmabuf_import(stale);
4635            }
4636
4637            if !self.dmabuf_cache.contains_key(&fd) {
4638                let Some(egl) = self.egl.clone() else {
4639                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4640                    return Err("a session opened without an EGL display imports no dmabuf".into());
4641                };
4642                let stride = dmabuf.strides().next().unwrap_or(0) as i32;
4643                let offset = dmabuf.offsets().next().unwrap_or(0) as i32;
4644
4645                let attribs = [
4646                    EGL_WIDTH,
4647                    self.width as i32,
4648                    EGL_HEIGHT,
4649                    self.height as i32,
4650                    EGL_LINUX_DRM_FOURCC_EXT,
4651                    egl_import_fourcc(fmt.code) as i32,
4652                    EGL_DMA_BUF_PLANE0_FD_EXT,
4653                    fd,
4654                    EGL_DMA_BUF_PLANE0_OFFSET_EXT,
4655                    offset,
4656                    EGL_DMA_BUF_PLANE0_PITCH_EXT,
4657                    stride,
4658                    EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
4659                    (modifier & 0xFFFFFFFF) as i32,
4660                    EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
4661                    (modifier >> 32) as i32,
4662                    EGL_NONE,
4663                ];
4664
4665                let egl_image = (egl.eglCreateImageKHR)(
4666                    self.egl_display,
4667                    ptr::null_mut(),
4668                    EGL_LINUX_DMA_BUF_EXT,
4669                    ptr::null_mut(),
4670                    attribs.as_ptr(),
4671                );
4672                if egl_image == EGL_NO_IMAGE_KHR {
4673                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4674                    return Err("Failed to create EGLImage".into());
4675                }
4676
4677                let mut cuda_resource: CUgraphicsResource = ptr::null_mut();
4678                if (self.cuda.cuGraphicsEGLRegisterImage)(&mut cuda_resource, egl_image, 1)
4679                    != CUresult::CUDA_SUCCESS
4680                {
4681                    (egl.eglDestroyImageKHR)(self.egl_display, egl_image);
4682                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4683                    return Err("Failed to register EGLImage".into());
4684                }
4685
4686                let mut egl_frame: CUeglFrame = std::mem::zeroed();
4687                if (self.cuda.cuGraphicsResourceGetMappedEglFrame)(
4688                    &mut egl_frame,
4689                    cuda_resource,
4690                    0,
4691                    0,
4692                ) != CUresult::CUDA_SUCCESS
4693                {
4694                    (self.cuda.cuGraphicsUnregisterResource)(cuda_resource);
4695                    (egl.eglDestroyImageKHR)(self.egl_display, egl_image);
4696                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4697                    return Err("Failed to map EGL frame".into());
4698                }
4699
4700                let input = match (
4701                    self.direct_dmabuf,
4702                    direct_plane(&egl_frame, self.width, self.height),
4703                    fourcc_nvenc_format(fmt.code),
4704                ) {
4705                    (true, Some(plane), Some(format)) => {
4706                        self.register_direct_input(&egl_frame, plane, format)
4707                    }
4708                    _ => DmaBufInput::Copy,
4709                };
4710                crate::log::debug!(
4711                    "[NVENC] dmabuf imported as a {} frame ({} planes, {}x{}, pitch {}, {} channels of element format {}): {}.",
4712                    match egl_frame.frame_type {
4713                        CU_EGL_FRAME_TYPE_PITCH => "pitch-linear",
4714                        CU_EGL_FRAME_TYPE_ARRAY => "CUDA-array",
4715                        _ => "unknown-kind",
4716                    },
4717                    egl_frame.plane_count,
4718                    egl_frame.width,
4719                    egl_frame.height,
4720                    egl_frame.pitch,
4721                    egl_frame.num_channels,
4722                    egl_frame.cu_format,
4723                    match input {
4724                        DmaBufInput::Direct { .. } => "encoding in place",
4725                        DmaBufInput::Copy => "copying per frame",
4726                    }
4727                );
4728
4729                // An array-typed import is read through a texture, so the convert never needs
4730                // it copied into linear memory.
4731                let tex = if self.csc.is_some() && egl_frame.frame_type == CU_EGL_FRAME_TYPE_ARRAY {
4732                    ChromaConvert::texture_for(&self.cuda, egl_frame.frame.p_array[0])
4733                } else {
4734                    0
4735                };
4736                if self.dmabuf_cache.len() >= DMABUF_IMPORTS
4737                    && let Some(&stalest) = self
4738                        .dmabuf_cache
4739                        .iter()
4740                        .min_by_key(|(_, c)| c.last_use)
4741                        .map(|(fd, _)| fd)
4742                    && let Some(stale) = self.dmabuf_cache.remove(&stalest)
4743                {
4744                    self.release_dmabuf_import(stale);
4745                }
4746                self.dmabuf_cache.insert(
4747                    fd,
4748                    CachedDmaBuf {
4749                        identity,
4750                        last_use: frame_number,
4751                        egl_image,
4752                        cuda_resource,
4753                        egl_frame,
4754                        input,
4755                        tex,
4756                    },
4757                );
4758            }
4759
4760            let (egl_frame, input, tex) = {
4761                let cached = self.dmabuf_cache.get_mut(&fd).unwrap();
4762                cached.last_use = frame_number;
4763                (cached.egl_frame, cached.input, cached.tex)
4764            };
4765            let (mapped, format) = match input {
4766                DmaBufInput::Direct { mapped, format, .. } => (mapped, format),
4767                DmaBufInput::Copy => {
4768                    let mut copy_params = CUDA_MEMCPY2D {
4769                        srcMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
4770                        srcHost: ptr::null(),
4771                        srcDevice: 0,
4772                        srcArray: ptr::null_mut(),
4773                        srcPitch: 0,
4774                        dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
4775                        dstHost: ptr::null_mut(),
4776                        dstDevice: self.input_device_ptr,
4777                        dstArray: ptr::null_mut(),
4778                        dstPitch: self.input_pitch,
4779                        WidthInBytes: (self.width * 4) as usize,
4780                        Height: self.height as usize,
4781                        ..Default::default()
4782                    };
4783                    if egl_frame.frame_type == CU_EGL_FRAME_TYPE_ARRAY {
4784                        copy_params.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_ARRAY;
4785                        copy_params.srcArray = egl_frame.frame.p_array[0];
4786                    } else {
4787                        copy_params.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_DEVICE;
4788                        copy_params.srcDevice = egl_frame.frame.p_pitch[0] as CUdeviceptr;
4789                        copy_params.srcPitch = egl_frame.pitch as usize;
4790                    }
4791                    // A fourcc without a packed NVENC equivalent keeps the surface's current
4792                    // registration; the copy still lands the bytes, as it always has.
4793                    if let Some(format) = fourcc_nvenc_format(fmt.code)
4794                        && let Err(e) = self.set_input_format(format)
4795                    {
4796                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4797                        return Err(e);
4798                    }
4799                    if (self.cuda.cuMemcpy2DAsync_v2)(&copy_params, ptr::null_mut())
4800                        != CUresult::CUDA_SUCCESS
4801                    {
4802                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4803                        return Err("Sanitization copy failed".into());
4804                    }
4805                    (self.mapped_input_buffer, self.input_format)
4806                }
4807            };
4808
4809            // The convert reads the frame where it already is: the import itself when the driver
4810            // mapped it pitch-linear or handed back an array a texture covers, the session's own
4811            // surface when the frame was copied into it. No case adds a copy of its own.
4812            let swap = format == NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR;
4813            let converted = match input {
4814                DmaBufInput::Copy => {
4815                    self.convert_packed(self.input_device_ptr, self.input_pitch, swap)
4816                }
4817                DmaBufInput::Direct { .. } if egl_frame.frame_type == CU_EGL_FRAME_TYPE_PITCH => {
4818                    self.convert_packed(
4819                        egl_frame.frame.p_pitch[0] as CUdeviceptr,
4820                        egl_frame.pitch as usize,
4821                        swap,
4822                    )
4823                }
4824                DmaBufInput::Direct { .. } => self.convert_texture(tex, swap),
4825            };
4826            let (mapped, format) = match converted {
4827                Ok(Some(converted)) => converted,
4828                Ok(None) => (mapped, format),
4829                Err(e) => {
4830                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4831                    return Err(e);
4832                }
4833            };
4834
4835            let result = self.submit_frame(mapped, format, frame_number, force_idr);
4836            if result.is_err() {
4837                (self.cuda.cuStreamSynchronize)(ptr::null_mut());
4838            }
4839            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4840            result
4841        }
4842    }
4843
4844    /// Register the first plane of a mapped dmabuf frame with NVENC in place — as a pitch-linear
4845    /// CUDA device pointer or as a CUDA array, per `plane` — and map it as an input, under the
4846    /// already-current CUDA context. Either step failing falls back to `DmaBufInput::Copy` — the
4847    /// per-frame copy then serves that import for as long as it is cached, so a driver that declines
4848    /// the direct path costs one failed registration, not a failed frame.
4849    unsafe fn register_direct_input(
4850        &mut self,
4851        frame: &CUeglFrame,
4852        plane: DirectPlane,
4853        format: NV_ENC_BUFFER_FORMAT,
4854    ) -> DmaBufInput {
4855        let (resource_type, resource, pitch) = match plane {
4856            DirectPlane::Pitch(pitch) => (
4857                NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
4858                frame.frame.p_pitch[0],
4859                pitch,
4860            ),
4861            DirectPlane::Array(row_bytes) => (
4862                NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDAARRAY,
4863                frame.frame.p_array[0] as *mut c_void,
4864                row_bytes,
4865            ),
4866        };
4867        let mut reg_res = NV_ENC_REGISTER_RESOURCE {
4868            version: sv(NvStruct::RegisterResource),
4869            resourceType: resource_type,
4870            width: self.width,
4871            height: self.height,
4872            resourceToRegister: resource,
4873            pitch,
4874            bufferFormat: format,
4875            bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
4876            ..Default::default()
4877        };
4878        let st =
4879            (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res);
4880        if st != NVENCSTATUS::NV_ENC_SUCCESS {
4881            eprintln!(
4882                "[NVENC] dmabuf plane registration ({plane:?}) refused ({st:?}); copying per frame."
4883            );
4884            return DmaBufInput::Copy;
4885        }
4886        let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
4887            version: sv(NvStruct::MapInputResource),
4888            registeredResource: reg_res.registeredResource,
4889            ..Default::default()
4890        };
4891        let st = (self.nvenc_funcs.nvEncMapInputResource.unwrap())(
4892            self.encoder_session,
4893            &mut map_params,
4894        );
4895        if st != NVENCSTATUS::NV_ENC_SUCCESS {
4896            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
4897                self.encoder_session,
4898                reg_res.registeredResource,
4899            );
4900            eprintln!("[NVENC] dmabuf plane mapping refused ({st:?}); copying per frame.");
4901            return DmaBufInput::Copy;
4902        }
4903        DmaBufInput::Direct {
4904            registered: reg_res.registeredResource,
4905            mapped: map_params.mappedResource,
4906            format,
4907        }
4908    }
4909
4910    /// Encode a host BGRA frame (B,G,R,A in memory, NVENC's word-ordered `ARGB` — the layout an
4911    /// XShm grab or the pixman framebuffer yields) through `encode_cpu_packed`.
4912    pub fn encode_cpu_argb(
4913        &mut self,
4914        argb: &[u8],
4915        src_stride: usize,
4916        frame_number: u64,
4917        crf: u32,
4918        force_idr: bool,
4919    ) -> Result<Vec<u8>, String> {
4920        self.encode_cpu_packed(argb, src_stride, false, frame_number, crf, force_idr)
4921    }
4922
4923    /// Encode a host packed-pixel frame by uploading it straight into the packed input surface,
4924    /// with no CPU-side color conversion: the surface is either the chroma convert's source or,
4925    /// where the kernel did not load, NVENC's own conversion's; a host prepass would cost this
4926    /// path its copy-free property.
4927    ///
4928    /// `rgba_input` names the byte order — `false` for B,G,R,A (X11 XShm, the pixman framebuffer),
4929    /// `true` for R,G,B,A (a GLES readback) — and the input surface is registered with NVENC in
4930    /// that order (`ARGB` / `ABGR`, re-registered in place when it changes), so both arrive at the
4931    /// hardware CSC untouched. `src_stride` is the source row stride in bytes (`>= width*4`).
4932    /// Steps, under the pushed CUDA context after any pending QP change:
4933    ///
4934    /// 1. **Bounds-check** the source against `stride × (rows-1) + width*4`, erroring rather than
4935    ///    reading past a short buffer.
4936    /// 2. **Pin the source once**: unless pinning was disabled at init (`PIXELFLUX_NVENC_PIN=0`, read
4937    ///    once into `pin_uploads`), page-lock each distinct source base address via `pin_host_source`
4938    ///    so the upload is a direct DMA from the caller's buffer instead of a pageable copy staged
4939    ///    through a driver bounce buffer. The persistent, bounded shm / pool sources make this a
4940    ///    one-time bounded cost.
4941    /// 3. **Upload and submit**: `cuMemcpy2DAsync` the rows into the input surface on the default
4942    ///    stream honoring `src_stride`, then `submit_frame`. NVENC processes its input on that same
4943    ///    stream, so the upload is ordered before the encode without a host wait, and the blocking
4944    ///    bitstream lock inside `submit_frame` (or the stream sync on its error path) guarantees the
4945    ///    upload has finished reading `pixels` by the time this returns — the caller may reuse the
4946    ///    buffer immediately.
4947    pub fn encode_cpu_packed(
4948        &mut self,
4949        pixels: &[u8],
4950        src_stride: usize,
4951        rgba_input: bool,
4952        frame_number: u64,
4953        crf: u32,
4954        force_idr: bool,
4955    ) -> Result<Vec<u8>, String> {
4956        unsafe {
4957            self.reconfigure_if_needed(crf);
4958            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4959
4960            let width_bytes = (self.width * 4) as usize;
4961            let rows = self.height as usize;
4962            let needed = if rows == 0 {
4963                0
4964            } else {
4965                src_stride * (rows - 1) + width_bytes
4966            };
4967            if src_stride < width_bytes || pixels.len() < needed {
4968                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4969                return Err(format!(
4970                    "packed buffer too small: len={} need>={} (stride={}, {}x{})",
4971                    pixels.len(),
4972                    needed,
4973                    src_stride,
4974                    self.width,
4975                    self.height
4976                ));
4977            }
4978
4979            let format = if rgba_input {
4980                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
4981            } else {
4982                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
4983            };
4984            // With the convert in place the packed surface is the kernel's source and needs no
4985            // registration of its own; NVENC is handed the NV12 the kernel writes.
4986            if self.csc.is_none()
4987                && let Err(e) = self.set_input_format(format)
4988            {
4989                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4990                return Err(e);
4991            }
4992
4993            if self.pin_uploads {
4994                self.pin_host_source(pixels.as_ptr() as usize, pixels.len());
4995            }
4996
4997            let copy = CUDA_MEMCPY2D {
4998                srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
4999                srcHost: pixels.as_ptr() as *const c_void,
5000                srcPitch: src_stride,
5001                dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
5002                dstDevice: self.input_device_ptr,
5003                dstPitch: self.input_pitch,
5004                WidthInBytes: width_bytes,
5005                Height: rows,
5006                ..Default::default()
5007            };
5008            let copied = (self.cuda.cuMemcpy2DAsync_v2)(&copy, ptr::null_mut());
5009            if copied != CUresult::CUDA_SUCCESS {
5010                (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
5011                return Err(format!(
5012                    "packed host->device upload failed ({}; host {:#x}+{} at stride {}, {}x{}, device pitch {}, pinned {})",
5013                    Self::get_error_string(&self.cuda, copied),
5014                    pixels.as_ptr() as usize,
5015                    pixels.len(),
5016                    src_stride,
5017                    self.width,
5018                    self.height,
5019                    self.input_pitch,
5020                    self.pin_uploads
5021                ));
5022            }
5023
5024            let (mapped, submitted) =
5025                match self.convert_packed(self.input_device_ptr, self.input_pitch, rgba_input) {
5026                    Ok(Some(converted)) => converted,
5027                    Ok(None) => (self.mapped_input_buffer, self.input_format),
5028                    Err(e) => {
5029                        (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
5030                        return Err(e);
5031                    }
5032                };
5033            let result = self.submit_frame(mapped, submitted, frame_number, force_idr);
5034            if result.is_err() {
5035                (self.cuda.cuStreamSynchronize)(ptr::null_mut());
5036            }
5037            self.upload_hash = None;
5038            if let Some(words) = self.upload_words.take()
5039                && let Some(Some(hash)) = self.band_hash.as_mut()
5040            {
5041                self.upload_hashes = hash.read(&self.cuda, words);
5042            }
5043            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
5044            result
5045        }
5046    }
5047
5048    /// `encode_cpu_packed` for rows a client owns, such as a wl_shm pool's, uploaded without
5049    /// page-locking them. The client unmaps its pool whenever it likes, and a registration
5050    /// outlives the mapping: a pool mapped later over the same addresses would be read from the
5051    /// old pool's pages, or fail to upload where it straddles them.
5052    pub fn encode_client_packed(
5053        &mut self,
5054        pixels: &[u8],
5055        src_stride: usize,
5056        rgba_input: bool,
5057        frame_number: u64,
5058        crf: u32,
5059        force_idr: bool,
5060    ) -> Result<Vec<u8>, String> {
5061        let pin = std::mem::replace(&mut self.pin_uploads, false);
5062        let out =
5063            self.encode_cpu_packed(pixels, src_stride, rgba_input, frame_number, crf, force_idr);
5064        self.pin_uploads = pin;
5065        out
5066    }
5067
5068    /// Run the chroma convert over an array-typed import through `tex`, answering the input
5069    /// NVENC should be handed. `None` where the session has no convert, or where the driver gave
5070    /// no texture for the import and NVENC's own conversion stands in, which a 10-bit session,
5071    /// initialized for the convert's surface, has none of.
5072    unsafe fn convert_texture(
5073        &self,
5074        tex: CUtexObject,
5075        rgba_input: bool,
5076    ) -> Result<Option<(NV_ENC_INPUT_PTR, NV_ENC_BUFFER_FORMAT)>, String> {
5077        match self.csc.as_ref() {
5078            Some(csc) if tex != 0 => {
5079                csc.run_texture(&self.cuda, tex, self.width, self.height, rgba_input)?;
5080                Ok(Some(csc.input()))
5081            }
5082            _ if self.bit_depth > 8 => {
5083                Err("the driver gave no texture for the import a 10-bit session converts".into())
5084            }
5085            _ => Ok(None),
5086        }
5087    }
5088
5089    /// Run the chroma convert over a packed surface, answering the input NVENC should be
5090    /// handed, or `None` where the session has no convert and encodes the packed surface itself.
5091    /// The caller holds the CUDA context current.
5092    unsafe fn convert_packed(
5093        &self,
5094        src: CUdeviceptr,
5095        src_pitch: usize,
5096        rgba_input: bool,
5097    ) -> Result<Option<(NV_ENC_INPUT_PTR, NV_ENC_BUFFER_FORMAT)>, String> {
5098        match self.csc.as_ref() {
5099            Some(csc) => {
5100                csc.run(
5101                    &self.cuda,
5102                    src,
5103                    src_pitch,
5104                    self.width,
5105                    self.height,
5106                    rgba_input,
5107                )?;
5108                Ok(Some(csc.input()))
5109            }
5110            None => Ok(None),
5111        }
5112    }
5113
5114    /// The geometry the session is currently initialized for.
5115    pub fn width(&self) -> u32 {
5116        self.width
5117    }
5118
5119    /// The geometry the session is currently initialized for.
5120    pub fn height(&self) -> u32 {
5121        self.height
5122    }
5123
5124    /// Make this session's CUDA context current on the calling thread, so a capture source can
5125    /// allocate its frames in the very context the encoder reads them from — the whole basis of
5126    /// a zero-copy hand-over. Paired with [`NvencEncoder::pop_context`].
5127    pub(crate) fn push_context(&self) -> bool {
5128        unsafe { (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context) == CUresult::CUDA_SUCCESS }
5129    }
5130
5131    /// Give back the context [`NvencEncoder::push_context`] made current.
5132    pub(crate) fn pop_context(&self) {
5133        unsafe {
5134            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
5135        }
5136    }
5137
5138    /// Drop the registration of an externally-owned input surface, under the pushed CUDA context.
5139    ///
5140    /// The caller owns the memory behind it, so this must run before that owner releases it: a
5141    /// registration outliving its buffer leaves the driver holding a mapping of freed video
5142    /// memory.
5143    pub fn release_external_input(&mut self) {
5144        if self.external_inputs.is_empty() {
5145            return;
5146        }
5147        unsafe {
5148            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
5149            self.unmap_external_inputs();
5150            let _ = (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
5151        }
5152    }
5153
5154    /// Unmap and unregister every external input, with the CUDA context already current.
5155    unsafe fn unmap_external_inputs(&mut self) {
5156        for ext in std::mem::take(&mut self.external_inputs) {
5157            unsafe { self.unmap_one_input(ext) };
5158        }
5159    }
5160
5161    unsafe fn unmap_one_input(&self, ext: ExternalInput) {
5162        (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(self.encoder_session, ext.mapped);
5163        (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(self.encoder_session, ext.registered);
5164    }
5165
5166    /// The mapped NVENC input for an externally-owned device pointer, registering it in place the
5167    /// first time and reusing that registration for every later frame from the same buffer.
5168    ///
5169    /// A capture source hands back one buffer, or a few in turn (NvFBC alternates between two on
5170    /// recent drivers), for as long as its geometry holds, so the register and map cost is paid
5171    /// once per buffer rather than per frame. Up to `EXTERNAL_INPUTS` stay registered; a pitch,
5172    /// format or geometry that changes releases the registrations made for the old one.
5173    unsafe fn register_external_input(
5174        &mut self,
5175        device_ptr: CUdeviceptr,
5176        pitch: usize,
5177        format: NV_ENC_BUFFER_FORMAT,
5178    ) -> Result<NV_ENC_INPUT_PTR, String> {
5179        let (width, height) = (self.width, self.height);
5180        let fits = |e: &ExternalInput| {
5181            e.pitch == pitch && e.format == format && e.width == width && e.height == height
5182        };
5183        if let Some(ext) = self
5184            .external_inputs
5185            .iter()
5186            .find(|e| e.device_ptr == device_ptr && fits(e))
5187        {
5188            return Ok(ext.mapped);
5189        }
5190        let (kept, stale): (Vec<_>, Vec<_>) = std::mem::take(&mut self.external_inputs)
5191            .into_iter()
5192            .partition(fits);
5193        self.external_inputs = kept;
5194        for ext in stale {
5195            unsafe { self.unmap_one_input(ext) };
5196        }
5197        if self.external_inputs.len() >= EXTERNAL_INPUTS {
5198            let oldest = self.external_inputs.remove(0);
5199            unsafe { self.unmap_one_input(oldest) };
5200        }
5201        let mut reg_res = NV_ENC_REGISTER_RESOURCE {
5202            version: sv(NvStruct::RegisterResource),
5203            resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
5204            width: self.width,
5205            height: self.height,
5206            resourceToRegister: device_ptr as *mut c_void,
5207            pitch: pitch as u32,
5208            bufferFormat: format,
5209            bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
5210            ..Default::default()
5211        };
5212        let st =
5213            (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res);
5214        if st != NVENCSTATUS::NV_ENC_SUCCESS {
5215            return Err(format!(
5216                "failed to register the captured frame as an NVENC input ({st:?})"
5217            ));
5218        }
5219        let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
5220            version: sv(NvStruct::MapInputResource),
5221            registeredResource: reg_res.registeredResource,
5222            ..Default::default()
5223        };
5224        let st = (self.nvenc_funcs.nvEncMapInputResource.unwrap())(
5225            self.encoder_session,
5226            &mut map_params,
5227        );
5228        if st != NVENCSTATUS::NV_ENC_SUCCESS {
5229            (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
5230                self.encoder_session,
5231                reg_res.registeredResource,
5232            );
5233            return Err(format!(
5234                "failed to map the captured frame as an NVENC input ({st:?})"
5235            ));
5236        }
5237        self.external_inputs.push(ExternalInput {
5238            device_ptr,
5239            pitch,
5240            width: self.width,
5241            height: self.height,
5242            format,
5243            registered: reg_res.registeredResource,
5244            mapped: map_params.mappedResource,
5245        });
5246        Ok(map_params.mappedResource)
5247    }
5248
5249    /// Encode a frame that already lives in video memory, reading it exactly where it was
5250    /// produced.
5251    ///
5252    /// This is the zero-copy hand-over: `device_ptr` addresses packed pixels in this session's own
5253    /// CUDA context — the X11 NvFBC capture buffer the NVIDIA driver composited the screen into —
5254    /// so no upload or `cuMemcpy` stands between the screen and the bitstream. The chroma convert
5255    /// reads that buffer where it lies, as it does for every other input, and a session without
5256    /// one hands the packed surface to NVENC's own conversion instead, registered in place.
5257    /// `rgba` names the byte order (`false` for the B,G,R,A the driver's native format delivers),
5258    /// and `pitch` is the buffer's row stride in bytes.
5259    ///
5260    /// The caller must keep the buffer alive and unmodified until this returns; the blocking
5261    /// bitstream lock inside means the encoder has finished reading by then, so the next capture
5262    /// may overwrite it.
5263    pub fn encode_cuda_pitch(
5264        &mut self,
5265        device_ptr: CUdeviceptr,
5266        pitch: usize,
5267        rgba: bool,
5268        frame_number: u64,
5269        crf: u32,
5270        force_idr: bool,
5271    ) -> Result<Vec<u8>, String> {
5272        unsafe {
5273            self.reconfigure_if_needed(crf);
5274            // The pitch has to cover the session's rows whichever conversion reads them: the
5275            // kernel indexes by it, and NVENC's own registration requires the 4-byte alignment.
5276            if pitch < self.width as usize * 4 || !pitch.is_multiple_of(4) {
5277                return Err(format!(
5278                    "external input pitch {pitch} does not cover {}x{} at 4-byte alignment",
5279                    self.width, self.height
5280                ));
5281            }
5282            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
5283            let format = if rgba {
5284                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
5285            } else {
5286                NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
5287            };
5288            let submitted = match self.convert_packed(device_ptr, pitch, rgba) {
5289                Ok(Some(converted)) => Ok(converted),
5290                Ok(None) => self
5291                    .register_external_input(device_ptr, pitch, format)
5292                    .map(|m| (m, format)),
5293                Err(e) => Err(e),
5294            };
5295            let (mapped, submitted) = match submitted {
5296                Ok(pair) => pair,
5297                Err(e) => {
5298                    (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
5299                    return Err(e);
5300                }
5301            };
5302            let result = self.submit_frame(mapped, submitted, frame_number, force_idr);
5303            if result.is_err() {
5304                (self.cuda.cuStreamSynchronize)(ptr::null_mut());
5305            }
5306            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
5307            result
5308        }
5309    }
5310
5311    /// Hash the next host frame's bands of `rows` rows on the device as it encodes, for
5312    /// `take_upload_hashes` to read after: the change detection a Turbo frame's paint-over
5313    /// reads, for the cost of a kernel beside the encode instead of a CPU pass over the frame.
5314    /// False where the driver refuses the kernel, which leaves the hash to the caller.
5315    pub fn hash_next_upload(&mut self, rows: u32) -> bool {
5316        self.upload_hashes = None;
5317        self.upload_words = None;
5318        let available = unsafe {
5319            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
5320            let cuda = &self.cuda;
5321            let loaded = self
5322                .band_hash
5323                .get_or_insert_with(|| BandHash::new(cuda))
5324                .is_some();
5325            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
5326            loaded
5327        };
5328        self.upload_hash = available.then_some(rows);
5329        available
5330    }
5331
5332    /// The band hashes of the host frame last coded where `hash_next_upload` asked for them.
5333    pub fn take_upload_hashes(&mut self) -> Option<Vec<u64>> {
5334        self.upload_hashes.take()
5335    }
5336
5337    /// The hash of each band of `rows` rows of the `width`x`height` packed frame at
5338    /// `device_ptr` (`pitch` bytes a row, in this session's context), computed where the frame
5339    /// lies: the change detection of a capture that reports none, NvFBC's. None where the driver
5340    /// refuses the kernel.
5341    pub fn band_hashes(
5342        &mut self,
5343        device_ptr: CUdeviceptr,
5344        pitch: usize,
5345        width: u32,
5346        height: u32,
5347        rows: u32,
5348    ) -> Option<Vec<u64>> {
5349        if pitch < width as usize * 4 {
5350            return None;
5351        }
5352        unsafe {
5353            let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
5354            let cuda = &self.cuda;
5355            let hashes = self
5356                .band_hash
5357                .get_or_insert_with(|| BandHash::new(cuda))
5358                .as_mut()
5359                .and_then(|h| h.run(cuda, device_ptr, pitch, width, height, rows));
5360            (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
5361            hashes
5362        }
5363    }
5364}
5365
5366#[cfg(test)]
5367mod tests {
5368    use super::*;
5369
5370    /// Every codec's arm declares the BT.709 matrix the session converts with, in both chroma
5371    /// formats and with no device involved. A stream whose pixels and signal disagree shifts
5372    /// every saturated color on the client, and only a GPU would otherwise report it.
5373    #[test]
5374    fn every_codec_declares_the_conversion_matrix() {
5375        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5376            for fullcolor in [false, true] {
5377                let mut config = NV_ENC_CONFIG {
5378                    version: sv(NvStruct::Config),
5379                    ..Default::default()
5380                };
5381                NvencEncoder::configure_codec(
5382                    &mut config,
5383                    codec,
5384                    fullcolor,
5385                    8,
5386                    nvenc_level(codec, 1280, 720, 60, 0, true),
5387                    1,
5388                    0,
5389                    &NvencTuning::default(),
5390                );
5391                let got = unsafe {
5392                    match codec {
5393                        Codec::H265 => {
5394                            config
5395                                .encodeCodecConfig
5396                                .hevcConfig
5397                                .hevcVUIParameters
5398                                .colourMatrix
5399                        }
5400                        Codec::Av1 => config.encodeCodecConfig.av1Config.matrixCoefficients,
5401                        _ => {
5402                            config
5403                                .encodeCodecConfig
5404                                .h264Config
5405                                .h264VUIParameters
5406                                .colourMatrix
5407                        }
5408                    }
5409                };
5410                assert_eq!(
5411                    got as u32,
5412                    NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709 as u32,
5413                    "{codec:?} 4:4:4={fullcolor}"
5414                );
5415            }
5416        }
5417    }
5418
5419    #[test]
5420    fn anchors_require_the_negotiated_api_and_device_capacity() {
5421        for acked in [false, true] {
5422            assert_eq!(anchor_count(Codec::Av1, 12, true, Some(6), 4, acked), 0);
5423            assert_eq!(anchor_count(Codec::Av1, 13, false, Some(6), 4, acked), 0);
5424            assert_eq!(anchor_count(Codec::Av1, 13, true, None, 4, acked), 0);
5425            assert_eq!(anchor_count(Codec::Av1, 13, true, Some(0), 4, acked), 0);
5426            assert_eq!(anchor_count(Codec::Av1, 13, true, Some(6), 2, acked), 0);
5427            assert_eq!(anchor_count(Codec::Av1, 13, true, Some(1), 3, acked), 1);
5428            for api in [11, 12, 13] {
5429                assert_eq!(
5430                    anchor_count(Codec::H265, api, true, Some(8), 4, acked),
5431                    ANCHORS
5432                );
5433                for codec in [Codec::H264, Codec::H265] {
5434                    assert_eq!(anchor_count(codec, api, false, Some(8), 4, acked), 0);
5435                    assert_eq!(anchor_count(codec, api, true, Some(0), 4, acked), 0);
5436                    assert_eq!(anchor_count(codec, api, true, Some(8), 2, acked), 0);
5437                }
5438            }
5439        }
5440        assert_eq!(anchor_count(Codec::Av1, 13, true, Some(6), 4, false), 1);
5441        assert_eq!(
5442            anchor_count(Codec::Av1, 13, true, Some(6), 4, true),
5443            ANCHORS
5444        );
5445        for api in [11, 12, 13] {
5446            for acked in [false, true] {
5447                assert_eq!(anchor_count(Codec::H264, api, true, Some(8), 4, acked), 1);
5448            }
5449        }
5450        // No room for two: the one an unacknowledged session keeps.
5451        assert_eq!(anchor_count(Codec::Av1, 13, true, Some(8), 3, true), 1);
5452        assert_eq!(anchor_count(Codec::Av1, 13, true, Some(1), 4, true), 1);
5453    }
5454
5455    /// AV1's persistent reference must be enabled in its own codec arm; merely keeping an
5456    /// anchor in ReferenceWindow would report a dependency the device does not retain.
5457    #[test]
5458    fn av1_anchor_configuration_matches_the_reference_window() {
5459        for anchors in [0, 1] {
5460            let mut config = NV_ENC_CONFIG::default();
5461            NvencEncoder::configure_codec(
5462                &mut config,
5463                Codec::Av1,
5464                false,
5465                8,
5466                13,
5467                AV1_REFERENCES,
5468                anchors,
5469                &NvencTuning::default(),
5470            );
5471            let av1 = unsafe { config.encodeCodecConfig.av1Config };
5472            assert_eq!(av1.enableLTR(), anchors as u32);
5473            assert_eq!(av1.ltrNumFrames, anchors as u32);
5474            assert_eq!(av1.maxNumRefFramesInDPB, AV1_REFERENCES);
5475        }
5476    }
5477
5478    /// Frames an anchor's settling leaves out of every prediction take the bands they held with
5479    /// them: the band the anchor holds reaches back over theirs, and an anchor that holds none
5480    /// loses them to the cleanup; bands before them stay as they are.
5481    #[test]
5482    fn bands_left_out_of_the_predictions_are_coded_again() {
5483        let sweep = || VecDeque::from([(10, 0.0, 0.1), (11, 0.1, 0.2), (12, 0.2, 0.3)]);
5484        let mut log = sweep();
5485        assert_eq!(
5486            reach_back(&mut log, 11, Some((0.3, 0.4)), true),
5487            (Some((0.1, 0.4)), false)
5488        );
5489        assert_eq!(log, VecDeque::from([(10, 0.0, 0.1)]));
5490        assert_eq!(
5491            reach_back(&mut sweep(), 11, None, false),
5492            (None, true),
5493            "a frame holding nothing loses them"
5494        );
5495        assert_eq!(
5496            reach_back(&mut sweep(), 11, None, true),
5497            (None, false),
5498            "a frame held whole codes them"
5499        );
5500        assert_eq!(
5501            reach_back(&mut sweep(), 13, Some((0.3, 0.4)), true),
5502            (Some((0.3, 0.4)), false),
5503            "no band among the frames left out"
5504        );
5505    }
5506
5507    /// A band's share of the picture lands on the blocks it covers, rounded outward, so the bands
5508    /// of a sweep leave no block between them, and nowhere else.
5509    #[test]
5510    fn a_band_covers_its_share_of_the_blocks() {
5511        let mut map = Vec::new();
5512        fill_band_map(&mut map, Codec::H264, 1920, 1080, (0.0, 1.0 / 64.0), -33);
5513        assert_eq!(map.len(), 120 * 68);
5514        assert_eq!(map.iter().filter(|&&d| d == -33).count(), 128);
5515        assert!(map[..128].iter().all(|&d| d == -33) && map[128..].iter().all(|&d| d == 0));
5516        fill_band_map(&mut map, Codec::H265, 1920, 1080, (0.5, 0.75), -80);
5517        assert_eq!(map.len(), 60 * 34);
5518        assert!(map[..1020].iter().all(|&d| d == 0) && map[1530..].iter().all(|&d| d == 0));
5519        assert!(
5520            map[1020..1530].iter().all(|&d| d == -51),
5521            "the delta is bounded to the quantizer range"
5522        );
5523        fill_band_map(&mut map, Codec::Av1, 1920, 1080, (0.0, 1.0), -211);
5524        assert_eq!(map.len(), 30 * 17);
5525        assert!(
5526            map.iter().all(|&d| d == -128),
5527            "an AV1 delta is bounded to what the map holds"
5528        );
5529        let covered = |a: f64, b: f64| {
5530            let mut m = Vec::new();
5531            fill_band_map(&mut m, Codec::H264, 1280, 720, (a, b), -1);
5532            m.iter().map(|&d| d != 0).collect::<Vec<bool>>()
5533        };
5534        let (x, y) = (covered(0.0, 0.3), covered(0.3, 1.0));
5535        assert!(
5536            x.iter().zip(&y).all(|(a, b)| *a || *b),
5537            "adjacent bands leave no block out"
5538        );
5539    }
5540}
5541
5542#[cfg(test)]
5543pub(crate) mod gpu_tests {
5544    use super::*;
5545    use crate::encoders::reference::REFERENCE_FRAMES;
5546
5547    /// Test helper: H.264 full-frame capture settings at `w×h`, `fps`, CRF 25.
5548    fn settings(w: i32, h: i32, fps: f64) -> CaptureSettings {
5549        CaptureSettings {
5550            width: w,
5551            height: h,
5552            codec: Codec::H264,
5553            target_fps: fps,
5554            video_crf: 25,
5555            ..Default::default()
5556        }
5557    }
5558
5559    /// Test helper: a session for checks that hand it a fresh buffer every frame, which it pins
5560    /// none of: a registration outlives the buffer it page-locked, and a later buffer at an
5561    /// overlapping address then fails to upload, or uploads the pages of the freed one, where a
5562    /// capture's persistent shm is released on every reshape.
5563    fn host_session(s: &CaptureSettings) -> Result<NvencEncoder, String> {
5564        NvencEncoder::new(s, ptr::null()).map(|mut enc| {
5565            enc.pin_uploads = false;
5566            enc
5567        })
5568    }
5569
5570    /// Test helper: `host_session`, or `None` where the device has no engine for the codec (AV1
5571    /// before Ada, HEVC on Kepler) or for its 4:4:4 (Kepler), which the caller skips.
5572    fn codec_session(s: &CaptureSettings) -> Option<NvencEncoder> {
5573        match host_session(s) {
5574            Ok(enc) => Some(enc),
5575            Err(e) if e.contains("engine") => {
5576                println!("{:?}: {e}", s.codec);
5577                None
5578            }
5579            Err(e) => panic!("{:?} NVENC init: {e}", s.codec),
5580        }
5581    }
5582
5583    /// Test helper: a `w×h` BGRA frame filled with a hashed gradient (offset by `seed`) so
5584    /// the content has structure and encodes are non-trivial.
5585    fn frame(w: usize, h: usize, seed: u8) -> Vec<u8> {
5586        let mut f = vec![0u8; w * h * 4];
5587        for (i, px) in f.as_chunks_mut::<4>().0.iter_mut().enumerate() {
5588            let v = ((i as u32).wrapping_mul(2654435761) >> 24) as u8;
5589            px[0] = v.wrapping_add(seed);
5590            px[1] = v ^ seed;
5591            px[2] = seed;
5592            px[3] = 255;
5593        }
5594        f
5595    }
5596
5597    /// Test helper: hand the session's `encode_config` to `nvEncReconfigureEncoder` as it stands,
5598    /// for checks that vary one rate-control field, with the driver's answer.
5599    fn reconfigure_raw(enc: &mut NvencEncoder) -> (NVENCSTATUS, String) {
5600        unsafe {
5601            let status = enc.reconfigure(false, false);
5602            let detail = if status == NVENCSTATUS::NV_ENC_SUCCESS {
5603                "accepted".to_string()
5604            } else {
5605                last_error(&enc.nvenc_funcs, enc.encoder_session)
5606            };
5607            (status, detail)
5608        }
5609    }
5610
5611    /// Test helper: how far apart, in mean luma, the last picture of `frames` decodes with and
5612    /// without the frames at the `lost` indices.
5613    fn apart_without(codec: Codec, frames: &[Vec<u8>], lost: std::ops::Range<usize>) -> f64 {
5614        use crate::webcam::decode::{Decoder as _, VideoDecoder};
5615        let (mut whole, mut lossy) = (
5616            VideoDecoder::new(codec).unwrap(),
5617            VideoDecoder::new(codec).unwrap(),
5618        );
5619        for (i, f) in frames.iter().enumerate() {
5620            assert!(whole.decode(f).expect("decode"), "{codec:?} frame {i}");
5621            if !lost.contains(&i) {
5622                assert!(
5623                    lossy.decode(f).expect("decode past the loss"),
5624                    "{codec:?} frame {i}"
5625                );
5626            }
5627        }
5628        luma_apart(&whole.frame().unwrap(), &lossy.frame().unwrap())
5629    }
5630
5631    /// Test helper: the mean luma distance between two decoded pictures.
5632    fn luma_apart(
5633        a: &crate::webcam::convert::I420View<'_>,
5634        b: &crate::webcam::convert::I420View<'_>,
5635    ) -> f64 {
5636        a.y.chunks(a.y_stride)
5637            .zip(b.y.chunks(b.y_stride))
5638            .take(a.height)
5639            .flat_map(|(ra, rb)| {
5640                ra[..a.width]
5641                    .iter()
5642                    .zip(&rb[..a.width])
5643                    .map(|(&x, &y)| (f64::from(x) - f64::from(y)).abs())
5644            })
5645            .sum::<f64>()
5646            / (a.width * a.height) as f64
5647    }
5648
5649    /// Test helper: the mean luma distance between the last picture decoded from every frame and
5650    /// the last one decoded from the frames at the `keep` indices alone; None where those left the
5651    /// last one undecodable.
5652    fn apart_keeping(codec: Codec, frames: &[Vec<u8>], keep: &[usize]) -> Option<f64> {
5653        use crate::webcam::decode::{Decoder as _, VideoDecoder};
5654        let (mut whole, mut part) = (
5655            VideoDecoder::new(codec).unwrap(),
5656            VideoDecoder::new(codec).unwrap(),
5657        );
5658        let mut got = false;
5659        for (i, f) in frames.iter().enumerate() {
5660            let _ = whole.decode(f);
5661            if keep.contains(&i) {
5662                got = part.decode(f).unwrap_or(false) && i == frames.len() - 1;
5663            }
5664        }
5665        if !got {
5666            return None;
5667        }
5668        Some(luma_apart(&whole.frame()?, &part.frame()?))
5669    }
5670
5671    /// Test helper: the gradient frame with a 256x256 block of another gradient moved `step`
5672    /// blocks along its top rows, the frames of a steady desktop-like sequence.
5673    fn moving_frame(w: usize, h: usize, step: usize) -> Vec<u8> {
5674        let mut f = frame(w, h, 10);
5675        let block = frame(256, 256, 200);
5676        let x0 = (step * 64) % (w - 256);
5677        for row in 0..256.min(h) {
5678            let dst = (row * w + x0) * 4;
5679            f[dst..dst + 256 * 4].copy_from_slice(&block[row * 256 * 4..(row + 1) * 256 * 4]);
5680        }
5681        f
5682    }
5683
5684    /// Test helper: a NAL unit with its emulation-prevention bytes removed.
5685    fn rbsp(nal: &[u8]) -> Vec<u8> {
5686        let mut out = Vec::with_capacity(nal.len());
5687        let mut zeros = 0usize;
5688        for &b in nal {
5689            if zeros >= 2 && b == 3 {
5690                zeros = 0;
5691                continue;
5692            }
5693            zeros = if b == 0 { zeros + 1 } else { 0 };
5694            out.push(b);
5695        }
5696        out
5697    }
5698
5699    /// Test helper: luma PSNR of one decoded access unit against the BGRA frame it encodes, the
5700    /// source taken through the limited-range BT.709 luma the session converts with.
5701    fn luma_psnr(
5702        dec: &mut crate::webcam::decode::VideoDecoder,
5703        pkt: &[u8],
5704        src: &[u8],
5705        w: usize,
5706        h: usize,
5707    ) -> f64 {
5708        use crate::webcam::decode::Decoder;
5709        assert!(
5710            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
5711            "no picture"
5712        );
5713        let v = dec.frame().expect("decoded frame");
5714        let mut se = 0f64;
5715        for y in 0..h {
5716            for x in 0..w {
5717                let p = &src[(y * w + x) * 4..(y * w + x) * 4 + 3];
5718                let luma = crate::encoders::chroma_siting::ycbcr(
5719                    [p[2], p[1], p[0]].map(f64::from),
5720                    crate::encoders::chroma_siting::BT709,
5721                )[0];
5722                let d = f64::from(v.y[y * v.y_stride + x]) - luma;
5723                se += d * d;
5724            }
5725        }
5726        let mse = se / (w * h) as f64;
5727        if mse == 0.0 {
5728            99.0
5729        } else {
5730            10.0 * (255.0f64 * 255.0 / mse).log10()
5731        }
5732    }
5733
5734    /// Test helper: one CBR bench row. Encodes `seq` on `enc`, its first frame as the warm-up key
5735    /// frame, and prints the achieved rate at `fps`, the smallest and largest frame, and the luma
5736    /// PSNR of every decoded frame against its source.
5737    fn cbr_row(
5738        label: &str,
5739        enc: &mut NvencEncoder,
5740        codec: Codec,
5741        seq: &[&Vec<u8>],
5742        w: usize,
5743        h: usize,
5744        fps: usize,
5745    ) {
5746        use crate::webcam::decode::VideoDecoder;
5747        let n = seq.len() - 1;
5748        let first = enc
5749            .encode_cpu_packed(seq[0], w * 4, false, 0, 25, true)
5750            .expect("warm-up");
5751        let mut pkts: Vec<Vec<u8>> = Vec::with_capacity(n);
5752        per_frame(label, n, |i| {
5753            pkts.push(
5754                enc.encode_cpu_packed(seq[1 + i], w * 4, false, 1 + i as u64, 25, false)
5755                    .expect("encode"),
5756            );
5757        });
5758        let sizes: Vec<usize> = pkts
5759            .iter()
5760            .map(|p| p.len().saturating_sub(VIDEO_HEADER_LEN))
5761            .collect();
5762        let bytes: usize = sizes.iter().sum();
5763        let mut dec = VideoDecoder::new(codec).expect("decoder");
5764        luma_psnr(&mut dec, &first, seq[0], w, h);
5765        let psnr: Vec<f64> = pkts
5766            .iter()
5767            .enumerate()
5768            .map(|(i, p)| luma_psnr(&mut dec, p, seq[1 + i], w, h))
5769            .collect();
5770        println!(
5771            "    {} kbps, frames {}..{} kbit, luma PSNR {:.1} dB mean, {:.1} dB worst",
5772            bytes * 8 * fps / n / 1000,
5773            sizes.iter().min().unwrap() * 8 / 1000,
5774            sizes.iter().max().unwrap() * 8 / 1000,
5775            psnr.iter().sum::<f64>() / n as f64,
5776            psnr.iter().copied().fold(f64::INFINITY, f64::min),
5777        );
5778    }
5779
5780    /// Test helper: read the big-endian width/height (bytes 6-9) from the wire header.
5781    fn wire_dims(pkt: &[u8]) -> (u16, u16) {
5782        (
5783            u16::from_be_bytes([pkt[6], pkt[7]]),
5784            u16::from_be_bytes([pkt[8], pkt[9]]),
5785        )
5786    }
5787
5788    /// End-to-end in-place resize on a real GPU: encode 720p, grow to 1080p and verify the
5789    /// first post-resize frame is an IDR (steady frames are P), shrink to 480p, exercise the
5790    /// rejection cases (beyond headroom, chroma flip, RC-mode flip) and confirm the session still
5791    /// encodes afterward, and optionally dump a decodable stream. Ignored by default; run with
5792    /// `cargo test gpu_ -- --ignored --nocapture --test-threads=1` — building NVENC sessions
5793    /// concurrently races in the driver and intermittently faults, so the GPU set runs serially.
5794    #[test]
5795    #[ignore]
5796    fn gpu_resolution_reconfigure_roundtrip() {
5797        let mut s = settings(1280, 720, 60.0);
5798        let t0 = std::time::Instant::now();
5799        let mut enc = host_session(&s).expect("NVENC init");
5800        let init_ms = t0.elapsed().as_secs_f64() * 1000.0;
5801
5802        let mut stream: Vec<u8> = Vec::new();
5803        let f720 = frame(1280, 720, 10);
5804        for i in 0..5u64 {
5805            let pkt = enc
5806                .encode_cpu_argb(&f720, 1280 * 4, i, 25, i == 0)
5807                .expect("encode 720p");
5808            assert_eq!(wire_dims(&pkt), (1280, 720));
5809            stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5810        }
5811
5812        assert!(
5813            !enc.reconfigure_resolution(&s)
5814                .expect("same-size reconfigure"),
5815            "unchanged dimensions must not reset the session"
5816        );
5817        let pkt = enc
5818            .encode_cpu_argb(&f720, 1280 * 4, 5, 25, false)
5819            .expect("encode after same-size reconfigure");
5820        assert_eq!(
5821            pkt[1] & 0x0f,
5822            FRAME_DELTA,
5823            "the stream continues without an IDR at unchanged dimensions"
5824        );
5825        assert!(
5826            pkt.len() > VIDEO_HEADER_LEN,
5827            "a locked bitstream carries the encoded picture"
5828        );
5829        stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5830
5831        s.width = 1920;
5832        s.height = 1080;
5833        let t1 = std::time::Instant::now();
5834        assert!(enc.reconfigure_resolution(&s).expect("grow reconfigure"));
5835        let grow_ms = t1.elapsed().as_secs_f64() * 1000.0;
5836        let f1080 = frame(1920, 1080, 40);
5837        let pkt = enc
5838            .encode_cpu_argb(&f1080, 1920 * 4, 6, 25, false)
5839            .expect("encode 1080p");
5840        assert_eq!(pkt[0], 0x04);
5841        assert_eq!(
5842            pkt[1] & 0x0f,
5843            FRAME_KEY,
5844            "first frame after a resize must be an IDR"
5845        );
5846        assert_eq!(wire_dims(&pkt), (1920, 1080));
5847        assert!(
5848            pkt.len() > VIDEO_HEADER_LEN,
5849            "a locked bitstream carries the encoded picture"
5850        );
5851        stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5852        for i in 7..10u64 {
5853            let pkt = enc
5854                .encode_cpu_argb(&f1080, 1920 * 4, i, 25, false)
5855                .expect("encode 1080p");
5856            assert_eq!(
5857                pkt[1] & 0x0f,
5858                FRAME_DELTA,
5859                "steady frames after the IDR are P frames"
5860            );
5861            stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5862        }
5863
5864        s.width = 640;
5865        s.height = 480;
5866        let t2 = std::time::Instant::now();
5867        assert!(enc.reconfigure_resolution(&s).expect("shrink reconfigure"));
5868        let shrink_ms = t2.elapsed().as_secs_f64() * 1000.0;
5869        let f480 = frame(640, 480, 70);
5870        let pkt = enc
5871            .encode_cpu_argb(&f480, 640 * 4, 10, 25, false)
5872            .expect("encode 480p");
5873        assert_eq!(pkt[1] & 0x0f, FRAME_KEY);
5874        assert_eq!(wire_dims(&pkt), (640, 480));
5875        stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5876
5877        s.width = 4100;
5878        s.height = 2400;
5879        assert!(enc.reconfigure_resolution(&s).is_err(), "beyond headroom");
5880        s.width = 640;
5881        s.height = 480;
5882        s.video_fullcolor = true;
5883        assert!(enc.reconfigure_resolution(&s).is_err(), "chroma flip");
5884        s.video_fullcolor = false;
5885        s.video_cbr_mode = true;
5886        assert!(enc.reconfigure_resolution(&s).is_err(), "RC mode flip");
5887        s.video_cbr_mode = false;
5888        let pkt = enc
5889            .encode_cpu_argb(&f480, 640 * 4, 11, 25, false)
5890            .expect("session survives rejected reconfigures");
5891        stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5892
5893        println!(
5894            "init={init_ms:.1}ms grow(720p->1080p)={grow_ms:.1}ms shrink(1080p->480p)={shrink_ms:.1}ms"
5895        );
5896        if let Ok(path) = std::env::var("NVENC_TEST_DUMP") {
5897            std::fs::write(&path, &stream).unwrap();
5898            println!("wrote {} bytes to {path}", stream.len());
5899        }
5900    }
5901
5902    /// On a real GPU: every codec the device lists an engine for comes up on host frames
5903    /// without loading EGL, frames it emits carry the codec's wire id and a kind the
5904    /// bitstream agrees with, decode back
5905    /// to the painted picture, and a key frame forced mid-stream starts a fresh decoder on
5906    /// its own; a codec the device lacks (AV1 before Ada) is refused with a message naming
5907    /// it. HEVC 4:4:4 is taken where the device carries it, AV1 never quietly. Ignored by
5908    /// default.
5909    #[test]
5910    #[ignore]
5911    fn gpu_codec_sessions_encode_and_decode() {
5912        use crate::encoders::codec::{
5913            FRAME_DELTA, FRAME_KEY, av1_is_key, h264_frame_type, h265_frame_type, parse_video_type,
5914        };
5915        use crate::webcam::decode::{Decoder, VideoDecoder};
5916        let (w, h) = (1280usize, 720usize);
5917        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5918            let mut s = settings(w as i32, h as i32, 60.0);
5919            s.codec = codec;
5920            let mut enc = match host_session(&s) {
5921                Ok(enc) => enc,
5922                Err(e) => {
5923                    println!("{codec:?}: {e}");
5924                    assert!(
5925                        e.contains("engine"),
5926                        "a missing codec must be refused as such: {e}"
5927                    );
5928                    continue;
5929                }
5930            };
5931            assert_eq!(enc.codec(), codec);
5932            assert!(enc.egl.is_none(), "a session on host frames loads no EGL");
5933            let mut dec = VideoDecoder::new(codec).expect("decoder");
5934            for i in 0..6u64 {
5935                let src = frame(w, h, 10 + i as u8);
5936                let pkt = enc
5937                    .encode_cpu_argb(&src, w * 4, i, 25, i == 0)
5938                    .expect("encode");
5939                let (wire_codec, kind) = parse_video_type(pkt[1]).expect("video type byte");
5940                assert_eq!(wire_codec, codec);
5941                assert_eq!(
5942                    kind,
5943                    if i == 0 { FRAME_KEY } else { FRAME_DELTA },
5944                    "{codec:?} frame {i}"
5945                );
5946                let payload = &pkt[VIDEO_HEADER_LEN..];
5947                if codec != Codec::Av1 {
5948                    assert!(
5949                        payload.starts_with(&[0, 0, 0, 1]),
5950                        "{codec:?} frame {i}: the bitstream starts right after the header"
5951                    );
5952                }
5953                let read = match codec {
5954                    Codec::H264 => h264_frame_type(payload),
5955                    Codec::H265 => h265_frame_type(payload),
5956                    _ => {
5957                        if av1_is_key(payload) {
5958                            FRAME_KEY
5959                        } else {
5960                            FRAME_DELTA
5961                        }
5962                    }
5963                };
5964                assert_eq!(
5965                    read, kind,
5966                    "{codec:?} frame {i}: the bitstream disagrees with pictureType"
5967                );
5968                assert!(
5969                    dec.decode(payload).expect("decode"),
5970                    "{codec:?} frame {i} decoded nothing"
5971                );
5972                let f = dec.frame().unwrap();
5973                assert_eq!((f.width, f.height), (w, h));
5974            }
5975            let key = enc
5976                .encode_cpu_argb(&frame(w, h, 99), w * 4, 6, 25, true)
5977                .expect("forced key");
5978            assert_eq!(parse_video_type(key[1]), Some((codec, FRAME_KEY)));
5979            let mut fresh = VideoDecoder::new(codec).expect("decoder");
5980            assert!(
5981                fresh.decode(&key[VIDEO_HEADER_LEN..]).expect("decode"),
5982                "{codec:?}: a forced key frame must decode alone"
5983            );
5984
5985            let mut full = s.clone();
5986            full.video_fullcolor = true;
5987            let Some(enc) = codec_session(&full) else {
5988                continue;
5989            };
5990            if codec.fullcolor() {
5991                println!("{codec:?} 4:4:4: {}", enc.is_fullcolor());
5992            } else {
5993                assert!(!enc.is_fullcolor(), "{codec:?} never carries 4:4:4");
5994            }
5995        }
5996    }
5997
5998    /// On a real GPU: a 10-bit request is coded at 10 bits in each format the probe lists them
5999    /// for and at 8 in every other, through the convert that writes the 10-bit surface, and an
6000    /// in-place resize keeps it. The decoders here take 8-bit streams alone, so a 10-bit key
6001    /// frame is one they refuse for its depth. Ignored by default.
6002    #[test]
6003    #[ignore]
6004    fn gpu_ten_bit_follows_the_device() {
6005        use crate::webcam::decode::{Decoder, VideoDecoder};
6006        let (w, h) = (1280usize, 720usize);
6007        for (codec, formats) in probe_codecs(0).expect("probe") {
6008            for fullcolor in [false, codec.fullcolor()] {
6009                let mut s = settings(w as i32, h as i32, 60.0);
6010                s.codec = codec;
6011                s.video_fullcolor = fullcolor;
6012                s.video_bit_depth = 10;
6013                let Some(mut enc) = codec_session(&s) else {
6014                    continue;
6015                };
6016                let carried = formats.ten_bit[usize::from(fullcolor && formats.fullcolor)];
6017                println!("{codec:?} 4:4:4 {fullcolor}: {}-bit", enc.bit_depth());
6018                assert_eq!(enc.bit_depth(), if carried { 10 } else { 8 }, "{codec:?}");
6019                assert_eq!(
6020                    enc.csc.as_ref().map(|c| c.layout),
6021                    ConvertLayout::of(enc.is_fullcolor(), enc.bit_depth()),
6022                    "{codec:?}"
6023                );
6024                let key = enc
6025                    .encode_cpu_argb(&frame(w, h, 1), w * 4, 0, 25, true)
6026                    .expect("key frame");
6027                let decoded = VideoDecoder::new(codec)
6028                    .expect("decoder")
6029                    .decode(&key[VIDEO_HEADER_LEN..]);
6030                if carried {
6031                    let refusal = format!("{:?}", decoded.expect_err("an 8-bit decoder"));
6032                    assert!(refusal.contains("bit depth"), "{codec:?}: {refusal}");
6033                } else if !enc.is_fullcolor() {
6034                    assert!(decoded.expect("decode"), "{codec:?}");
6035                }
6036                s.width = 1920;
6037                s.height = 1080;
6038                enc.reconfigure_resolution(&s).expect("resize");
6039                assert_eq!(enc.bit_depth(), if carried { 10 } else { 8 });
6040                enc.encode_cpu_argb(&frame(1920, 1080, 2), 1920 * 4, 1, 25, false)
6041                    .expect("a frame after the resize");
6042                s.video_bit_depth = 8;
6043                assert_eq!(
6044                    enc.reconfigure_resolution(&s).is_err(),
6045                    carried,
6046                    "{codec:?}: a depth change rebuilds the session"
6047                );
6048            }
6049        }
6050    }
6051
6052    /// A frame a client lost is left out of the device's predictions: the next frame predicts
6053    /// from the newest frame before it and names it, a decoder that never saw the lost frames
6054    /// decodes it as one that saw everything does, the stream declares the decoded picture
6055    /// buffer the level admits, and an in-place resize redeclares it: a loss as deep as the new
6056    /// buffer's recent frames reach is still predicted past, a deeper one from the key frame the
6057    /// resize forced where an anchor is kept, and losing the last reference costs a key frame. A
6058    /// device that cannot invalidate a reference tracks none and says so. Ignored by default.
6059    #[test]
6060    #[ignore]
6061    fn gpu_predicts_past_a_lost_frame() {
6062        use crate::encoders::reference::Reference;
6063        use crate::encoders::sps::h264_max_num_ref_frames;
6064        let (w, h) = (1280usize, 720usize);
6065        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
6066            let mut s = settings(w as i32, h as i32, 60.0);
6067            s.codec = codec;
6068            s.omit_stripe_headers = true;
6069            let mut enc = match host_session(&s) {
6070                Ok(enc) => enc,
6071                Err(e) => {
6072                    println!("{codec:?}: {e}");
6073                    continue;
6074                }
6075            };
6076            let encode = |enc: &mut NvencEncoder, i: usize, w: usize, h: usize| {
6077                let out = enc
6078                    .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6079                    .expect("encode");
6080                (out, enc.last_reference())
6081            };
6082            let (first, reference) = encode(&mut enc, 0, w, h);
6083            if reference == Reference::Untracked {
6084                println!(
6085                    "{codec:?}: this device cannot invalidate a reference, so nothing is tracked"
6086                );
6087                assert!(!enc.invalidate_reference(0));
6088                continue;
6089            }
6090            assert_eq!(reference, Reference::None);
6091            if codec == Codec::H264 {
6092                assert_eq!(
6093                    h264_max_num_ref_frames(&first),
6094                    Some(REFERENCE_FRAMES),
6095                    "the SPS declares the DPB"
6096                );
6097            }
6098            let mut frames = vec![first];
6099            for i in 1..8 {
6100                let (out, reference) = encode(&mut enc, i, w, h);
6101                assert_eq!(
6102                    reference,
6103                    Reference::Frame(i as u16 - 1),
6104                    "{codec:?} frame {i}"
6105                );
6106                frames.push(out);
6107            }
6108            assert!(
6109                enc.invalidate_reference(5),
6110                "{codec:?}: the device refused the invalidation"
6111            );
6112            let (out, reference) = encode(&mut enc, 8, w, h);
6113            // AV1's four-slot DPB leaves three recent pictures beside its one anchor.
6114            // The oldest recent picture (4) is already gone, so it predicts from anchor 0.
6115            let before_loss = if codec == Codec::Av1
6116                && enc
6117                    .references
6118                    .as_ref()
6119                    .is_some_and(ReferenceWindow::anchored)
6120            {
6121                0
6122            } else {
6123                4
6124            };
6125            assert_eq!(reference, Reference::Frame(before_loss), "{codec:?}");
6126            frames.push(out);
6127            let (out, reference) = encode(&mut enc, 9, w, h);
6128            assert_eq!(reference, Reference::Frame(8), "{codec:?}");
6129            frames.push(out);
6130            let off = apart_without(codec, &frames, 5..8);
6131            println!("{codec:?}: frame 9 without frames 5-7 is {off:.3} off the complete decode");
6132            assert!(
6133                off < 0.5,
6134                "{codec:?}: the decoder that lost frames 5-7 shows frame 9 {off:.2} off the one that saw them"
6135            );
6136            // A grow to a level admitting fewer frames declares the smaller buffer at the IDR the
6137            // resize forces.
6138            s.width = 1920;
6139            s.height = 1080;
6140            assert!(
6141                enc.reconfigure_resolution(&s).expect("in-place grow"),
6142                "{codec:?}"
6143            );
6144            let dpb = enc.dpb as usize;
6145            let anchored = enc.references.as_ref().unwrap().anchored();
6146            let (out, reference) = encode(&mut enc, 10, 1920, 1080);
6147            assert_eq!(reference, Reference::None);
6148            if codec == Codec::H264 {
6149                assert_eq!(
6150                    h264_max_num_ref_frames(&out),
6151                    Some(4),
6152                    "1080p at level 4.2 admits four"
6153                );
6154            }
6155            let mut frames = vec![out];
6156            for i in 11..=10 + dpb {
6157                let (out, reference) = encode(&mut enc, i, 1920, 1080);
6158                assert_eq!(
6159                    reference,
6160                    Reference::Frame(i as u16 - 1),
6161                    "{codec:?} frame {i}"
6162                );
6163                frames.push(out);
6164            }
6165            assert!(
6166                enc.invalidate_reference(12),
6167                "{codec:?}: the device refused the invalidation"
6168            );
6169            let (out, reference) = encode(&mut enc, 11 + dpb, 1920, 1080);
6170            assert_eq!(
6171                reference,
6172                Reference::Frame(if anchored { 10 } else { 11 }),
6173                "{codec:?}: use the newest reference still held before the loss"
6174            );
6175            frames.push(out);
6176            let (out, reference) = encode(&mut enc, 12 + dpb, 1920, 1080);
6177            assert_eq!(reference, Reference::Frame(11 + dpb as u16), "{codec:?}");
6178            frames.push(out);
6179            let off = apart_without(codec, &frames, 2..dpb + 1);
6180            println!(
6181                "{codec:?}: at 1080p, frame {} without frames 12-{} is {off:.3} off the complete decode",
6182                12 + dpb,
6183                10 + dpb
6184            );
6185            assert!(
6186                off < 0.5,
6187                "{codec:?}: the decoder that lost frames 12-{} shows {off:.2} off the one that saw them",
6188                10 + dpb
6189            );
6190            assert!(enc.invalidate_reference(11));
6191            assert_eq!(
6192                encode(&mut enc, 13 + dpb, 1920, 1080).1,
6193                if anchored {
6194                    Reference::Frame(10)
6195                } else {
6196                    Reference::None
6197                },
6198                "{codec:?}: losing frame 11 requires the older anchor or a key frame"
6199            );
6200            assert!(enc.invalidate_reference(10));
6201            assert_eq!(
6202                encode(&mut enc, 14 + dpb, 1920, 1080).1,
6203                if anchored {
6204                    Reference::None
6205                } else {
6206                    Reference::Frame(13 + dpb as u16)
6207                },
6208                "{codec:?}: losing the anchor requires a key frame; a report before a new key frame is ignored"
6209            );
6210        }
6211    }
6212
6213    /// A report for a frame an earlier invalidation already left out keeps the chain predicted
6214    /// since: lose 80 and 81, recover from 79, and the report for 81 that arrives after 98 lets 99
6215    /// predict from 98 rather than cost a key frame; a loss in the new chain still invalidates
6216    /// it. The stream without the lost frames decodes as the complete one does. Ignored by
6217    /// default.
6218    #[test]
6219    #[ignore]
6220    fn gpu_a_covered_loss_keeps_the_recovered_chain() {
6221        use crate::encoders::reference::Reference;
6222        let (w, h) = (1920usize, 1080usize);
6223        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
6224            let mut s = settings(w as i32, h as i32, 60.0);
6225            s.codec = codec;
6226            s.omit_stripe_headers = true;
6227            let mut enc = match host_session(&s) {
6228                Ok(enc) => enc,
6229                Err(e) => {
6230                    println!("{codec:?}: {e}");
6231                    continue;
6232                }
6233            };
6234            let mut frames = Vec::new();
6235            for i in 0..=107usize {
6236                let (lost, expect) = match i {
6237                    82 => (Some(80), Some(79)),
6238                    99 => (Some(81), Some(98)),
6239                    107 => (Some(106), Some(105)),
6240                    _ => (None, None),
6241                };
6242                if let Some(lost) = lost {
6243                    assert!(enc.invalidate_reference(lost), "{codec:?}: lost {lost}");
6244                }
6245                frames.push(
6246                    enc.encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6247                        .expect("encode"),
6248                );
6249                if i == 0 && enc.last_reference() == Reference::Untracked {
6250                    break;
6251                }
6252                if let Some(from) = expect {
6253                    assert_eq!(
6254                        enc.last_reference(),
6255                        Reference::Frame(from),
6256                        "{codec:?} {i}"
6257                    );
6258                }
6259            }
6260            if enc.last_reference() == Reference::Untracked {
6261                println!("{codec:?}: this device cannot invalidate a reference");
6262                continue;
6263            }
6264            let received: Vec<usize> = (0..frames.len())
6265                .filter(|i| !matches!(i, 80 | 81 | 106))
6266                .collect();
6267            let off = apart_keeping(codec, &frames, &received);
6268            assert!(
6269                off.is_some_and(|x| x < 0.5),
6270                "{codec:?}: without frames 80, 81 and 106 the last frame is {off:?} off"
6271            );
6272            println!("{codec:?}: frame 99 predicts from 98 after the covered report, {off:?} off");
6273        }
6274    }
6275
6276    /// A held refresh of the whole picture past `HELD_REFRESH_LIMIT_S` of a constant rate is coded
6277    /// again to fit `HELD_KEY_BUDGET_S`, and one under it stands. The frame coded again predicts
6278    /// from the frame before the first attempt, which the device leaves out as it does a lost
6279    /// frame: a decoder that never sees the attempt decodes what follows as one that saw it does,
6280    /// and a loss reported for the frame sent in its place leaves that frame out. Ignored by
6281    /// default.
6282    #[test]
6283    #[ignore]
6284    fn gpu_a_held_refresh_past_its_limit_is_coded_again() {
6285        use crate::encoders::reference::Reference;
6286        let (w, h) = (1280usize, 720usize);
6287        let still = frame(w, h, 10);
6288        let encode = |enc: &mut NvencEncoder, i: u64, held: Option<u32>| {
6289            if let Some(crf) = held {
6290                enc.hold_quantizer(crf, None);
6291            }
6292            let out = enc
6293                .encode_cpu_argb(&still, w * 4, i, 25, i == 0)
6294                .expect("encode");
6295            (out, enc.last_reference())
6296        };
6297        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
6298            let mut s = settings(w as i32, h as i32, 60.0);
6299            s.codec = codec;
6300            s.omit_stripe_headers = true;
6301            s.video_cbr_mode = true;
6302            s.video_bitrate_kbps = 50_000;
6303            let mut enc = match host_session(&s) {
6304                Ok(enc) => enc,
6305                Err(e) => {
6306                    println!("{codec:?}: {e}");
6307                    continue;
6308                }
6309            };
6310            let mut frames: Vec<_> = (0..4).map(|i| encode(&mut enc, i, None).0).collect();
6311            if enc.last_reference() == Reference::Untracked {
6312                println!("{codec:?}: this device cannot invalidate a reference");
6313                continue;
6314            }
6315            // An attempt within the limit of this rate, taken back as the session takes back one
6316            // past it.
6317            let (attempt, _) = encode(&mut enc, 4, Some(20));
6318            assert!(enc.retract_last(), "{codec:?}: the device refused");
6319            frames.push(attempt);
6320            let (out, reference) = encode(&mut enc, 4, Some(40));
6321            assert_eq!(reference, Reference::Frame(3), "{codec:?}");
6322            frames.push(out);
6323            let (out, reference) = encode(&mut enc, 5, None);
6324            assert_eq!(reference, Reference::Frame(4), "{codec:?}");
6325            frames.push(out);
6326            let off = apart_without(codec, &frames, 4..5);
6327            assert!(
6328                off < 0.5,
6329                "{codec:?}: without the attempt the frames after it decode {off:.2} off"
6330            );
6331            assert!(enc.invalidate_reference(4), "{codec:?}");
6332            let (out, reference) = encode(&mut enc, 6, None);
6333            assert!(
6334                matches!(reference, Reference::Frame(id) if id < 4),
6335                "{codec:?}: {reference:?} after losing the frame sent in place of the attempt"
6336            );
6337            frames.push(out);
6338            let lost = apart_without(codec, &frames, 4..7);
6339            assert!(
6340                lost < 0.5,
6341                "{codec:?}: past the loss the frame decodes {lost:.2} off"
6342            );
6343            // Past `HELD_REFRESH_LIMIT_S` of the target the session codes such a frame again on its
6344            // own (AV1 on 595.91.07 caps a held frame at a second of the target itself); under it,
6345            // the frame stands.
6346            for (kbps, first) in [(100, 7u64), (1000, 11)] {
6347                s.video_bitrate_kbps = kbps;
6348                assert!(enc.reconfigure_rate(&s), "{codec:?}");
6349                for i in first..first + 3 {
6350                    encode(&mut enc, i, None);
6351                }
6352                let pts = |enc: &NvencEncoder| enc.references.as_ref().map_or(0, |r| r.next_pts());
6353                let before = pts(&enc);
6354                let (out, reference) = encode(&mut enc, first + 3, Some(20));
6355                assert_eq!(reference, Reference::Frame(first as u16 + 2), "{codec:?}");
6356                let spent = out.len() as f64 / (f64::from(kbps) * 1000.0 / 8.0);
6357                let coded = pts(&enc) - before;
6358                println!(
6359                    "{codec:?}: a held frame at {kbps} kbps goes out at {} bytes, {spent:.2} s of the target, encoded {coded} times",
6360                    out.len()
6361                );
6362                assert!(spent <= crate::encoders::HELD_REFRESH_LIMIT_S, "{codec:?}");
6363                if kbps == 1000 {
6364                    assert_eq!(coded, 1, "{codec:?}: a frame under the limit stands");
6365                }
6366            }
6367        }
6368    }
6369
6370    /// A cleanup frame that comes out past its cap (`cap`) is taken back and coded again: a held
6371    /// band in a shorter one, which `refit` reports, and a frame of the rate control's at the
6372    /// coarsest quantizer, refining nothing. The frame sent in place of the attempt predicts from
6373    /// the one before it, and the stream decodes as one without the attempt; a frame within its
6374    /// cap stands. Ignored by default.
6375    #[test]
6376    #[ignore]
6377    fn gpu_a_cleanup_frame_past_its_cap_is_coded_again() {
6378        use crate::encoders::reference::Reference;
6379        use crate::webcam::decode::{Decoder as _, VideoDecoder};
6380        let (w, h) = (1280usize, 720usize);
6381        let still = frame(w, h, 10);
6382        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
6383            let mut s = settings(w as i32, h as i32, 60.0);
6384            s.codec = codec;
6385            s.omit_stripe_headers = true;
6386            s.video_cbr_mode = true;
6387            s.video_bitrate_kbps = 2000;
6388            let mut enc = match host_session(&s) {
6389                Ok(enc) => enc,
6390                Err(e) => {
6391                    println!("{codec:?}: {e}");
6392                    continue;
6393                }
6394            };
6395            let encode = |enc: &mut NvencEncoder, i: u64| {
6396                enc.encode_cpu_argb(&still, w * 4, i, 25, i == 0)
6397                    .expect("encode")
6398            };
6399            let mut frames: Vec<_> = (0..4).map(|i| encode(&mut enc, i)).collect();
6400            if enc.last_reference() == Reference::Untracked {
6401                println!("{codec:?}: this device cannot invalidate a reference");
6402                continue;
6403            }
6404            enc.hold_quantizer(10, Some((0.0, 0.5)));
6405            enc.cap(4000);
6406            frames.push(encode(&mut enc, 4));
6407            let end = enc.refit().expect("the band taken back");
6408            println!(
6409                "{codec:?}: a band to 0.5 coded again to {end:.3}, {} bytes",
6410                frames[4].len()
6411            );
6412            assert!(end > 0.0 && end < 0.5, "{codec:?}: {end}");
6413            assert_eq!(enc.last_reference(), Reference::Frame(3), "{codec:?}");
6414            enc.cap(1);
6415            frames.push(encode(&mut enc, 5));
6416            assert_eq!(
6417                enc.refit(),
6418                Some(0.0),
6419                "{codec:?}: the rate control's frame taken back"
6420            );
6421            assert_eq!(enc.last_reference(), Reference::Frame(4), "{codec:?}");
6422            enc.cap(usize::MAX);
6423            frames.push(encode(&mut enc, 6));
6424            assert_eq!(
6425                enc.refit(),
6426                None,
6427                "{codec:?}: a frame within its cap stands"
6428            );
6429            let mut decoder = VideoDecoder::new(codec).unwrap();
6430            for (i, f) in frames.iter().enumerate() {
6431                assert!(decoder.decode(f).expect("decode"), "{codec:?} frame {i}");
6432            }
6433        }
6434    }
6435
6436    /// Two consumers share a session told which frames both hold: one takes every frame, the other
6437    /// misses stretches, everything but the anchors and, where there are two, then everything,
6438    /// and once it has room reports the frame its missing run starts with. Of two anchors, the
6439    /// newest both hold stays pinned while the schedule marks the other, which predicts from the
6440    /// frame before it (both hold none of the two newest), so the one behind misses the anchors
6441    /// too; the one behind decodes each frame it is sent as the other does, the first after its
6442    /// report predicting from the anchor it holds; and no frame after the first is a key frame.
6443    /// H.264 keeps one anchor, a frame every forty-eight, and marks none while the one behind
6444    /// holds no recent frame, which then predicts past from the one it holds. Ignored by default.
6445    #[test]
6446    #[ignore]
6447    fn gpu_acknowledged_anchors_carry_a_consumer_left_behind() {
6448        use crate::encoders::codec::parse_video_type;
6449        use crate::encoders::reference::Reference;
6450        use crate::webcam::decode::{Decoder as _, VideoDecoder};
6451        let (w, h) = (1280usize, 720usize);
6452        // (frames it has no room for, anchors too, the frame its report goes ahead of, the anchor
6453        // the frame after the report predicts from), for two anchors and for one
6454        let pinned = [(13..=40, false, 42, 12u16), (61..=100, true, 102, 60)];
6455        let alone = [(49..=90, false, 92, 48u16), (97..=150, false, 152, 96)];
6456        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
6457            let mut s = settings(w as i32, h as i32, 60.0);
6458            s.codec = codec;
6459            s.acknowledge_references = true;
6460            let mut enc = match host_session(&s) {
6461                Ok(enc) => enc,
6462                Err(e) => {
6463                    println!("{codec:?}: {e}");
6464                    continue;
6465                }
6466            };
6467            let anchors = enc
6468                .references
6469                .as_ref()
6470                .map_or(0, ReferenceWindow::anchor_count);
6471            let (stretches, period, last) = match anchors {
6472                ANCHORS => (&pinned, 12, 120),
6473                1 => (&alone, 48, 160),
6474                _ => {
6475                    println!("{codec:?}: this device or API keeps no long-term reference");
6476                    continue;
6477                }
6478            };
6479            let (mut whole, mut part) = (
6480                VideoDecoder::new(codec).unwrap(),
6481                VideoDecoder::new(codec).unwrap(),
6482            );
6483            let mut holds = Vec::new();
6484            let mut run = None;
6485            let (mut worst, mut sizes) = (0f64, (0usize, 0usize, 0usize, 0usize));
6486            for i in 0..=last {
6487                let stretch = stretches.iter().find(|t| t.0.contains(&i));
6488                if let Some(&(_, _, _, from)) = stretches.iter().find(|t| t.2 == i) {
6489                    let first = run.take().expect("a missing run");
6490                    assert!(enc.take_report(ReferenceReport::Lost(first)), "{codec:?}");
6491                    assert_eq!(
6492                        enc.references
6493                            .as_ref()
6494                            .and_then(ReferenceWindow::newest_valid)
6495                            .map(|f| f.0),
6496                        Some(from),
6497                        "{codec:?}: {first} reported lost before {i}"
6498                    );
6499                }
6500                let out = enc
6501                    .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6502                    .expect("encode");
6503                let (_, kind) = parse_video_type(out[1]).expect("video type byte");
6504                assert_eq!(
6505                    kind == FRAME_KEY,
6506                    i == 0,
6507                    "{codec:?}: frame {i} is kind {kind}"
6508                );
6509                let reference = enc.last_reference();
6510                let can = match reference {
6511                    Reference::None => true,
6512                    Reference::Frame(r) => holds[r as usize],
6513                    Reference::Untracked => false,
6514                };
6515                let anchor = out[1] & FRAME_ANCHOR != 0;
6516                if anchor {
6517                    assert!(
6518                        can,
6519                        "{codec:?}: anchor {i} predicts from {reference:?}, held by one"
6520                    );
6521                }
6522                assert_eq!(
6523                    anchor,
6524                    i > 0 && i % period == 0 && can,
6525                    "{codec:?}: frame {i} predicting from {reference:?}"
6526                );
6527                let sent = can && stretch.is_none_or(|t| anchor && !t.1);
6528                let bits = &out[VIDEO_HEADER_LEN..];
6529                assert!(whole.decode(bits).unwrap_or(false), "{codec:?}: frame {i}");
6530                if sent {
6531                    assert!(
6532                        part.decode(bits).unwrap_or(false),
6533                        "{codec:?}: frame {i} sent"
6534                    );
6535                    let off = luma_apart(&whole.frame().unwrap(), &part.frame().unwrap());
6536                    assert!(off < 0.5, "{codec:?}: frame {i} sent is {off:.3} off");
6537                    worst = worst.max(off);
6538                    run = None;
6539                    enc.acknowledge_reference(i as u16, true);
6540                } else if can {
6541                    run = Some(i as u16);
6542                }
6543                holds.push(sent);
6544                if i > 0 {
6545                    let bucket = if anchor { &mut sizes.2 } else { &mut sizes.0 };
6546                    *bucket += out.len();
6547                    if anchor { sizes.3 += 1 } else { sizes.1 += 1 }
6548                }
6549            }
6550            println!(
6551                "{codec:?}: worst {worst:.3} apart; {} B a frame, {} B an anchor ({} anchors)",
6552                sizes.0 / sizes.1.max(1),
6553                sizes.2 / sizes.3.max(1),
6554                sizes.3
6555            );
6556        }
6557    }
6558
6559    /// With the second anchor lost, a loss of the two newest frames is predicted past from the
6560    /// newest frame the device still holds, and the window names it: on AV1 one past the recent
6561    /// frames beside two anchors, held in the long-term slot the lost anchor lends. A decoder
6562    /// given the frames up to it, and none after, decodes the next frame as one given every frame
6563    /// does. Ignored by default.
6564    #[test]
6565    #[ignore]
6566    fn gpu_a_lost_anchor_lends_its_slot_to_the_recent_frames() {
6567        let (w, h) = (1280usize, 720usize);
6568        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
6569            let mut s = settings(w as i32, h as i32, 60.0);
6570            s.codec = codec;
6571            s.acknowledge_references = true;
6572            s.omit_stripe_headers = true;
6573            let mut enc = match host_session(&s) {
6574                Ok(enc) => enc,
6575                Err(e) => {
6576                    println!("{codec:?}: {e}");
6577                    continue;
6578                }
6579            };
6580            if !enc
6581                .references
6582                .as_ref()
6583                .is_some_and(ReferenceWindow::anchored)
6584            {
6585                println!("{codec:?}: this device or API keeps no long-term reference");
6586                continue;
6587            }
6588            let mut frames = Vec::new();
6589            let mut from = 0;
6590            for i in 0..=22usize {
6591                match i {
6592                    16 => assert!(enc.take_report(ReferenceReport::Lost(12)), "{codec:?}"),
6593                    21 => assert!(enc.take_report(ReferenceReport::Lost(19)), "{codec:?}"),
6594                    _ => {}
6595                }
6596                let out = enc
6597                    .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6598                    .expect("encode");
6599                if i <= 11 || (16..=17).contains(&i) {
6600                    enc.acknowledge_reference(i as u16, true);
6601                }
6602                frames.push(out);
6603                if i == 21 {
6604                    let Reference::Frame(r) = enc.last_reference() else {
6605                        panic!("{codec:?}: {:?} past the loss", enc.last_reference());
6606                    };
6607                    from = r as usize;
6608                }
6609            }
6610            if codec == Codec::Av1 {
6611                assert_eq!(from, 18, "{codec:?}: past the loss of 19 and 20");
6612            }
6613            let keep: Vec<usize> = (0..=11).chain(16..=from).chain([21, 22]).collect();
6614            let off = apart_keeping(codec, &frames, &keep);
6615            println!("{codec:?}: past the loss from {from}, {off:?} apart");
6616            assert!(
6617                off.is_some_and(|off| off < 0.5),
6618                "{codec:?}: given the frames up to {from}, frame 22 decodes {off:?} apart"
6619            );
6620        }
6621    }
6622
6623    /// A loss deeper than the recent frames is predicted past from the newest anchor before it,
6624    /// the one the device predicts from, and decodes as the complete stream does; one before every
6625    /// anchor costs a key frame. An H.264 session, and an AV1 one on an API with AV1 long-term
6626    /// references, keeps one anchor, a frame every forty-eight, an H.265 one two, a frame every
6627    /// twelve by turns, and on Pascal reaches one recent frame past its buffer. A CBR rate lowered
6628    /// between the anchor and the loss keeps the anchor. Ignored by default.
6629    #[test]
6630    #[ignore]
6631    fn gpu_predicts_past_a_loss_deeper_than_the_recent_frames() {
6632        use crate::encoders::reference::Reference;
6633        // (codec, frames sent after the key frame, first frame lost, the anchor predicted from, an
6634        // older anchor the device must not have taken, CBR lowered from 20 to 8 Mbit/s as the lost
6635        // frame is encoded), at 1080p and, for the frames `split_mode` splits, at 4K
6636        let cases = [
6637            (Codec::H264, 60u16, 50u16, Some(48u16), Some(0u16), false),
6638            (Codec::H264, 60, 57, Some(48), Some(0), false),
6639            (Codec::H264, 80, 64, Some(48), Some(0), false),
6640            (Codec::H264, 80, 51, Some(48), Some(0), false),
6641            (Codec::H264, 80, 64, Some(48), Some(0), true),
6642            (Codec::H264, 60, 40, None, None, false),
6643            (Codec::Av1, 60, 50, Some(48), Some(0), false),
6644            (Codec::Av1, 60, 57, Some(48), Some(0), false),
6645            (Codec::Av1, 80, 64, Some(48), Some(0), false),
6646            (Codec::Av1, 80, 64, Some(48), Some(0), true),
6647            (Codec::Av1, 60, 40, None, None, false),
6648            (Codec::H265, 30, 28, Some(27), Some(26), false),
6649            (Codec::H265, 30, 27, Some(24), Some(12), false),
6650            (Codec::H265, 40, 37, Some(36), Some(24), false),
6651            (Codec::H265, 40, 30, Some(24), Some(12), false),
6652            (Codec::H265, 58, 40, Some(36), Some(24), false),
6653            (Codec::H265, 58, 37, Some(36), Some(24), false),
6654            (Codec::H265, 40, 20, None, None, false),
6655        ];
6656        let split = [
6657            (Codec::Av1, 60u16, 50u16, Some(48u16), Some(0u16), false),
6658            (Codec::H265, 30, 28, Some(27), Some(26), false),
6659            (Codec::H265, 40, 30, Some(24), Some(12), false),
6660        ];
6661        let fmt = |v: Option<f64>| v.map_or("no picture".to_string(), |x| format!("{x:.3}"));
6662        let sized = (cases.into_iter().map(|c| (1920usize, 1080usize, c)))
6663            .chain(split.into_iter().map(|c| (3840, 2160, c)));
6664        for (w, h, (codec, sent, lost, from, older, retarget)) in sized {
6665            let mut s = settings(w as i32, h as i32, 60.0);
6666            s.codec = codec;
6667            s.omit_stripe_headers = true;
6668            if retarget {
6669                s.video_cbr_mode = true;
6670                s.video_bitrate_kbps = 20_000;
6671            }
6672            let mut enc = match host_session(&s) {
6673                Ok(enc) => enc,
6674                Err(e) => {
6675                    println!("{codec:?}: {e}");
6676                    continue;
6677                }
6678            };
6679            if codec == Codec::Av1
6680                && !enc
6681                    .references
6682                    .as_ref()
6683                    .is_some_and(ReferenceWindow::anchored)
6684            {
6685                println!("{codec:?}: this device or API keeps no long-term reference");
6686                continue;
6687            }
6688            let mut frames = Vec::new();
6689            for i in 0..=sent as usize {
6690                if retarget && i == lost as usize {
6691                    s.video_bitrate_kbps = 8000;
6692                    assert!(
6693                        enc.reconfigure_rate(&s),
6694                        "{codec:?}: the rate change was refused"
6695                    );
6696                }
6697                frames.push(
6698                    enc.encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6699                        .expect("encode"),
6700                );
6701                if i == 0 && enc.last_reference() == Reference::Untracked {
6702                    break;
6703                }
6704            }
6705            if enc.last_reference() == Reference::Untracked {
6706                println!("{codec:?}: this device cannot invalidate a reference");
6707                continue;
6708            }
6709            let normal = frames[sent as usize].len();
6710            assert!(
6711                enc.invalidate_reference(lost),
6712                "{codec:?}: the device refused the invalidation"
6713            );
6714            let next = sent as usize + 1;
6715            frames.push(
6716                enc.encode_cpu_argb(&moving_frame(w, h, next), w * 4, next as u64, 25, false)
6717                    .expect("encode"),
6718            );
6719            let reference = enc.last_reference();
6720            let keeping = |upto: usize| (0..=upto).chain([next]).collect::<Vec<_>>();
6721            let without_lost: Vec<usize> = (0..=next)
6722                .filter(|&i| i < lost as usize || i == next)
6723                .collect();
6724            println!(
6725                "{codec:?} {w}x{h}: frame {lost} of {sent} lost, frame {next} predicts from {reference:?}, {} B against {normal} B",
6726                frames[next].len()
6727            );
6728            // The frame after an anchor is marked predicts from the anchor itself.
6729            if let Some(anchor) = from.filter(|&a| a > 0 && a < sent).map(usize::from) {
6730                let skipped: Vec<usize> = (0..=anchor + 1).filter(|&i| i != anchor).collect();
6731                let off = apart_keeping(codec, &frames[..=anchor + 1], &skipped);
6732                assert!(
6733                    off.is_none_or(|x| x >= 0.5),
6734                    "{codec:?}: frame {} decodes without {anchor} ({})",
6735                    anchor + 1,
6736                    fmt(off)
6737                );
6738            }
6739            match from {
6740                Some(anchor) => {
6741                    assert_eq!(
6742                        reference,
6743                        Reference::Frame(anchor),
6744                        "{codec:?}: lost {lost}"
6745                    );
6746                    let off = apart_keeping(codec, &frames, &without_lost);
6747                    assert!(
6748                        off.is_some_and(|x| x < 0.5),
6749                        "{codec:?}: without {lost}-{sent} the frame is {} off",
6750                        fmt(off)
6751                    );
6752                    let off = apart_keeping(codec, &frames, &keeping(anchor as usize));
6753                    assert!(
6754                        off.is_some_and(|x| x < 0.5),
6755                        "{codec:?}: up to the anchor {anchor} the frame is {} off",
6756                        fmt(off)
6757                    );
6758                    if let Some(older) = older {
6759                        let off = apart_keeping(codec, &frames, &keeping(older as usize));
6760                        let past_buffer = codec == Codec::H265
6761                            && anchor == sent - (enc.dpb - ANCHORS as u32) as u16;
6762                        if past_buffer && off.is_some_and(|x| x < 0.5) {
6763                            println!(
6764                                "{codec:?}: lost {lost}, the device predicts from before {anchor}, the frame past its buffer"
6765                            );
6766                        } else {
6767                            assert!(
6768                                off.is_none_or(|x| x >= 0.5),
6769                                "{codec:?}: the frame decodes from anchor {older} alone ({})",
6770                                fmt(off)
6771                            );
6772                        }
6773                    }
6774                }
6775                None => assert_eq!(
6776                    reference,
6777                    Reference::None,
6778                    "{codec:?}: lost {lost}, before every anchor"
6779                ),
6780            }
6781        }
6782    }
6783
6784    /// A session opened at 1080p and resized in place to 720p keeps the buffer it declared, which
6785    /// the driver never raises, and still predicts past a loss six frames deep from the key frame
6786    /// the resize forced, its first anchor, decoding as the complete stream does. Ignored by
6787    /// default.
6788    #[test]
6789    #[ignore]
6790    fn gpu_a_shrink_predicts_past_a_loss_deeper_than_its_buffer() {
6791        use crate::encoders::reference::Reference;
6792        use crate::encoders::sps::h264_max_num_ref_frames;
6793        for codec in [Codec::H264, Codec::H265] {
6794            let mut s = settings(1920, 1080, 60.0);
6795            s.codec = codec;
6796            s.omit_stripe_headers = true;
6797            let mut enc = match host_session(&s) {
6798                Ok(enc) => enc,
6799                Err(e) if codec == Codec::H265 => {
6800                    println!("{codec:?}: {e}");
6801                    continue;
6802                }
6803                Err(e) => panic!("{codec:?}: {e}"),
6804            };
6805            let encode = |enc: &mut NvencEncoder, i: usize, w: usize, h: usize| {
6806                let out = enc
6807                    .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6808                    .expect("encode");
6809                (out, enc.last_reference())
6810            };
6811            let (first, reference) = encode(&mut enc, 0, 1920, 1080);
6812            if reference == Reference::Untracked {
6813                println!("{codec:?}: this device cannot invalidate a reference");
6814                continue;
6815            }
6816            let declared = enc.dpb;
6817            if codec == Codec::H264 {
6818                assert_eq!(
6819                    h264_max_num_ref_frames(&first),
6820                    Some(4),
6821                    "1080p at level 4.2 admits four"
6822                );
6823            }
6824            (s.width, s.height) = (1280, 720);
6825            assert!(
6826                enc.reconfigure_resolution(&s).expect("in-place shrink"),
6827                "{codec:?}"
6828            );
6829            assert_eq!(
6830                enc.dpb, declared,
6831                "{codec:?}: the driver never raises the buffer"
6832            );
6833            let mut frames = Vec::new();
6834            for i in 1..=8 {
6835                let (out, reference) = encode(&mut enc, i, 1280, 720);
6836                let want = if i == 1 {
6837                    Reference::None
6838                } else {
6839                    Reference::Frame(i as u16 - 1)
6840                };
6841                assert_eq!(reference, want, "{codec:?} frame {i}");
6842                frames.push(out);
6843            }
6844            assert!(
6845                enc.invalidate_reference(2),
6846                "{codec:?}: the device refused the invalidation"
6847            );
6848            let (out, reference) = encode(&mut enc, 9, 1280, 720);
6849            assert_eq!(
6850                reference,
6851                Reference::Frame(1),
6852                "{codec:?}: a loss six deep, from the resize's key frame"
6853            );
6854            frames.push(out);
6855            let off = apart_without(codec, &frames, 1..8);
6856            println!(
6857                "{codec:?}: at 720p on a {declared}-frame buffer, frame 9 without frames 2-8 is {off:.3} off the complete decode"
6858            );
6859            assert!(
6860                off < 0.5,
6861                "{codec:?}: the decoder that lost frames 2-8 shows {off:.2} off the one that saw them"
6862            );
6863        }
6864    }
6865
6866    /// On a real GPU, print what a loss costs the picture at CBR: the frames from one `depth`
6867    /// frames deep on never reach the decoder, as a receiver's frame buffer holds back what
6868    /// predicts from a lost frame, the loss is reported, and the recovery frame's size and the
6869    /// luma PSNR of it and the fourteen frames after it are measured against their sources, over
6870    /// losses at scattered phases; depth 0 is the same frames with nothing lost. Prints
6871    /// measurements to quote rather than asserting. Ignored by default.
6872    #[test]
6873    #[ignore]
6874    fn gpu_bench_loss_recovery() {
6875        use crate::encoders::reference::Reference;
6876        use crate::webcam::decode::{Decoder as _, VideoDecoder};
6877        let (w, h) = (1920usize, 1080usize);
6878        for codec in [Codec::H264, Codec::H265] {
6879            let mut s = settings(w as i32, h as i32, 60.0);
6880            s.codec = codec;
6881            s.video_cbr_mode = true;
6882            s.video_bitrate_kbps = 8000;
6883            for depth in [0usize, 1, 2, 3, 5, 8, 12] {
6884                let mut enc = host_session(&s).expect("init");
6885                let mut dec = VideoDecoder::new(codec).unwrap();
6886                let mut pending: std::collections::VecDeque<Vec<u8>> = Default::default();
6887                let mut rng = 0x2545_f491_4f6c_dd1du64 ^ depth as u64;
6888                let (mut sizes, mut first, mut after, mut keys) =
6889                    (Vec::new(), Vec::new(), Vec::new(), 0);
6890                let mut i = 0usize;
6891                for event in 0..10 {
6892                    rng ^= rng << 13;
6893                    rng ^= rng >> 7;
6894                    rng ^= rng << 17;
6895                    let gap = if event == 0 {
6896                        60
6897                    } else {
6898                        20 + (rng % 37) as usize
6899                    };
6900                    for _ in 0..gap {
6901                        let out = enc
6902                            .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6903                            .expect("encode");
6904                        pending.push_back(out);
6905                        i += 1;
6906                        while pending.len() > depth + 1 {
6907                            let out = pending.pop_front().unwrap();
6908                            dec.decode(&out[VIDEO_HEADER_LEN..]).expect("decode");
6909                        }
6910                    }
6911                    // The newest `depth + 1` frames, the lost one and those after it, never arrive.
6912                    if depth == 0 {
6913                        for out in pending.drain(..) {
6914                            dec.decode(&out[VIDEO_HEADER_LEN..]).expect("decode");
6915                        }
6916                    } else {
6917                        pending.clear();
6918                        enc.invalidate_reference((i - 1 - depth) as u16);
6919                    }
6920                    let mut row = Vec::new();
6921                    for k in 0..15 {
6922                        let src = moving_frame(w, h, i);
6923                        let out = enc
6924                            .encode_cpu_argb(&src, w * 4, i as u64, 25, false)
6925                            .expect("encode");
6926                        if k == 0 {
6927                            sizes.push(out.len() - VIDEO_HEADER_LEN);
6928                            keys += usize::from(enc.last_reference() == Reference::None);
6929                        }
6930                        row.push(luma_psnr(&mut dec, &out, &src, w, h));
6931                        i += 1;
6932                    }
6933                    first.push(row[0]);
6934                    after.push(row.iter().sum::<f64>() / row.len() as f64);
6935                }
6936                sizes.sort();
6937                let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len() as f64;
6938                println!(
6939                    "RECOVERY {codec:?} depth {depth}: {keys}/10 key frames, answer {} B median, \
6940                     answer PSNR {:.2} dB mean ({:.2} worst), with the 14 after {:.2} dB mean",
6941                    sizes[sizes.len() / 2],
6942                    mean(&first),
6943                    first.iter().copied().fold(f64::INFINITY, f64::min),
6944                    mean(&after),
6945                );
6946            }
6947        }
6948    }
6949
6950    /// NVENC counts H.264's `frame_num` in 256 values and the stream in 65536 (`WideFrameNum`): a
6951    /// loss covering frame 256, where NVENC's own count wraps, is predicted past rather than
6952    /// answered with a key frame, and the decoder that lost it shows the next frame as one that
6953    /// saw it does. Ignored by default.
6954    #[test]
6955    #[ignore]
6956    fn gpu_predicts_past_a_loss_at_the_devices_frame_num_wrap() {
6957        use crate::encoders::reference::Reference;
6958        use crate::encoders::sps::h264_frame_num_range;
6959        let (w, h) = (1280usize, 720usize);
6960        let mut s = settings(w as i32, h as i32, 60.0);
6961        s.omit_stripe_headers = true;
6962        let mut enc = host_session(&s).expect("H.264 session");
6963        let encode = |enc: &mut NvencEncoder, i: usize| {
6964            let out = enc
6965                .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6966                .expect("encode");
6967            (out, enc.last_reference())
6968        };
6969        let (first, reference) = encode(&mut enc, 0);
6970        if reference == Reference::Untracked {
6971            println!("this device cannot invalidate a reference, so nothing is tracked");
6972            return;
6973        }
6974        let range = h264_frame_num_range(&first).expect("the key frame carries the SPS") as usize;
6975        assert_eq!(range, 65536, "NVENC's 256 values, a byte wider");
6976        let mut frames = vec![first];
6977        for i in 1..=258 {
6978            frames.push(encode(&mut enc, i).0);
6979        }
6980        assert!(
6981            enc.invalidate_reference(256),
6982            "the device's wrap frame is reported lost"
6983        );
6984        let (out, reference) = encode(&mut enc, 259);
6985        assert!(
6986            matches!(reference, Reference::Frame(r) if r < 256),
6987            "{reference:?} past the loss"
6988        );
6989        frames.push(out);
6990        let off = apart_without(Codec::H264, &frames, 256..259);
6991        assert!(
6992            off < 0.5,
6993            "past the loss at the wrap the frame decodes {off:.2} off"
6994        );
6995    }
6996
6997    /// On a real GPU, a CBR session resized 720p→1080p folds the new bitrate into the resize
6998    /// reconfigure (asserts `averageBitRate` updated to 8 Mbit/s) and the first post-resize frame is
6999    /// an IDR at the new dimensions. Ignored by default.
7000    #[test]
7001    #[ignore]
7002    fn gpu_resolution_reconfigure_cbr() {
7003        let mut s = settings(1280, 720, 60.0);
7004        s.video_cbr_mode = true;
7005        s.video_bitrate_kbps = 4000;
7006        let mut enc = host_session(&s).expect("NVENC init");
7007        let f720 = frame(1280, 720, 10);
7008        for i in 0..3u64 {
7009            enc.encode_cpu_argb(&f720, 1280 * 4, i, 25, i == 0)
7010                .expect("encode 720p");
7011        }
7012        s.width = 1920;
7013        s.height = 1080;
7014        s.video_bitrate_kbps = 8000;
7015        enc.reconfigure_resolution(&s).expect("cbr resize+rate");
7016        assert_eq!(enc.encode_config.rcParams.averageBitRate, 8_000_000);
7017        let f1080 = frame(1920, 1080, 40);
7018        let pkt = enc
7019            .encode_cpu_argb(&f1080, 1920 * 4, 3, 25, false)
7020            .expect("encode 1080p");
7021        assert_eq!(pkt[1] & 0x0f, FRAME_KEY);
7022        assert_eq!(wire_dims(&pkt), (1920, 1080));
7023    }
7024
7025    /// A session told to keep one reference frame (`video_reference_frames`) declares a decoded
7026    /// picture buffer of one, H.264 and H.265 alike, and keeps no anchor; AV1 keeps its eight.
7027    /// Ignored by default.
7028    #[test]
7029    #[ignore]
7030    fn gpu_sessions_keep_the_reference_frames_they_are_given() {
7031        use crate::encoders::sps::h264_max_num_ref_frames;
7032        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
7033            let mut s = settings(1280, 720, 60.0);
7034            s.codec = codec;
7035            s.omit_stripe_headers = true;
7036            s.acknowledge_references = true;
7037            s.video_reference_frames = 1;
7038            let mut enc = match host_session(&s) {
7039                Ok(enc) => enc,
7040                Err(e) => {
7041                    println!("{codec:?}: {e}");
7042                    continue;
7043                }
7044            };
7045            let key = enc
7046                .encode_cpu_argb(&frame(1280, 720, 10), 1280 * 4, 0, 25, true)
7047                .expect("encode");
7048            let anchors = enc
7049                .references
7050                .as_ref()
7051                .map_or(0, ReferenceWindow::anchor_count);
7052            match codec {
7053                Codec::Av1 => assert_eq!(enc.dpb, AV1_REFERENCES, "AV1"),
7054                _ => {
7055                    assert_eq!(enc.dpb, 1, "{codec:?}");
7056                    assert_eq!(anchors, 0, "{codec:?}");
7057                }
7058            }
7059            if codec == Codec::H264 {
7060                assert_eq!(h264_max_num_ref_frames(&key), Some(1));
7061            }
7062        }
7063    }
7064
7065    /// Every H.264 session bounds reordering at zero, 4:2:0 and 4:4:4, and so does the IDR an
7066    /// in-place resize forces, which re-declares the stream. Ignored by default.
7067    #[test]
7068    #[ignore]
7069    fn gpu_h264_bounds_reordering_at_zero() {
7070        use crate::encoders::sps::fixtures::assert_no_reorder;
7071        for fullcolor in [false, true] {
7072            let mut s = settings(1920, 1080, 60.0);
7073            s.video_fullcolor = fullcolor;
7074            s.omit_stripe_headers = true;
7075            let mut enc = match host_session(&s) {
7076                Ok(enc) => enc,
7077                Err(e) => {
7078                    println!("fullcolor {fullcolor}: {e}");
7079                    continue;
7080                }
7081            };
7082            let key = enc
7083                .encode_cpu_argb(&frame(1920, 1080, 10), 1920 * 4, 0, 25, true)
7084                .expect("encode 1080p");
7085            assert_no_reorder(&key, &format!("NVENC fullcolor {fullcolor} 1080p"));
7086            s.width = 1280;
7087            s.height = 720;
7088            if enc.reconfigure_resolution(&s).expect("in-place resize") {
7089                let key = enc
7090                    .encode_cpu_argb(&frame(1280, 720, 20), 1280 * 4, 1, 25, false)
7091                    .expect("encode 720p");
7092                assert_no_reorder(
7093                    &key,
7094                    &format!("NVENC fullcolor {fullcolor} 720p after the resize"),
7095                );
7096            }
7097        }
7098    }
7099
7100    /// On a real GPU, a resize the driver refuses ends in the rebuild its callers already do:
7101    /// the ladder the session came from opens a new one at the new size, declaring the buffer
7102    /// its own level admits. Forced by asking to raise the buffer a 1080p session declared,
7103    /// which the driver refuses. Ignored by default.
7104    #[test]
7105    #[ignore]
7106    fn gpu_a_refused_resize_is_rebuilt() {
7107        use crate::encoders::sps::h264_max_num_ref_frames;
7108        use crate::encoders::{FrameEncoder, FrameSource, select_frame_encoder};
7109        let mut s = settings(1920, 1080, 60.0);
7110        let mut enc = host_session(&s).expect("NVENC init");
7111        let key = enc
7112            .encode_cpu_argb(&frame(1920, 1080, 10), 1920 * 4, 0, 25, true)
7113            .expect("encode 1080p");
7114        assert_eq!(
7115            h264_max_num_ref_frames(&key[VIDEO_HEADER_LEN..]),
7116            Some(4),
7117            "1080p at level 4.2 admits four"
7118        );
7119        enc.dpb = REFERENCE_FRAMES;
7120        s.width = 1280;
7121        s.height = 720;
7122        let prior = Some(FrameEncoder::Nvenc(enc));
7123        let Some(FrameEncoder::Nvenc(mut enc)) =
7124            select_frame_encoder(&mut s, FrameSource::Host { rgba: false }, prior, "test")
7125        else {
7126            panic!("the ladder rebuilt no NVENC session");
7127        };
7128        assert_eq!(
7129            enc.references.as_ref().map_or(0, ReferenceWindow::next_pts),
7130            0,
7131            "a new session, not the refused one"
7132        );
7133        let key = enc
7134            .encode_cpu_argb(&frame(1280, 720, 20), 1280 * 4, 1, 25, false)
7135            .expect("encode 720p");
7136        assert_eq!(key[1] & 0x0f, FRAME_KEY);
7137        assert_eq!(wire_dims(&key), (1280, 720));
7138        assert_eq!(
7139            h264_max_num_ref_frames(&key[VIDEO_HEADER_LEN..]),
7140            Some(REFERENCE_FRAMES),
7141            "720p declares eight"
7142        );
7143    }
7144
7145    /// Test helper: `value` placed so it ends where a page the process cannot touch begins, so a
7146    /// driver reading or writing past it faults. The mapping lives as long as the process.
7147    fn guarded<T>(value: T) -> &'static mut T {
7148        unsafe {
7149            let page = libc::sysconf(libc::_SC_PAGESIZE) as usize;
7150            let map = libc::mmap(
7151                ptr::null_mut(),
7152                2 * page,
7153                libc::PROT_READ | libc::PROT_WRITE,
7154                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
7155                -1,
7156                0,
7157            );
7158            assert_ne!(map, libc::MAP_FAILED);
7159            assert_eq!(
7160                libc::mprotect(map.cast::<u8>().add(page).cast(), page, libc::PROT_NONE),
7161                0
7162            );
7163            let at = map
7164                .cast::<u8>()
7165                .add(page - std::mem::size_of::<T>())
7166                .cast::<T>();
7167            at.write(value);
7168            &mut *at
7169        }
7170    }
7171
7172    /// On a real GPU, at every API version a driver may be negotiated at, the driver stays inside
7173    /// the structs it is handed: each is placed against a page the process cannot touch, where a
7174    /// read or write past it faults, and every version runs in a process of its own, pinned there
7175    /// with `PIXELFLUX_NVENC_MAX_API`. Ignored by default.
7176    #[test]
7177    #[ignore]
7178    fn gpu_the_driver_stays_inside_the_structs_it_is_handed() {
7179        const PINNED: &str = "PIXELFLUX_TEST_NVENC_PINNED";
7180        if std::env::var_os(PINNED).is_none() {
7181            let name = format!(
7182                "{}::gpu_the_driver_stays_inside_the_structs_it_is_handed",
7183                module_path!().split_once("::").unwrap().1
7184            );
7185            for api in ["10.0", "11.0", "11.1", "12.0", "12.1", "13.0"] {
7186                let out = std::process::Command::new(std::env::current_exe().unwrap())
7187                    .args([
7188                        name.as_str(),
7189                        "--exact",
7190                        "--ignored",
7191                        "--nocapture",
7192                        "--test-threads=1",
7193                    ])
7194                    .env("PIXELFLUX_NVENC_MAX_API", api)
7195                    .env(PINNED, "1")
7196                    .output()
7197                    .expect("the test binary runs");
7198                let stdout = String::from_utf8_lossy(&out.stdout);
7199                let report = stdout
7200                    .lines()
7201                    .find(|l| l.contains("stayed inside"))
7202                    .unwrap_or_default();
7203                println!("{report}");
7204                assert!(
7205                    out.status.success() && !report.is_empty(),
7206                    "API {api}: {}\n{stdout}",
7207                    out.status
7208                );
7209            }
7210            return;
7211        }
7212        let mut enc = host_session(&settings(1280, 720, 60.0)).expect("NVENC init");
7213        unsafe {
7214            let funcs = enc.nvenc_funcs;
7215            let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
7216            let mut open = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
7217                version: sv(NvStruct::OpenSessionExParams),
7218                deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
7219                device: enc.cuda_context as *mut c_void,
7220                apiVersion: neg_api(),
7221                ..Default::default()
7222            };
7223            let mut session = ptr::null_mut();
7224            assert_eq!(
7225                (funcs.nvEncOpenEncodeSessionEx.unwrap())(&mut open, &mut session),
7226                NVENCSTATUS::NV_ENC_SUCCESS
7227            );
7228            enc.init_params.encodeConfig = &mut enc.encode_config;
7229            let init = guarded(Negotiated::new(enc.init_params));
7230            let status = (funcs.nvEncInitializeEncoder.unwrap())(session, &mut init.value);
7231            (funcs.nvEncDestroyEncoder.unwrap())(session);
7232            assert_eq!(status, NVENCSTATUS::NV_ENC_SUCCESS, "initialize");
7233            let reconfigure = guarded(reconfigure_params(
7234                &enc.init_params,
7235                nvenc_cur_ver(),
7236                true,
7237                true,
7238            ));
7239            let status = (funcs.nvEncReconfigureEncoder.unwrap())(
7240                enc.encoder_session,
7241                &mut reconfigure.value,
7242            );
7243            assert_eq!(status, NVENCSTATUS::NV_ENC_SUCCESS, "reconfigure");
7244            let output = enc.bitstream_buffers[0];
7245            let mut pic = NV_ENC_PIC_PARAMS {
7246                version: sv(NvStruct::PicParams),
7247                inputWidth: enc.width,
7248                inputHeight: enc.height,
7249                inputBuffer: enc.mapped_input_buffer,
7250                outputBitstream: output,
7251                bufferFmt: enc.input_format,
7252                pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
7253                ..Default::default()
7254            };
7255            assert_eq!(
7256                (funcs.nvEncEncodePicture.unwrap())(enc.encoder_session, &mut pic),
7257                NVENCSTATUS::NV_ENC_SUCCESS
7258            );
7259            let lock = guarded(Negotiated::new(NV_ENC_LOCK_BITSTREAM {
7260                version: sv(NvStruct::LockBitstream),
7261                outputBitstream: output,
7262                ..Default::default()
7263            }));
7264            let status = (funcs.nvEncLockBitstream.unwrap())(enc.encoder_session, &mut lock.value);
7265            assert_eq!(status, NVENCSTATUS::NV_ENC_SUCCESS, "lock");
7266            assert!(
7267                lock.value.bitstreamSizeInBytes > 0,
7268                "the locked bitstream carries the picture"
7269            );
7270            (funcs.nvEncUnlockBitstream.unwrap())(enc.encoder_session, output);
7271            (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
7272        }
7273        let (maj, min) = nvenc_cur_ver();
7274        println!("API {maj}.{min}: the driver stayed inside every struct");
7275    }
7276
7277    /// A source a capture keeps and repaints, as its shm segment is, is page-locked once and
7278    /// read as it is at each upload, and after `release_pinned_hosts`, as a reshape calls it, a
7279    /// new one is: each decoded frame shows the picture just painted, not an earlier one.
7280    /// Ignored by default.
7281    #[test]
7282    #[ignore]
7283    fn gpu_pinned_uploads_read_the_source_as_it_is_now() {
7284        use crate::webcam::decode::VideoDecoder;
7285        let (w, h) = (1280usize, 720usize);
7286        let mut enc = NvencEncoder::new(&settings(w as i32, h as i32, 60.0), ptr::null())
7287            .expect("NVENC init");
7288        assert!(enc.pin_uploads, "the production default pins");
7289        let mut dec = VideoDecoder::new(Codec::H264).expect("decoder");
7290        for (source, frames) in [(0, 0..4usize), (1, 4..8usize)] {
7291            if source == 1 {
7292                enc.release_pinned_hosts();
7293            }
7294            let mut shm = vec![0u8; w * h * 4];
7295            for i in frames {
7296                shm.copy_from_slice(&moving_frame(w, h, i));
7297                let pkt = enc
7298                    .encode_cpu_packed(&shm, w * 4, false, i as u64, 25, i == 0)
7299                    .expect("encode");
7300                let psnr = luma_psnr(&mut dec, &pkt, &moving_frame(w, h, i), w, h);
7301                assert!(
7302                    psnr > 30.0,
7303                    "source {source}, frame {i}: {psnr:.1} dB against the picture just painted"
7304                );
7305            }
7306        }
7307    }
7308
7309    /// A client's memfd pool replaced by one mapped at the same address, as a toolkit swaps its
7310    /// buffers, uploads the picture the new pool holds: the rows are read where they lie and
7311    /// never page-locked, so no registration keeps the old pool's pages at that address.
7312    /// Ignored by default.
7313    #[test]
7314    #[ignore]
7315    fn gpu_a_client_pool_mapped_where_the_last_one_was_uploads_its_own_picture() {
7316        use crate::webcam::decode::VideoDecoder;
7317        let (w, h) = (1280usize, 720usize);
7318        let len = w * h * 4;
7319        let mut enc = NvencEncoder::new(&settings(w as i32, h as i32, 60.0), ptr::null())
7320            .expect("NVENC init");
7321        assert!(enc.pin_uploads, "the production default pins");
7322        let mut dec = VideoDecoder::new(Codec::H264).expect("decoder");
7323        let mut at: *mut c_void = ptr::null_mut();
7324        for (pool, frames) in [(0, 0..4usize), (1, 4..8usize)] {
7325            let fd = crate::memfd_create(c"client-pool", libc::MFD_CLOEXEC);
7326            assert!(fd >= 0, "memfd_create");
7327            let flags = if at.is_null() {
7328                libc::MAP_SHARED
7329            } else {
7330                libc::MAP_SHARED | libc::MAP_FIXED
7331            };
7332            // SAFETY: a fresh memfd sized to `len` and mapped whole; the second pool is placed
7333            // over the range the first one was unmapped from, which nothing else holds.
7334            let rows = unsafe {
7335                assert_eq!(libc::ftruncate(fd, len as libc::off_t), 0);
7336                let p = libc::mmap(at, len, libc::PROT_READ | libc::PROT_WRITE, flags, fd, 0);
7337                libc::close(fd);
7338                assert_ne!(p, libc::MAP_FAILED, "mmap");
7339                std::slice::from_raw_parts_mut(p as *mut u8, len)
7340            };
7341            at = rows.as_mut_ptr().cast();
7342            for i in frames {
7343                rows.copy_from_slice(&moving_frame(w, h, i));
7344                let pkt = enc
7345                    .encode_client_packed(rows, w * 4, false, i as u64, 25, i == 0)
7346                    .expect("encode");
7347                let psnr = luma_psnr(&mut dec, &pkt, &moving_frame(w, h, i), w, h);
7348                assert!(
7349                    psnr > 30.0,
7350                    "pool {pool}, frame {i}: {psnr:.1} dB against the picture just painted"
7351                );
7352            }
7353            assert!(
7354                enc.pin_uploads,
7355                "the client path restores the production default"
7356            );
7357            // SAFETY: the mapping made above, which no slice outlives.
7358            unsafe { libc::munmap(at, len) };
7359        }
7360    }
7361
7362    /// On a real GPU whose driver caps the NVENC sessions a device runs at once, as it does on
7363    /// consumer boards, the probe of a device with every session taken answers `SESSIONS_TAKEN`,
7364    /// and once one frees it lists the codecs again. A device that takes 65 sessions at once has
7365    /// no cap to reach and says so. The sessions are closed before anything is asserted, so a
7366    /// failure leaves none held against the cap for the tests after it. Ignored by default.
7367    /// A capture restarted while a game holds the GPU's memory keeps its place: once a session
7368    /// has opened, the device's primary CUDA context outlives it (`keep_primary_context`), so a
7369    /// session reopened after another context has taken all but 48 MiB still opens and encodes,
7370    /// where a context created afresh needs about 106 MiB.
7371    #[test]
7372    #[ignore]
7373    fn gpu_a_session_reopened_under_a_full_gpu_keeps_its_context() {
7374        type Ctx = *mut c_void;
7375        let first = host_session(&settings(256, 128, 60.0)).expect("NVENC init");
7376        let device = first.cuda_device;
7377        drop(first);
7378        unsafe {
7379            let lib = Library::new("libcuda.so.1").expect("libcuda");
7380            let state: Symbol<unsafe extern "C" fn(CUdevice, *mut u32, *mut i32) -> CUresult> =
7381                lib.get(b"cuDevicePrimaryCtxGetState\0").unwrap();
7382            let create: Symbol<unsafe extern "C" fn(*mut Ctx, u32, CUdevice) -> CUresult> =
7383                lib.get(b"cuCtxCreate_v2\0").unwrap();
7384            let destroy: Symbol<unsafe extern "C" fn(Ctx) -> CUresult> =
7385                lib.get(b"cuCtxDestroy_v2\0").unwrap();
7386            let info: Symbol<unsafe extern "C" fn(*mut usize, *mut usize) -> CUresult> =
7387                lib.get(b"cuMemGetInfo_v2\0").unwrap();
7388            let alloc: Symbol<unsafe extern "C" fn(*mut u64, usize) -> CUresult> =
7389                lib.get(b"cuMemAlloc_v2\0").unwrap();
7390            let (mut flags, mut active) = (0u32, 0i32);
7391            assert_eq!(
7392                state(device, &mut flags, &mut active),
7393                CUresult::CUDA_SUCCESS
7394            );
7395
7396            let mut game: Ctx = ptr::null_mut();
7397            assert_eq!(create(&mut game, 0, device), CUresult::CUDA_SUCCESS);
7398            let (mut free, mut total) = (0usize, 0usize);
7399            let mut chunk = 256usize << 20;
7400            let mut taken = 0usize;
7401            while chunk >= 1 << 20 {
7402                let mut ptr = 0u64;
7403                info(&mut free, &mut total);
7404                if free < (48 << 20) + chunk || alloc(&mut ptr, chunk) != CUresult::CUDA_SUCCESS {
7405                    chunk /= 2;
7406                } else {
7407                    taken += chunk;
7408                }
7409            }
7410            info(&mut free, &mut total);
7411            println!(
7412                "took {} MiB; {} MiB of {} left",
7413                taken >> 20,
7414                free >> 20,
7415                total >> 20
7416            );
7417            let reopened = host_session(&settings(256, 128, 60.0)).map(|mut enc| {
7418                enc.encode_cpu_argb(&vec![0x40u8; 256 * 128 * 4], 256 * 4, 0, 25, true)
7419                    .map(|out| out.len())
7420            });
7421            destroy(game);
7422            println!(
7423                "primary context active after the first session: {active}; reopened: {reopened:?}"
7424            );
7425            assert_eq!(
7426                active, 1,
7427                "the primary context outlived the session that opened it"
7428            );
7429            let bytes = reopened.expect("the session reopens").expect("and encodes");
7430            assert!(bytes > 0);
7431        }
7432    }
7433
7434    #[test]
7435    #[ignore]
7436    fn gpu_a_device_out_of_sessions_says_so() {
7437        let enc = host_session(&settings(256, 128, 60.0)).expect("NVENC init");
7438        let mut sessions = Vec::new();
7439        let refused = unsafe {
7440            loop {
7441                let mut open = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
7442                    version: sv(NvStruct::OpenSessionExParams),
7443                    deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
7444                    device: enc.cuda_context as *mut c_void,
7445                    apiVersion: neg_api(),
7446                    ..Default::default()
7447                };
7448                let mut session = ptr::null_mut();
7449                match (enc.nvenc_funcs.nvEncOpenEncodeSessionEx.unwrap())(&mut open, &mut session) {
7450                    NVENCSTATUS::NV_ENC_SUCCESS => sessions.push(session),
7451                    status => break Some(status),
7452                }
7453                if sessions.len() == 64 {
7454                    break None;
7455                }
7456            }
7457        };
7458        let taken = probe_codecs(0);
7459        let opened = sessions.len();
7460        for session in sessions.drain(..) {
7461            unsafe { (enc.nvenc_funcs.nvEncDestroyEncoder.unwrap())(session) };
7462        }
7463        let Some(status) = refused else {
7464            println!("this device took 65 sessions at once: no cap to reach");
7465            return;
7466        };
7467        assert_eq!(
7468            session_refusal(status),
7469            SESSIONS_TAKEN,
7470            "session {} refused: {status:?}",
7471            opened + 2
7472        );
7473        assert_eq!(
7474            taken,
7475            Err(SESSIONS_TAKEN.to_string()),
7476            "every session taken"
7477        );
7478        assert!(
7479            probe_codecs(0).is_ok_and(|codecs| !codecs.is_empty()),
7480            "a freed session is listed again"
7481        );
7482    }
7483
7484    /// On a real GPU, a live frame-rate drop keeps the level the decoded picture buffer needs: a
7485    /// 1080p session opened at 120 fps declares the eight frames its level admits there, and the
7486    /// drop to 60 fps, whose own level admits fewer, is taken at that level, so a CBR session
7487    /// spends about twice the bits on each frame. Ignored by default.
7488    #[test]
7489    #[ignore]
7490    fn gpu_frame_rate_drop_keeps_the_level_the_buffer_needs() {
7491        for codec in [Codec::H264, Codec::H265] {
7492            let mut s = settings(1920, 1080, 120.0);
7493            s.codec = codec;
7494            s.video_cbr_mode = true;
7495            s.video_bitrate_kbps = 8000;
7496            let Some(mut enc) = codec_session(&s) else {
7497                continue;
7498            };
7499            let level = enc.declared_level();
7500            let frames: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(1920, 1080, i)).collect();
7501            let kbit = |enc: &mut NvencEncoder, range: std::ops::Range<u64>| {
7502                let n = (range.end - range.start) as f64;
7503                let bytes: usize = range
7504                    .map(|i| {
7505                        enc.encode_cpu_argb(&frames[i as usize % 16], 1920 * 4, i, 25, i == 0)
7506                            .expect("encode")
7507                            .len()
7508                            - VIDEO_HEADER_LEN
7509                    })
7510                    .sum();
7511                bytes as f64 * 8.0 / n / 1000.0
7512            };
7513            kbit(&mut enc, 0..20);
7514            let at120 = kbit(&mut enc, 20..80);
7515            s.target_fps = 60.0;
7516            assert!(
7517                enc.reconfigure_rate(&s),
7518                "{codec:?}: the driver refused 60 fps"
7519            );
7520            assert_eq!(
7521                enc.declared_level(),
7522                level,
7523                "{codec:?}: the level the buffer needs stays"
7524            );
7525            let at60 = kbit(&mut enc, 80..200);
7526            println!("{codec:?}: {at120:.1} kbit a frame at 120 fps, {at60:.1} at 60");
7527            assert!(
7528                at60 > 1.4 * at120,
7529                "{codec:?}: halving the frame rate left {at60:.1} kbit a frame against {at120:.1}"
7530            );
7531        }
7532    }
7533
7534    /// On a real GPU, a session of every codec the device carries is opened at the capture's
7535    /// rate as the fraction it names, a live change of rate reaches the driver the same way, and
7536    /// an H.264 stream declares the rate in its timing. Ignored by default.
7537    #[test]
7538    #[ignore]
7539    fn gpu_frame_rate_reaches_the_driver_as_its_fraction() {
7540        let f = frame(640, 360, 30);
7541        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
7542            for (num, den) in [(60000u32, 1001u32), (120000, 1001), (144000, 1001), (60, 1)] {
7543                let fps = f64::from(num) / f64::from(den);
7544                let mut s = settings(640, 360, fps);
7545                s.codec = codec;
7546                s.video_cbr_mode = true;
7547                s.video_bitrate_kbps = 8000;
7548                let mut enc = match host_session(&s) {
7549                    Ok(enc) => enc,
7550                    Err(e) if e.contains("engine") => {
7551                        println!("{codec:?}: {e}");
7552                        break;
7553                    }
7554                    Err(e) => panic!("{codec:?} at {num}/{den}: {e}"),
7555                };
7556                assert_eq!(
7557                    (enc.init_params.frameRateNum, enc.init_params.frameRateDen),
7558                    (num, den),
7559                    "{codec:?}"
7560                );
7561                let pkt = enc
7562                    .encode_cpu_argb(&f, 640 * 4, 0, 25, true)
7563                    .expect("encode");
7564                if codec == Codec::H264 {
7565                    let (tick, scale) = crate::encoders::sps::h264_timing(&pkt[VIDEO_HEADER_LEN..])
7566                        .expect("SPS timing");
7567                    assert_eq!(
7568                        u64::from(scale) * u64::from(den),
7569                        2 * u64::from(num) * u64::from(tick),
7570                        "the SPS declares {scale}/2x{tick}, not {num}/{den}"
7571                    );
7572                }
7573                s.target_fps = 30.0;
7574                assert!(
7575                    enc.reconfigure_rate(&s),
7576                    "{codec:?}: the driver refused 30 fps"
7577                );
7578                assert_eq!(
7579                    (enc.init_params.frameRateNum, enc.init_params.frameRateDen),
7580                    (30, 1)
7581                );
7582                s.target_fps = fps;
7583                assert!(
7584                    enc.reconfigure_rate(&s),
7585                    "{codec:?}: the driver refused {num}/{den}"
7586                );
7587                assert_eq!(
7588                    (enc.init_params.frameRateNum, enc.init_params.frameRateDen),
7589                    (num, den),
7590                    "{codec:?}"
7591                );
7592                enc.encode_cpu_argb(&f, 640 * 4, 1, 25, false)
7593                    .expect("encode after the change");
7594            }
7595        }
7596    }
7597
7598    /// On a real GPU, a live frame-rate rise past the declared level's macroblock rate raises the
7599    /// level at a key frame, whose sequence header is where a decoder learns it: a 1080p session
7600    /// opened at 60 fps declares level 4.2, and 120 fps takes 5.1. Ignored by default.
7601    #[test]
7602    #[ignore]
7603    fn gpu_frame_rate_rise_raises_the_level_at_a_key_frame() {
7604        let mut s = settings(1920, 1080, 60.0);
7605        let mut enc = host_session(&s).expect("NVENC init");
7606        let f = frame(1920, 1080, 30);
7607        for i in 0..3u64 {
7608            enc.encode_cpu_argb(&f, 1920 * 4, i, 25, i == 0)
7609                .expect("encode");
7610        }
7611        assert_eq!(enc.declared_level(), 42);
7612        s.target_fps = 120.0;
7613        assert!(enc.reconfigure_rate(&s), "the driver refused 120 fps");
7614        let pkt = enc
7615            .encode_cpu_argb(&f, 1920 * 4, 3, 25, false)
7616            .expect("encode");
7617        assert_eq!(
7618            pkt[1] & 0x0f,
7619            FRAME_KEY,
7620            "the raised level reaches the stream at a key frame"
7621        );
7622        let sps = crate::encoders::codec::annexb_nals(&pkt[VIDEO_HEADER_LEN..])
7623            .find(|n| n[0] & 0x1f == 7)
7624            .expect("an SPS");
7625        assert_eq!(sps[3], 51, "level_idc");
7626    }
7627
7628    /// On a real GPU, a live CBR rate change moves the VBV initial delay with the buffer: after
7629    /// the bitrate is lowered the delay equals the new, smaller buffer, the driver takes the
7630    /// reconfigure, and a steady desktop-like sequence lands at the new rate. What the driver
7631    /// makes of a delay left above the buffer is printed rather than asserted. Ignored by default.
7632    /// A capped constant quantizer under a peak its picture never reaches codes every frame as
7633    /// the uncapped session does, byte for byte: it never leaves the constant quantizer. Ignored
7634    /// by default.
7635    #[test]
7636    #[ignore]
7637    fn gpu_a_capped_constant_quantizer_codes_as_uncapped_under_a_peak_it_does_not_reach() {
7638        let (w, h) = (640usize, 360usize);
7639        let plain = settings(w as i32, h as i32, 60.0);
7640        let capped = CaptureSettings {
7641            video_bitrate_kbps: 200_000,
7642            video_vbv_multiplier: crate::encoders::CAPPED_VBV_FRAMES,
7643            ..plain.clone()
7644        };
7645        let mut a = host_session(&plain).expect("NVENC init");
7646        let mut b = host_session(&capped).expect("NVENC init");
7647        assert!(a.peak.is_none() && b.peak.is_some());
7648        for i in 0..60u64 {
7649            let f = moving_frame(w, h, i as usize);
7650            let one = a.encode_cpu_argb(&f, w * 4, i, 25, i == 0).expect("encode");
7651            let two = b.encode_cpu_argb(&f, w * 4, i, 25, i == 0).expect("encode");
7652            assert!(
7653                one == two,
7654                "frame {i}: {} bytes uncapped, {} capped",
7655                one.len(),
7656                two.len()
7657            );
7658        }
7659        assert_eq!(
7660            b.encode_config.rcParams.rateControlMode,
7661            NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP
7662        );
7663    }
7664
7665    /// A capped constant quantizer whose content overflows its peak binds to it without a key
7666    /// frame and codes within it, never finer than its quantizer, and once the peak rises past
7667    /// what the content takes goes back to the constant quantizer, again without one. Ignored
7668    /// by default.
7669    #[test]
7670    #[ignore]
7671    fn gpu_a_capped_constant_quantizer_binds_and_releases_without_a_key_frame() {
7672        use crate::encoders::codec::{FRAME_KEY, parse_video_type};
7673        let (w, h) = (640usize, 360usize);
7674        let mut s = CaptureSettings {
7675            video_bitrate_kbps: 200_000,
7676            video_vbv_multiplier: crate::encoders::CAPPED_VBV_FRAMES,
7677            ..settings(w as i32, h as i32, 60.0)
7678        };
7679        let mut enc = host_session(&s).expect("NVENC init");
7680        let floor = enc.current_qp;
7681        let mut sizes = Vec::new();
7682        let code = |enc: &mut NvencEncoder, i: u64, crf: u32| {
7683            let out = enc
7684                .encode_cpu_argb(
7685                    &crate::encoders::scrolled_text(w, h, i as usize),
7686                    w * 4,
7687                    i,
7688                    crf,
7689                    i == 0,
7690                )
7691                .expect("encode");
7692            assert!(
7693                i == 0 || parse_video_type(out[1]).map(|(_, k)| k) != Some(FRAME_KEY),
7694                "frame {i} is a key frame"
7695            );
7696            (out.len() - VIDEO_HEADER_LEN) * 8
7697        };
7698        for i in 0..30 {
7699            sizes.push(code(&mut enc, i, 25));
7700        }
7701        let kbps = |f: &[usize]| f.iter().sum::<usize>() as f64 * 60.0 / f.len() as f64 / 1000.0;
7702        let free = kbps(&sizes[1..]);
7703        s.video_bitrate_kbps = (free / 3.0) as i32;
7704        enc.reconfigure_rate(&s);
7705        let mut bound = Vec::new();
7706        for i in 30..150 {
7707            bound.push(code(&mut enc, i, 25));
7708            if i >= 40 {
7709                assert!(
7710                    enc.last_avg_qp >= floor,
7711                    "frame {i} coded finer than the quantizer"
7712                );
7713            }
7714        }
7715        assert_eq!(
7716            enc.encode_config.rcParams.rateControlMode,
7717            NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR,
7718            "content over the peak binds the session"
7719        );
7720        let held = kbps(&bound[30..]);
7721        println!(
7722            "free {free:.0} kbps, held {held:.0} kbps under a {} kbps peak",
7723            s.video_bitrate_kbps
7724        );
7725        assert!(
7726            held <= 1.1 * f64::from(s.video_bitrate_kbps),
7727            "the session ran at {held:.0} kbps under a {} kbps peak",
7728            s.video_bitrate_kbps
7729        );
7730        s.video_bitrate_kbps = 200_000;
7731        enc.reconfigure_rate(&s);
7732        for i in 150..150 + u64::from(PeakBucket::RELEASE_FRAMES) + 10 {
7733            code(&mut enc, i, 25);
7734        }
7735        assert_eq!(
7736            enc.encode_config.rcParams.rateControlMode,
7737            NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP,
7738            "a peak above what the content takes releases it"
7739        );
7740        assert_eq!(enc.last_avg_qp, floor);
7741        // Released, it codes at the quantizer the session asks for, as an uncapped one does.
7742        let paint = enc.codec.hardware_quantizer(Hardware::Nvenc, 18);
7743        for i in 190..200 {
7744            code(&mut enc, i, 18);
7745        }
7746        assert_eq!(
7747            enc.last_avg_qp, paint,
7748            "a released session codes at its paint-over quantizer"
7749        );
7750        for i in 200..210 {
7751            code(&mut enc, i, 25);
7752        }
7753        assert_eq!(enc.last_avg_qp, floor);
7754    }
7755
7756    /// A capped constant quantizer bound to its peak stays bound while frames refining every
7757    /// block through the QP map 25 steps finer are coded: the constant rate holds their mean
7758    /// quantizer over the floor, so none counts toward the release. Ignored by default.
7759    #[test]
7760    #[ignore]
7761    fn gpu_a_bound_session_refining_blocks_is_not_released_by_their_deltas() {
7762        let (w, h) = (640usize, 360usize);
7763        let mut s = CaptureSettings {
7764            video_bitrate_kbps: 200_000,
7765            video_vbv_multiplier: crate::encoders::CAPPED_VBV_FRAMES,
7766            ..settings(w as i32, h as i32, 60.0)
7767        };
7768        let mut enc = host_session(&s).expect("NVENC init");
7769        let mut ledger = crate::ledger::BlockLedger::new(w as i32, h as i32);
7770        ledger.frame([]);
7771        let plan = ledger.plan(0, usize::MAX, usize::MAX).expect("every block");
7772        let mut sizes = Vec::new();
7773        let code = |enc: &mut NvencEncoder, i: u64, refine: bool| {
7774            if refine {
7775                enc.refine_blocks(&crate::ledger::Refinement {
7776                    plan: plan.clone(),
7777                    target: 0,
7778                    from: 25,
7779                    reach: u32::MAX,
7780                });
7781            }
7782            let out = enc
7783                .encode_cpu_argb(
7784                    &crate::encoders::scrolled_text(w, h, i as usize),
7785                    w * 4,
7786                    i,
7787                    25,
7788                    i == 0,
7789                )
7790                .expect("encode");
7791            out.len() * 8
7792        };
7793        for i in 0..30 {
7794            sizes.push(code(&mut enc, i, false));
7795        }
7796        let free = sizes[1..].iter().sum::<usize>() as f64 * 60.0 / 29.0 / 1000.0;
7797        s.video_bitrate_kbps = (free / 3.0) as i32;
7798        enc.reconfigure_rate(&s);
7799        let (mut released, mut counted) = (0, 0);
7800        for i in 30..200 {
7801            code(&mut enc, i, i >= 60);
7802            if i >= 60 {
7803                released += usize::from(!enc.peak_bound());
7804                counted = counted.max(enc.peak.map_or(0, |p| p.floor_run()));
7805            }
7806        }
7807        println!(
7808            "{released} of 140 refining frames coded released, {counted} counted at the floor in a \
7809             row, free {free:.0} kbps"
7810        );
7811        assert_eq!(
7812            released, 0,
7813            "the session let go of a peak its content overflows"
7814        );
7815        assert_eq!(counted, 0, "a refining frame counted toward the release");
7816    }
7817
7818    #[test]
7819    #[ignore]
7820    fn gpu_rate_reconfigure_moves_the_vbv_initial_delay_with_the_buffer() {
7821        let (w, h) = (1920usize, 1080usize);
7822        let mut s = settings(w as i32, h as i32, 60.0);
7823        s.video_cbr_mode = true;
7824        s.video_bitrate_kbps = 8000;
7825        let mut enc = host_session(&s).expect("NVENC init");
7826        let rate = |enc: &NvencEncoder| {
7827            let rc = enc.encode_config.rcParams;
7828            (rc.averageBitRate, rc.vbvBufferSize, rc.vbvInitialDelay)
7829        };
7830        let vbv8 = cbr_vbv(&s, 8_000_000);
7831        assert_eq!(rate(&enc), (8_000_000, vbv8, vbv8));
7832        let frames: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(w, h, i)).collect();
7833        enc.encode_cpu_argb(&frames[0], w * 4, 0, 25, true)
7834            .expect("encode");
7835
7836        s.video_bitrate_kbps = 2000;
7837        assert!(enc.reconfigure_rate(&s), "the driver takes the lower rate");
7838        let vbv2 = cbr_vbv(&s, 2_000_000);
7839        assert_eq!(rate(&enc), (2_000_000, vbv2, vbv2));
7840        let n = 120u64;
7841        let mut bytes = 0usize;
7842        for i in 1..=n {
7843            let pkt = enc
7844                .encode_cpu_argb(&frames[i as usize % 16], w * 4, i, 25, false)
7845                .expect("encode");
7846            bytes += pkt.len().saturating_sub(VIDEO_HEADER_LEN);
7847        }
7848        let kbps = bytes as f64 * 8.0 * 60.0 / n as f64 / 1000.0;
7849        println!("CBR 2000 kbps after the rate change: {kbps:.0} kbps over {n} steady frames");
7850        assert!(
7851            (1500.0..=2500.0).contains(&kbps),
7852            "the session encodes at the new rate: {kbps:.0} kbps"
7853        );
7854
7855        enc.encode_config.rcParams.vbvInitialDelay = vbv2 * 4;
7856        let (status, detail) = reconfigure_raw(&mut enc);
7857        println!("a VBV initial delay of four buffers: {status:?} ({detail})");
7858        enc.encode_config.rcParams.vbvInitialDelay = vbv2;
7859        assert_eq!(reconfigure_raw(&mut enc).0, NVENCSTATUS::NV_ENC_SUCCESS);
7860        enc.encode_cpu_argb(&frames[7], w * 4, n + 1, 25, false)
7861            .expect("the session still encodes");
7862    }
7863
7864    /// The slice count and the HEVC tier are properties of the bitstream a client decodes, so
7865    /// they are read back out of it on a real GPU: four VCL NAL units per H.264 frame and one
7866    /// per HEVC frame at two geometries, and an HEVC SPS carrying the High tier flag beside the
7867    /// pinned level. Ignored by default.
7868    #[test]
7869    #[ignore]
7870    fn gpu_frames_carry_their_slices_and_hevc_declares_high_tier() {
7871        use crate::encoders::codec::annexb_nals;
7872        for (w, h) in [(1280usize, 720usize), (640, 480)] {
7873            for codec in [Codec::H264, Codec::H265] {
7874                let mut s = settings(w as i32, h as i32, 60.0);
7875                s.codec = codec;
7876                let Some(mut enc) = codec_session(&s) else {
7877                    continue;
7878                };
7879                for i in 0..4u64 {
7880                    let pkt = enc
7881                        .encode_cpu_argb(&frame(w, h, 20 + i as u8), w * 4, i, 25, i == 0)
7882                        .expect("encode");
7883                    let au = &pkt[VIDEO_HEADER_LEN..];
7884                    let vcl = annexb_nals(au)
7885                        .filter(|nal| match codec {
7886                            Codec::H265 => (nal[0] >> 1) & 0x3f < 32,
7887                            _ => matches!(nal[0] & 0x1f, 1 | 5),
7888                        })
7889                        .count();
7890                    let slices = if codec == Codec::H265 {
7891                        1
7892                    } else {
7893                        SLICES_PER_FRAME as usize
7894                    };
7895                    assert_eq!(vcl, slices, "{codec:?} {w}x{h} frame {i} slices");
7896                    if codec == Codec::H264 && i == 0 {
7897                        // After the one-byte NAL header the SPS RBSP carries profile_idc, the
7898                        // constraint-flag byte, then level_idc, which must be the current
7899                        // geometry's level rather than the 4K-headroom one so a decoder gating
7900                        // on it accepts the picture.
7901                        let sps = annexb_nals(au)
7902                            .find(|nal| nal[0] & 0x1f == 7)
7903                            .map(rbsp)
7904                            .expect("an SPS on the key frame");
7905                        assert_eq!(
7906                            u32::from(sps[3]),
7907                            nvenc_level(Codec::H264, w as u32, h as u32, 60, 0, true),
7908                            "level_idc at {w}x{h}"
7909                        );
7910                    }
7911                    if codec == Codec::H265 && i == 0 {
7912                        let sps = annexb_nals(au)
7913                            .find(|nal| (nal[0] >> 1) & 0x3f == 33)
7914                            .map(rbsp)
7915                            .expect("an SPS on the key frame");
7916                        // After the two-byte NAL header and the byte holding the VPS id, the
7917                        // sub-layer count, and the nesting flag, profile_tier_level opens with
7918                        // general_profile_space (2), general_tier_flag (1), and
7919                        // general_profile_idc (5); general_level_idc follows the 32
7920                        // compatibility flags and the 48 constraint bits.
7921                        assert_eq!((sps[3] >> 5) & 1, 1, "general_tier_flag at {w}x{h}");
7922                        assert_eq!(
7923                            u32::from(sps[14]),
7924                            nvenc_level(Codec::H265, w as u32, h as u32, 60, 0, true),
7925                            "general_level_idc at {w}x{h}"
7926                        );
7927                    }
7928                }
7929            }
7930        }
7931    }
7932
7933    /// The first coding tree block of each slice of an HEVC access unit, read out of the slice
7934    /// segment headers (the PPS has no dependent slice segments, which NVENC never enables), and
7935    /// whether the access unit's PPS, where it carries one, filters across slice boundaries.
7936    fn hevc_slice_starts(au: &[u8], ctbs: usize) -> (Vec<u32>, Option<bool>) {
7937        use crate::encoders::codec::annexb_nals;
7938        struct Bits<'a>(&'a [u8], usize);
7939        impl Bits<'_> {
7940            fn u(&mut self, n: u32) -> u32 {
7941                (0..n).fold(0, |acc, _| {
7942                    let bit = (self.0[self.1 / 8] >> (7 - self.1 % 8)) & 1;
7943                    self.1 += 1;
7944                    (acc << 1) | u32::from(bit)
7945                })
7946            }
7947            fn ue(&mut self) -> u32 {
7948                let mut zeros = 0;
7949                while self.u(1) == 0 {
7950                    zeros += 1;
7951                }
7952                (1 << zeros) - 1 + self.u(zeros)
7953            }
7954        }
7955        let address_bits = usize::BITS - (ctbs - 1).leading_zeros();
7956        let (mut starts, mut across) = (Vec::new(), None);
7957        for nal in annexb_nals(au) {
7958            let kind = (nal[0] >> 1) & 0x3f;
7959            let body = rbsp(nal);
7960            let mut b = Bits(&body[2..], 0);
7961            if kind == 34 {
7962                b.ue();
7963                b.ue();
7964                b.u(7);
7965                b.ue();
7966                b.ue();
7967                let _qp = b.ue();
7968                b.u(2);
7969                if b.u(1) == 1 {
7970                    b.ue();
7971                }
7972                b.ue();
7973                b.ue();
7974                b.u(4);
7975                let tiles = b.u(1) == 1;
7976                b.u(1);
7977                assert!(!tiles, "NVENC's HEVC codes no tiles");
7978                across = Some(b.u(1) == 1);
7979            } else if kind < 32 {
7980                let first = b.u(1) == 1;
7981                if (16..=23).contains(&kind) {
7982                    b.u(1);
7983                }
7984                b.ue();
7985                starts.push(if first { 0 } else { b.u(address_bits) });
7986            }
7987        }
7988        (starts, across)
7989    }
7990
7991    /// On a real GPU: an HEVC band frame starts a slice at its band's first coding tree block,
7992    /// across which the session never filters, so the rows the bands before refined keep their
7993    /// pixels; a band starting within the first `BAND_SLICES_MAX`th keeps the frame's one slice,
7994    /// and the frame after it is one slice again. Every frame decodes. Ignored by default.
7995    #[test]
7996    #[ignore]
7997    fn gpu_hevc_band_frames_start_a_slice_at_the_band() {
7998        use crate::webcam::decode::{Decoder as _, VideoDecoder};
7999        let (w, h) = (1920usize, 1080usize);
8000        let ctbs = w.div_ceil(32) * h.div_ceil(32);
8001        let mut s = settings(w as i32, h as i32, 60.0);
8002        s.codec = Codec::H265;
8003        s.omit_stripe_headers = true;
8004        let Some(mut enc) = codec_session(&s) else {
8005            return;
8006        };
8007        let still = frame(w, h, 40);
8008        let mut frames = Vec::new();
8009        let mut encode = |enc: &mut NvencEncoder, i: u64| {
8010            let pkt = enc
8011                .encode_cpu_argb(&still, w * 4, i, 25, i == 0)
8012                .expect("encode");
8013            frames.push(pkt.clone());
8014            hevc_slice_starts(&pkt, ctbs)
8015        };
8016        let (starts, across) = encode(&mut enc, 0);
8017        assert_eq!(starts, [0], "a key frame is one slice");
8018        assert_eq!(
8019            across,
8020            Some(false),
8021            "the PPS filters across no slice boundary"
8022        );
8023        let mut i = 1u64;
8024        for share in [0.5, 0.25, 0.75, 0.13, 0.05] {
8025            assert_eq!(
8026                encode(&mut enc, i).0,
8027                [0],
8028                "frame {i} before a band is one slice"
8029            );
8030            i += 1;
8031            enc.hold_quantizer(10, Some((share, share + 0.02)));
8032            let (starts, _) = encode(&mut enc, i);
8033            i += 1;
8034            let first = (share * ctbs as f64).floor() as usize;
8035            if ctbs.div_ceil(first) <= BAND_SLICES_MAX {
8036                assert!(
8037                    starts.contains(&(first as u32)) && starts.len() <= BAND_SLICES_MAX,
8038                    "a band from {first} starts a slice: {starts:?}"
8039                );
8040            } else {
8041                assert_eq!(starts, [0], "a band from {first} keeps the frame's slice");
8042            }
8043        }
8044        assert_eq!(
8045            encode(&mut enc, i).0,
8046            [0],
8047            "the frame after a band is one slice"
8048        );
8049        let mut decoder = VideoDecoder::new(Codec::H265).unwrap();
8050        for (k, f) in frames.iter().enumerate() {
8051            assert!(decoder.decode(f).expect("decode"), "frame {k}");
8052        }
8053    }
8054
8055    /// On a real GPU: what `SLICES_PER_FRAME` slices cost against one at a fixed quantizer,
8056    /// H.264 and HEVC at 1080p on the gradient frames the other benches use. Prints the rates and
8057    /// the difference; ignored by default.
8058    #[test]
8059    #[ignore]
8060    fn gpu_bench_slices() {
8061        let (w, h) = (1920usize, 1080usize);
8062        let n: usize = std::env::var("NVENC_BENCH_FRAMES")
8063            .ok()
8064            .and_then(|v| v.parse().ok())
8065            .unwrap_or(240);
8066        let frames: Vec<Vec<u8>> = (0..8u8).map(|k| frame(w, h, 10 + 30 * k)).collect();
8067        for codec in [Codec::H264, Codec::H265] {
8068            let mut s = settings(w as i32, h as i32, 60.0);
8069            s.codec = codec;
8070            let mut sizes = Vec::new();
8071            for slices in [1u32, SLICES_PER_FRAME] {
8072                let tuning = NvencTuning {
8073                    slices: Some(slices),
8074                    ..NvencTuning::default()
8075                };
8076                let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), tuning) {
8077                    Ok(enc) => enc,
8078                    Err(e) => {
8079                        println!("{codec:?}: {e}");
8080                        continue;
8081                    }
8082                };
8083                enc.encode_cpu_packed(&frames[0], w * 4, false, 0, 25, true)
8084                    .expect("warm-up");
8085                let mut bytes = 0usize;
8086                per_frame(&format!("{codec:?} {slices} slice(s)"), n, |i| {
8087                    bytes += enc
8088                        .encode_cpu_packed(&frames[i % 8], w * 4, false, 1 + i as u64, 25, false)
8089                        .expect("encode")
8090                        .len();
8091                });
8092                println!("    {} kbps", bytes * 8 * 60 / n / 1000);
8093                sizes.push(bytes);
8094            }
8095            if let [one, many] = sizes[..] {
8096                println!(
8097                    "{codec:?}: {SLICES_PER_FRAME} slices cost {:+.2}% bitrate at a fixed quantizer",
8098                    (many as f64 / one as f64 - 1.0) * 100.0
8099                );
8100            }
8101        }
8102    }
8103
8104    /// On a real GPU: how closely a CBR session holds its target under each VBV policy choice,
8105    /// for H.264 and HEVC at 1080p, on three kinds of content: the alternating gradient frames
8106    /// the other benches use, where every frame is a scene cut; a steady desktop-like sequence
8107    /// with one moving block; and that sequence entered through one cut. Each row varies the VBV
8108    /// (one frame or one and a half), the initial delay (a full buffer or the driver's default),
8109    /// and the slice count (`SLICES_PER_FRAME` or one), and prints the achieved rate, the
8110    /// largest and smallest frame, the luma PSNR of the decoded picture against the source (mean
8111    /// and worst frame), and the wall time per frame. `NVENC_BENCH_KBPS` and `NVENC_BENCH_FPS`
8112    /// move the target from 8000 kbit/s at 60 fps. Ignored by default.
8113    #[test]
8114    #[ignore]
8115    fn gpu_bench_cbr_policy() {
8116        let (w, h) = (1920usize, 1080usize);
8117        let env = |name: &str, default: usize| {
8118            std::env::var(name)
8119                .ok()
8120                .and_then(|v| v.parse().ok())
8121                .unwrap_or(default)
8122        };
8123        let n = env("NVENC_BENCH_FRAMES", 120);
8124        let kbps = env("NVENC_BENCH_KBPS", 8000);
8125        let fps = env("NVENC_BENCH_FPS", 60);
8126        let cuts: Vec<Vec<u8>> = (0..8u8).map(|k| frame(w, h, 10 + 30 * k)).collect();
8127        let steady: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(w, h, i)).collect();
8128        let other = frame(w, h, 200);
8129        let contents: Vec<(&str, Vec<&Vec<u8>>)> = vec![
8130            (
8131                "scene cuts",
8132                std::iter::once(&cuts[0])
8133                    .chain((0..n).map(|i| &cuts[1 + i % 7]))
8134                    .collect(),
8135            ),
8136            (
8137                "steady",
8138                std::iter::once(&steady[0])
8139                    .chain((0..n).map(|i| &steady[1 + i % 15]))
8140                    .collect(),
8141            ),
8142            (
8143                "one cut",
8144                std::iter::once(&other)
8145                    .chain((0..n).map(|i| &steady[i % 16]))
8146                    .collect(),
8147            ),
8148        ];
8149        println!("CBR {kbps} kbit/s at {fps} fps, {n} frames per row");
8150        for codec in [Codec::H264, Codec::H265] {
8151            for (content, seq) in &contents {
8152                for vbv_frames in [1.0f64, 1.5] {
8153                    for full_delay in [true, false] {
8154                        for slices in [SLICES_PER_FRAME, 1] {
8155                            let mut s = settings(w as i32, h as i32, fps as f64);
8156                            s.codec = codec;
8157                            s.video_cbr_mode = true;
8158                            s.video_bitrate_kbps = kbps as i32;
8159                            s.video_vbv_multiplier = vbv_frames;
8160                            let tuning = NvencTuning {
8161                                slices: Some(slices),
8162                                ..NvencTuning::default()
8163                            };
8164                            let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), tuning) {
8165                                Ok(enc) => enc,
8166                                Err(e) => {
8167                                    println!("{codec:?}: {e}");
8168                                    continue;
8169                                }
8170                            };
8171                            if !full_delay {
8172                                enc.encode_config.rcParams.vbvInitialDelay = 0;
8173                                assert_eq!(
8174                                    reconfigure_raw(&mut enc).0,
8175                                    NVENCSTATUS::NV_ENC_SUCCESS
8176                                );
8177                            }
8178                            let label = format!(
8179                                "{codec:?} {content}: VBV {vbv_frames} frame(s), initial delay {}, {slices} slice(s)",
8180                                if full_delay { "full" } else { "default" }
8181                            );
8182                            cbr_row(&label, &mut enc, codec, seq, w, h, fps);
8183                        }
8184                    }
8185                }
8186            }
8187        }
8188    }
8189
8190    /// On a real GPU: which H.264 rate-control setting lets a CBR session run past a small VBV.
8191    /// Holds a 1080p H.264 session at the target `NVENC_BENCH_KBPS` and `NVENC_BENCH_FPS` name
8192    /// on the scene-cut and one-cut sequences at one and one and a half frames of buffer, and
8193    /// changes one setting per row against the production session: single-pass and
8194    /// full-resolution two-pass rate control in place of the quarter-resolution first pass,
8195    /// `strictGOPTarget` off, and a quantizer ceiling of 51. Ignored by default.
8196    #[test]
8197    #[ignore]
8198    fn gpu_bench_cbr_rate_control() {
8199        let (w, h) = (1920usize, 1080usize);
8200        let env = |name: &str, default: usize| {
8201            std::env::var(name)
8202                .ok()
8203                .and_then(|v| v.parse().ok())
8204                .unwrap_or(default)
8205        };
8206        let n = env("NVENC_BENCH_FRAMES", 120);
8207        let kbps = env("NVENC_BENCH_KBPS", 8000);
8208        let fps = env("NVENC_BENCH_FPS", 60);
8209        let cuts: Vec<Vec<u8>> = (0..8u8).map(|k| frame(w, h, 10 + 30 * k)).collect();
8210        let steady: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(w, h, i)).collect();
8211        let other = frame(w, h, 200);
8212        let contents: Vec<(&str, Vec<&Vec<u8>>)> = vec![
8213            (
8214                "scene cuts",
8215                std::iter::once(&cuts[0])
8216                    .chain((0..n).map(|i| &cuts[1 + i % 7]))
8217                    .collect(),
8218            ),
8219            (
8220                "one cut",
8221                std::iter::once(&other)
8222                    .chain((0..n).map(|i| &steady[i % 16]))
8223                    .collect(),
8224            ),
8225        ];
8226        let variants = [
8227            (
8228                "production",
8229                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
8230                true,
8231                0,
8232            ),
8233            (
8234                "single pass",
8235                NV_ENC_MULTI_PASS::NV_ENC_MULTI_PASS_DISABLED,
8236                true,
8237                0,
8238            ),
8239            (
8240                "two-pass full",
8241                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_FULL_RESOLUTION,
8242                true,
8243                0,
8244            ),
8245            (
8246                "strictGOPTarget off",
8247                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
8248                false,
8249                0,
8250            ),
8251            (
8252                "max QP 51",
8253                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
8254                true,
8255                51,
8256            ),
8257        ];
8258        println!("H264 CBR {kbps} kbit/s at {fps} fps, {n} frames per row");
8259        for (content, seq) in &contents {
8260            for vbv_frames in [1.0f64, 1.5] {
8261                for (name, multipass, strict, max_qp) in variants {
8262                    let mut s = settings(w as i32, h as i32, fps as f64);
8263                    s.video_cbr_mode = true;
8264                    s.video_bitrate_kbps = kbps as i32;
8265                    s.video_vbv_multiplier = vbv_frames;
8266                    s.video_max_qp = max_qp;
8267                    let tuning = NvencTuning {
8268                        multipass,
8269                        ..NvencTuning::default()
8270                    };
8271                    let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), tuning) {
8272                        Ok(enc) => enc,
8273                        Err(e) => {
8274                            println!("{name}: {e}");
8275                            continue;
8276                        }
8277                    };
8278                    if !strict {
8279                        enc.encode_config.rcParams.set_strictGOPTarget(0);
8280                        assert_eq!(reconfigure_raw(&mut enc).0, NVENCSTATUS::NV_ENC_SUCCESS);
8281                    }
8282                    cbr_row(
8283                        &format!("{content}: VBV {vbv_frames} frame(s), {name}"),
8284                        &mut enc,
8285                        Codec::H264,
8286                        seq,
8287                        w,
8288                        h,
8289                        fps,
8290                    );
8291                }
8292            }
8293        }
8294    }
8295
8296    /// The reason HEVC pins High tier: with the level pinned rather than autoselected,
8297    /// NVENC validates the requested bitrate against that level's MaxBR, and Main tier's
8298    /// ceiling is low enough that an ordinary 4K desktop session runs into it.
8299    ///
8300    /// The test opens the same CBR HEVC session twice, once at each tier, at a bitrate
8301    /// above the Main-tier ceiling for the pinned level. Tier 1 must open. Tier 0 is
8302    /// reported rather than asserted: what the driver does above the ceiling is the thing
8303    /// being measured, and a future driver that stops rejecting it should not turn this
8304    /// into a red test, it should just make the printed line say so.
8305    #[test]
8306    #[ignore]
8307    fn gpu_hevc_high_tier_opens_above_the_main_tier_ceiling() {
8308        let mut s = settings(3840, 2160, 60.0);
8309        s.codec = Codec::H265;
8310        s.video_cbr_mode = true;
8311        // Above the Main-tier MaxBR of every 5.x level (40 Mbit/s at 5.1, 60 at 5.2).
8312        s.video_bitrate_kbps = 100_000;
8313
8314        let high = host_session(&s);
8315        match &high {
8316            Ok(enc) => unsafe {
8317                let c = enc.encode_config.encodeCodecConfig.hevcConfig;
8318                assert_eq!(c.tier, 1);
8319                println!(
8320                    "HEVC High tier at {} kbps: opened, level {}",
8321                    s.video_bitrate_kbps, c.level,
8322                );
8323            },
8324            Err(e) if e.contains("engine") => {
8325                println!("H265: {e}");
8326                return;
8327            }
8328            Err(e) => panic!("High tier must open above the Main-tier ceiling: {e}"),
8329        }
8330        drop(high);
8331
8332        // The same session at Main tier, through the tuned constructor so only the tier differs.
8333        let main = NvencEncoder::new_tuned(
8334            &s,
8335            ptr::null(),
8336            NvencTuning {
8337                hevc_high_tier: false,
8338                ..NvencTuning::default()
8339            },
8340        );
8341        match main {
8342            Ok(enc) => unsafe {
8343                println!(
8344                    "HEVC Main tier at {} kbps: opened on level {}",
8345                    s.video_bitrate_kbps, enc.encode_config.encodeCodecConfig.hevcConfig.level,
8346                )
8347            },
8348            Err(e) => println!(
8349                "HEVC Main tier at {} kbps: refused, the Main-tier ceiling: {e}",
8350                s.video_bitrate_kbps,
8351            ),
8352        }
8353    }
8354
8355    /// On a real GPU of two or more engines under API 12.1 or later, an AV1 session splits its
8356    /// frames at 1080p, in three strips on three engines, and an HEVC one only while a resize
8357    /// holds it at 4K, and the split frames decode to the picture painted; on one engine
8358    /// nothing splits. Ignored by default.
8359    #[test]
8360    #[ignore]
8361    fn gpu_split_frame_follows_the_codec_and_picture() {
8362        use crate::webcam::decode::VideoDecoder;
8363        use NV_ENC_SPLIT_ENCODE_MODE as Split;
8364        for codec in [Codec::H265, Codec::Av1] {
8365            let mut s = settings(1920, 1080, 60.0);
8366            s.codec = codec;
8367            let mut enc = match host_session(&s) {
8368                Ok(enc) => enc,
8369                Err(e) if e.contains("engine") => {
8370                    println!("{codec:?}: {e}");
8371                    continue;
8372                }
8373                Err(e) => panic!("{codec:?}: {e}"),
8374            };
8375            if !enc.engines.is_some_and(|n| n > 1) || nvenc_cur_ver() < (12, 1) {
8376                println!(
8377                    "{codec:?}: {}, API {:?}",
8378                    enc.split_summary(),
8379                    nvenc_cur_ver()
8380                );
8381                assert_eq!(
8382                    enc.init_params.splitEncodeMode(),
8383                    Split::NV_ENC_SPLIT_AUTO_MODE as u32
8384                );
8385                continue;
8386            }
8387            for (n, (w, h)) in [(1920usize, 1080usize), (3840, 2160), (1920, 1080)]
8388                .into_iter()
8389                .enumerate()
8390            {
8391                if n > 0 {
8392                    s.width = w as i32;
8393                    s.height = h as i32;
8394                    assert!(enc.reconfigure_resolution(&s).expect("resize"));
8395                }
8396                let want = match codec {
8397                    Codec::Av1 if enc.engines == Some(3) => Split::NV_ENC_SPLIT_THREE_FORCED_MODE,
8398                    Codec::Av1 => Split::NV_ENC_SPLIT_AUTO_FORCED_MODE,
8399                    _ if w * h >= 3840 * 2160 => Split::NV_ENC_SPLIT_AUTO_FORCED_MODE,
8400                    _ => Split::NV_ENC_SPLIT_AUTO_MODE,
8401                };
8402                assert_eq!(
8403                    enc.init_params.splitEncodeMode(),
8404                    want as u32,
8405                    "{codec:?} at {w}x{h}: {}",
8406                    enc.split_summary()
8407                );
8408                let mut dec = VideoDecoder::new(codec).expect("decoder");
8409                for i in 0..6usize {
8410                    let f = moving_frame(w, h, i);
8411                    let pkt = enc
8412                        .encode_cpu_packed(&f, w * 4, false, (n * 10 + i) as u64, 25, i == 0)
8413                        .expect("encode");
8414                    let psnr = luma_psnr(&mut dec, &pkt, &f, w, h);
8415                    assert!(psnr > 30.0, "{codec:?} {w}x{h} frame {i}: {psnr:.1} dB");
8416                }
8417                println!("{codec:?} {w}x{h}: {}", enc.split_summary());
8418            }
8419        }
8420    }
8421
8422    /// On a real GPU, a 1080p60 CBR session whose target lies past the ceiling of the level the
8423    /// picture alone would declare (H.264 4.2 at 62.5 Mbit/s, HEVC 4.1 High at 50, AV1's
8424    /// headroom 5.1 at the 26.7 `NVENC_AV1_RATE` holds it to where the driver does) opens on the
8425    /// level the rate raises it to, and a live raise past the ceiling, to 200 Mbit/s, which AV1
8426    /// holds at 106.7, is taken rather than refused. Ignored by default.
8427    #[test]
8428    #[ignore]
8429    fn gpu_cbr_targets_past_the_picture_level_open() {
8430        for (codec, kbps, level) in [
8431            (Codec::H264, 100_000, 50),
8432            (Codec::H265, 60_000, 150),
8433            (Codec::Av1, 30_000, 14),
8434        ] {
8435            let mut s = settings(1920, 1080, 60.0);
8436            s.codec = codec;
8437            s.video_cbr_mode = true;
8438            s.video_bitrate_kbps = kbps;
8439            let mut enc = match host_session(&s) {
8440                Ok(enc) => enc,
8441                Err(e) if e.contains("engine") => {
8442                    println!("{codec:?}: {e}");
8443                    continue;
8444                }
8445                Err(e) => panic!("{codec:?} at {kbps} kbps must open on level {level}: {e}"),
8446            };
8447            let level = if codec == Codec::Av1 && !NVENC_AV1_WEIGHTED.load(Ordering::Relaxed) {
8448                13
8449            } else {
8450                level
8451            };
8452            assert_eq!(
8453                enc.declared_level(),
8454                level,
8455                "{codec:?} level at {kbps} kbps"
8456            );
8457            s.video_bitrate_kbps = 200_000;
8458            assert!(
8459                enc.reconfigure_rate(&s),
8460                "{codec:?} live raise to 200 Mbit/s refused"
8461            );
8462            assert!(
8463                enc.declared_level() > level,
8464                "{codec:?} level did not rise with the target"
8465            );
8466            println!(
8467                "{codec:?}: {kbps} kbps opened on level {level}; 200 Mbit/s live took level {}",
8468                enc.declared_level()
8469            );
8470        }
8471    }
8472
8473    /// On a real GPU with AV1, a 30 Mbit/s session declares 5.1, Annex A's level for the rate,
8474    /// where the driver admits it, and where the driver holds AV1 to `NVENC_AV1_RATE` it opens on
8475    /// 5.2 after the refusal, as does a live raise from 8 Mbit/s, at a key frame; either session
8476    /// then codes a frame. Ignored by default.
8477    #[test]
8478    #[ignore]
8479    fn gpu_av1_declares_the_lowest_level_the_driver_admits() {
8480        let f = frame(1920, 1080, 30);
8481        for raised in [false, true] {
8482            NVENC_AV1_WEIGHTED.store(false, Ordering::Relaxed);
8483            let mut s = settings(1920, 1080, 60.0);
8484            s.codec = Codec::Av1;
8485            s.video_cbr_mode = true;
8486            s.video_bitrate_kbps = if raised { 8_000 } else { 30_000 };
8487            let mut enc = match host_session(&s) {
8488                Ok(enc) => enc,
8489                Err(e) if e.contains("engine") => {
8490                    println!("Av1: {e}");
8491                    return;
8492                }
8493                Err(e) => panic!("Av1 at {} kbps: {e}", s.video_bitrate_kbps),
8494            };
8495            enc.encode_cpu_argb(&f, 1920 * 4, 0, 25, true)
8496                .expect("encode");
8497            if raised {
8498                assert_eq!(enc.declared_level(), 13);
8499                s.video_bitrate_kbps = 30_000;
8500                assert!(
8501                    enc.reconfigure_rate(&s),
8502                    "the raise to 30 Mbit/s was refused"
8503                );
8504            }
8505            let weighted = NVENC_AV1_WEIGHTED.load(Ordering::Relaxed);
8506            assert_eq!(enc.declared_level(), if weighted { 14 } else { 13 });
8507            let pkt = enc
8508                .encode_cpu_argb(&f, 1920 * 4, 1, 25, false)
8509                .expect("encode at the level");
8510            if raised && weighted {
8511                assert!(
8512                    crate::encoders::codec::av1_is_key(&pkt[VIDEO_HEADER_LEN..]),
8513                    "a raised level reaches the decoder at a key frame"
8514                );
8515            }
8516            println!(
8517                "Av1 30 Mbit/s {}: level {} ({})",
8518                if raised { "raised from 8" } else { "opened" },
8519                enc.declared_level(),
8520                if weighted {
8521                    "the driver holds two thirds of Annex A"
8522                } else {
8523                    "Annex A"
8524                }
8525            );
8526        }
8527    }
8528
8529    /// On a real GPU, a live reconfigure costs a key frame only where one is asked for: a plain
8530    /// CBR rate change and a paint-over quantizer change go on predicting, and a target past the
8531    /// level's bitrate ceiling raises it at a key frame. A driver negotiated below 12.2 reads the
8532    /// flags elsewhere, so run it under `PIXELFLUX_NVENC_MAX_API` at each negotiable version too.
8533    /// Ignored by default.
8534    #[test]
8535    #[ignore]
8536    fn gpu_reconfigures_cost_a_key_frame_only_where_asked() {
8537        let f = frame(1920, 1080, 30);
8538        let kind = |enc: &mut NvencEncoder, n: u64, crf: u32| {
8539            enc.encode_cpu_argb(&f, 1920 * 4, n, crf, n == 0)
8540                .expect("encode")[1]
8541                & 0x0f
8542        };
8543        let mut s = settings(1920, 1080, 60.0);
8544        s.video_cbr_mode = true;
8545        s.video_bitrate_kbps = 8000;
8546        let mut enc = host_session(&s).expect("NVENC init");
8547        kind(&mut enc, 0, 25);
8548        for (n, kbps) in [(1, 6000), (2, 8000), (3, 6000)] {
8549            s.video_bitrate_kbps = kbps;
8550            assert!(enc.reconfigure_rate(&s));
8551            assert_eq!(
8552                kind(&mut enc, n, 25),
8553                FRAME_DELTA,
8554                "{:?}: after the change to {kbps} kbit/s",
8555                nvenc_cur_ver()
8556            );
8557        }
8558        s.video_bitrate_kbps = 200_000;
8559        assert!(enc.reconfigure_rate(&s));
8560        assert_eq!(
8561            kind(&mut enc, 4, 25),
8562            FRAME_KEY,
8563            "{:?}: the raised level reaches the stream at a key frame",
8564            nvenc_cur_ver()
8565        );
8566        let mut enc = host_session(&settings(1920, 1080, 60.0)).expect("NVENC init");
8567        for n in 0..6u64 {
8568            let want = if n == 0 { FRAME_KEY } else { FRAME_DELTA };
8569            assert_eq!(
8570                kind(&mut enc, n, if n % 2 == 0 { 25 } else { 18 }),
8571                want,
8572                "{:?}: paint-over frame {n}",
8573                nvenc_cur_ver()
8574            );
8575        }
8576    }
8577
8578    /// On a real GPU, print the device-memory cost of one 1080p session (via `nvidia-smi`),
8579    /// for measuring the reconfigure-headroom overhead. Ignored by default.
8580    #[test]
8581    #[ignore]
8582    fn gpu_vram_probe() {
8583        // The measurement wants the driver's own view of the device, which only nvidia-smi
8584        // gives from outside the session's context; a host or container without it reports that
8585        // rather than failing a test that cannot measure anything. Summed over every GPU it lists,
8586        // so the delta is the session's on whichever device it opened.
8587        fn used_mb() -> Option<i64> {
8588            let out = std::process::Command::new("nvidia-smi")
8589                .args(["--query-gpu=memory.used", "--format=csv,noheader,nounits"])
8590                .output()
8591                .ok()?;
8592            String::from_utf8_lossy(&out.stdout)
8593                .lines()
8594                .map(|l| l.trim().parse::<i64>().ok())
8595                .sum()
8596        }
8597        let s = settings(1920, 1080, 60.0);
8598        let Some(before) = used_mb() else {
8599            println!("VRAM probe skipped: nvidia-smi answered nothing");
8600            return;
8601        };
8602        let mut enc = host_session(&s).expect("init");
8603        let f = frame(1920, 1080, 5);
8604        for i in 0..3u64 {
8605            enc.encode_cpu_argb(&f, 1920 * 4, i, 25, i == 0)
8606                .expect("encode");
8607        }
8608        println!(
8609            "VRAM delta for one 1080p session: {} MiB",
8610            used_mb().unwrap_or(before) - before
8611        );
8612    }
8613
8614    /// On a real GPU, a session that starts taller than the default 2304 headroom (portrait
8615    /// 4K: 2160×4096, within NVENC's 4096 H.264 cap) takes its own size as the `maxEncode` ceiling
8616    /// and encodes at that resolution. Ignored by default.
8617    /// The VUI has to describe what the session actually emits, on a real GPU and through the
8618    /// same caps negotiation a session takes: BT.709 at limited range, which is what the kernel
8619    /// writes for 4:2:0 and what NVENC's own conversion follows for 4:4:4. Primaries and
8620    /// transfer follow the source, which is sRGB desktop pixels and therefore BT.709 too. A
8621    /// client that inverts the wrong matrix, or expands a limited-range frame as full-range,
8622    /// shifts color visibly.
8623    #[test]
8624    #[ignore]
8625    fn gpu_vui_describes_the_conversion() {
8626        for fullcolor in [false, true] {
8627            let mut s = settings(1280, 720, 60.0);
8628            s.video_fullcolor = fullcolor;
8629            let Some(enc) = codec_session(&s) else {
8630                continue;
8631            };
8632            // encodeCodecConfig is a union; a successful init leaves the H.264 arm live.
8633            let h264 = unsafe { &enc.encode_config.encodeCodecConfig.h264Config };
8634            let vui = &h264.h264VUIParameters;
8635            assert_eq!(
8636                vui.colourMatrix as u32,
8637                NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709 as u32,
8638                "fullcolor={fullcolor}"
8639            );
8640            assert_eq!(
8641                vui.colourPrimaries as u32,
8642                NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709 as u32
8643            );
8644            assert_eq!(
8645                vui.transferCharacteristics as u32,
8646                NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709 as u32
8647            );
8648            assert_eq!(vui.videoFullRangeFlag, 0, "fullcolor={fullcolor}");
8649            assert_eq!(h264.chromaFormatIDC, if enc.is_fullcolor() { 3 } else { 1 });
8650        }
8651    }
8652
8653    /// On a real GPU with a render node: the chroma convert reaches the zero-copy path too. A
8654    /// dmabuf painted with alternating single-pixel columns whose pair averages to gray comes
8655    /// back with neutral chroma, whichever way the driver mapped the import — pitch-linear, read
8656    /// in place, or a CUDA array, read through a texture. Neither case copies the RGB.
8657    /// Ignored by default.
8658    #[test]
8659    #[ignore]
8660    fn gpu_dmabuf_chroma_is_sited_at_the_block_center() {
8661        use crate::webcam::decode::{Codec, Decoder as _, VideoDecoder};
8662        let (w, h) = (256u32, 256u32);
8663        let s = settings(w as i32, h as i32, 60.0);
8664        let Some((gbm, mut renderer)) = gpu_render() else {
8665            return;
8666        };
8667        let egl_display = renderer.egl_context().display().get_display_handle().handle;
8668        let (_bo, dmabuf) = column_dmabuf(&gbm, &mut renderer, w, h);
8669        let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
8670        assert!(enc.csc.is_some(), "this GPU took no chroma convert");
8671        let pkt = enc.encode(&dmabuf, 0, 20, true).expect("dmabuf encode");
8672        let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
8673        assert!(
8674            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
8675            "no picture"
8676        );
8677        let v = dec.frame().expect("decoded frame");
8678        let (mut su, mut sv) = (0.0f64, 0.0f64);
8679        let n = (v.chroma_height() * v.chroma_width()) as f64;
8680        for r in 0..v.chroma_height() {
8681            for c in 0..v.chroma_width() {
8682                let i = r * v.uv_stride + c;
8683                su += f64::from(v.u[i]);
8684                sv += f64::from(v.v[i]);
8685            }
8686        }
8687        let (u, cr) = (su / n, sv / n);
8688        println!(
8689            "[chroma-siting] dmabuf mapped as {}: ({u:.1}, {cr:.1})",
8690            mapped_kind(&enc)
8691        );
8692        let off = (u - 128.0).hypot(cr - 128.0);
8693        assert!(
8694            off <= 2.0,
8695            "the zero-copy path leaves chroma {off:.1} off neutral"
8696        );
8697    }
8698
8699    /// A dmabuf painted with alternating single-pixel columns of blue and yellow, whose pair
8700    /// averages to gray.
8701    fn column_dmabuf(
8702        gbm: &gbm::Device<std::fs::File>,
8703        renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
8704        w: u32,
8705        h: u32,
8706    ) -> (gbm::BufferObject<()>, Dmabuf) {
8707        use gbm::{BufferObjectFlags, Format as GbmFormat};
8708        use smithay::backend::renderer::{Bind, Color32F, Frame, Renderer};
8709        use smithay::utils::{Physical, Rectangle, Size, Transform};
8710        let bo = gbm
8711            .create_buffer_object::<()>(w, h, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
8712            .expect("GBM buffer");
8713        let mut dmabuf = crate::create_dmabuf_from_bo(&bo);
8714        {
8715            let mut fb = renderer.bind(&mut dmabuf).expect("bind dmabuf");
8716            let size: Size<i32, Physical> = (w as i32, h as i32).into();
8717            let mut frame = renderer
8718                .render(&mut fb, size, Transform::Normal)
8719                .expect("render");
8720            let full: Rectangle<i32, Physical> = Rectangle::from_size(size);
8721            frame
8722                .clear(Color32F::new(0.0, 0.0, 1.0, 1.0), &[full])
8723                .expect("clear");
8724            for x in (1..w as i32).step_by(2) {
8725                let column: Rectangle<i32, Physical> =
8726                    Rectangle::new((x, 0).into(), (1, h as i32).into());
8727                frame
8728                    .draw_solid(
8729                        column,
8730                        &[Rectangle::from_size(column.size)],
8731                        Color32F::new(1.0, 1.0, 0.0, 1.0),
8732                    )
8733                    .expect("draw column");
8734            }
8735            let sync = frame.finish().expect("finish");
8736            let _ = sync.wait();
8737        }
8738        (bo, dmabuf)
8739    }
8740
8741    /// On a real GPU: a 4:2:0 session sites chroma at the center of the block on both axes.
8742    /// Rows of a color pair that averages to gray, and columns of the same pair, both come back
8743    /// neutral. NVENC's own conversion averages the rows but weights the columns 3:1, which is
8744    /// what `ChromaConvert` replaces; with the convert disabled the column case lands three
8745    /// quarters of the way to the left column's chroma, which is the color subpixel-antialiased
8746    /// text would keep on its glyph edges. That conversion follows the BT.709 the session
8747    /// declares, so the mix is compared against BT.709 here and against BT.601 in
8748    /// `gpu_hardware_conversion_matches_the_declared_matrix`, which declares that instead. A
8749    /// 4:4:4 session subsamples nothing, so it keeps the hardware conversion. Ignored by
8750    /// default.
8751    #[test]
8752    #[ignore]
8753    fn gpu_chroma_is_sited_at_the_block_center() {
8754        use crate::encoders::chroma_siting::{BT709, chroma};
8755        let (w, h) = (256usize, 256usize);
8756        let (blue, yellow) = ([0.0, 0.0, 255.0], [255.0, 255.0, 0.0]);
8757        let st = settings(w as i32, h as i32, 60.0);
8758        let mut enc = host_session(&st).expect("NVENC init");
8759        assert!(enc.csc.is_some(), "this GPU took no chroma convert");
8760        let rows = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, false));
8761        let cols = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, true));
8762        println!("[chroma-siting] convert: rows {rows:?} columns {cols:?}");
8763        for (label, (u, v)) in [("rows", rows), ("columns", cols)] {
8764            let off = (u - 128.0).hypot(v - 128.0);
8765            assert!(off <= 2.0, "{label} come back {off:.1} off neutral chroma");
8766        }
8767        unsafe {
8768            let cu = enc.cuda.clone();
8769            (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
8770            if let Some(csc) = enc.csc.take() {
8771                csc.release(&cu, &enc.nvenc_funcs, enc.encoder_session);
8772            }
8773            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8774        }
8775        let hardware = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, true));
8776        let weighted = chroma([0, 1, 2].map(|i| 0.75 * blue[i] + 0.25 * yellow[i]), BT709);
8777        println!("[chroma-siting] hardware: columns {hardware:?} against 3:1 {weighted:?}");
8778        assert!(
8779            (hardware.0 - weighted.0).hypot(hardware.1 - weighted.1) <= 2.0,
8780            "NVENC's own conversion weights the columns 3:1: {hardware:?} against {weighted:?}"
8781        );
8782
8783        let full = CaptureSettings {
8784            video_fullcolor: true,
8785            ..st
8786        };
8787        if let Some(enc444) = codec_session(&full)
8788            && enc444.is_fullcolor()
8789        {
8790            assert!(
8791                enc444.csc.is_none(),
8792                "a 4:4:4 session has no chroma to site and takes no convert"
8793            );
8794        }
8795    }
8796
8797    /// On a real GPU, in both chroma formats: the color chart decodes back to the painted
8798    /// color when the matrix the VUI declares is inverted, which is what a client does with
8799    /// every frame. 4:2:0 comes through the kernel and 4:4:4 through NVENC's own conversion, so
8800    /// this is what holds both to the declared matrix — the siting check cannot see a wrong one,
8801    /// its tile being neutral whichever matrix converts it. OpenH264, the H.264 decoder the tests
8802    /// carry, refuses a 4:4:4 stream's SPS, so that stream is held to what its SPS declares and
8803    /// the 4:4:4 picture is read back from the HEVC session, which converts the same way. Ignored
8804    /// by default.
8805    #[test]
8806    #[ignore]
8807    fn gpu_chart_decodes_to_the_color_that_was_painted() {
8808        use crate::encoders::chroma_siting::{BT709, chart_bgra, chart_error};
8809        use crate::encoders::sps::{h264_chroma_format_idc, read_color};
8810        use crate::webcam::decode::{Decoder as _, VideoDecoder};
8811        let (w, h) = (256usize, 128usize);
8812        for (codec, fullcolor) in [
8813            (Codec::H264, false),
8814            (Codec::H264, true),
8815            (Codec::H265, true),
8816        ] {
8817            let st = CaptureSettings {
8818                codec,
8819                video_fullcolor: fullcolor,
8820                ..settings(w as i32, h as i32, 60.0)
8821            };
8822            let Some(mut enc) = codec_session(&st) else {
8823                continue;
8824            };
8825            if fullcolor && !enc.is_fullcolor() {
8826                println!("[chart] this GPU carries no 4:4:4 {codec:?}");
8827                continue;
8828            }
8829            assert_eq!(
8830                enc.csc.is_some(),
8831                !fullcolor,
8832                "the convert follows the chroma format"
8833            );
8834            let check = |enc: &mut NvencEncoder, bgra: &[u8], w: usize, frame: u64, what: &str| {
8835                let pkt = enc
8836                    .encode_cpu_packed(bgra, w * 4, false, frame, 20, true)
8837                    .expect("encode");
8838                let stream = &pkt[VIDEO_HEADER_LEN..];
8839                if codec == Codec::H264 && fullcolor {
8840                    assert_eq!(
8841                        h264_chroma_format_idc(stream),
8842                        Some(3),
8843                        "the SPS declares 4:4:4"
8844                    );
8845                    let sps = crate::encoders::codec::annexb_nals(stream)
8846                        .find(|n| n[0] & 0x1f == 7)
8847                        .expect("an SPS");
8848                    assert_eq!(
8849                        read_color(sps).map(|s| (s.matrix, s.full_range)),
8850                        Some((1, false)),
8851                        "the SPS declares BT.709 limited"
8852                    );
8853                    println!("[chart] NVENC H264 4:4:4{what}: the SPS declares 4:4:4 BT.709");
8854                    return;
8855                }
8856                let mut dec = VideoDecoder::new(codec).expect("decoder");
8857                assert!(dec.decode(stream).expect("decode"), "no picture");
8858                let worst = chart_error(&dec.frame().expect("decoded frame"), BT709);
8859                println!(
8860                    "[chart] NVENC {codec:?} 4:4:4 {fullcolor}{what}: worst |dRGB| {worst:.1}"
8861                );
8862                assert!(
8863                    worst <= 8.0,
8864                    "the session paints {worst:.1} off the chart{what}"
8865                );
8866            };
8867            check(&mut enc, &chart_bgra(w, h), w, 0, "");
8868
8869            // A resize rebuilds the convert's surface around the new geometry, so the chart is
8870            // read back again at a size the session was not opened for.
8871            let (w2, h2) = (w + 64, h + 32);
8872            let grown = CaptureSettings {
8873                width: w2 as i32,
8874                height: h2 as i32,
8875                ..st
8876            };
8877            assert!(
8878                enc.reconfigure_resolution(&grown).expect("resize"),
8879                "the resize was taken"
8880            );
8881            assert_eq!(
8882                enc.csc.is_some(),
8883                !fullcolor,
8884                "the convert followed the resize"
8885            );
8886            check(&mut enc, &chart_bgra(w2, h2), w2, 1, " after a resize");
8887        }
8888    }
8889
8890    /// On a real GPU, the path a driver that refuses the PTX takes: a session the kernel is
8891    /// kept out of still encodes, and NVENC's own conversion follows the BT.709 its VUI
8892    /// declares — the same session with the matrix declared as SMPTE170M measures the BT.601
8893    /// mix instead, which is how that dependency was established and why the fallback can
8894    /// declare a matrix at all. Only the siting differs, the columns landing on the 3:1
8895    /// weighting. Ignored by default.
8896    #[test]
8897    #[ignore]
8898    fn gpu_hardware_conversion_matches_the_declared_matrix() {
8899        use crate::encoders::chroma_siting::{BT601, BT709, chart_bgra, chart_error, chroma};
8900        use crate::webcam::decode::{Decoder as _, VideoDecoder};
8901        let (w, h) = (256usize, 256usize);
8902        let (blue, yellow) = ([0.0, 0.0, 255.0], [255.0, 255.0, 0.0]);
8903        let st = settings(w as i32, h as i32, 60.0);
8904        let tuning = NvencTuning {
8905            hardware_csc: true,
8906            ..Default::default()
8907        };
8908        let mut enc = NvencEncoder::new_tuned(&st, ptr::null(), tuning).expect("NVENC init");
8909        assert!(
8910            enc.csc.is_none(),
8911            "the session was asked for NVENC's own conversion"
8912        );
8913
8914        let pkt = enc
8915            .encode_cpu_packed(&chart_bgra(w, h), w * 4, false, 0, 20, true)
8916            .expect("encode");
8917        let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
8918        assert!(
8919            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
8920            "no picture"
8921        );
8922        let worst = chart_error(&dec.frame().expect("decoded frame"), BT709);
8923        println!("[csc] NVENC's own conversion: chart worst |dRGB| {worst:.1}");
8924        assert!(
8925            worst <= 8.0,
8926            "the hardware conversion paints {worst:.1} off the declared matrix"
8927        );
8928
8929        let hardware = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, true));
8930        let mix = [0, 1, 2].map(|i| 0.75 * blue[i] + 0.25 * yellow[i]);
8931        let (weighted_601, weighted_709) = (chroma(mix, BT601), chroma(mix, BT709));
8932        let off = |c: (f64, f64)| (hardware.0 - c.0).hypot(hardware.1 - c.1);
8933        println!(
8934            "[csc] NVENC's own conversion: columns {hardware:?} against the 3:1 mix under \
8935             BT.709 {weighted_709:?} and BT.601 {weighted_601:?}"
8936        );
8937        assert!(
8938            off(weighted_709) <= 2.0 && off(weighted_709) < off(weighted_601),
8939            "the hardware conversion did not follow the declared matrix: {hardware:?}"
8940        );
8941    }
8942
8943    /// On a real GPU: the external-pointer path encodes the caller's buffer where it lies. The
8944    /// session's own staging surface is painted a color the frame does not contain first, so a
8945    /// picture that had been copied through it would decode to that color instead — this is the
8946    /// X11 NvFBC hand-over, without needing NvFBC to reach it. A pitch that cannot cover the
8947    /// session's rows is refused rather than read past. Ignored by default.
8948    #[test]
8949    #[ignore]
8950    fn gpu_external_pointer_is_encoded_where_it_lies() {
8951        use crate::encoders::chroma_siting::{BT709, ycbcr};
8952        use crate::webcam::decode::VideoDecoder;
8953        let (w, h) = (256usize, 128usize);
8954        let st = settings(w as i32, h as i32, 60.0);
8955        let mut enc = host_session(&st).expect("NVENC init");
8956        let paint = [32u8, 192, 64];
8957        let poison = [240u8, 16, 200];
8958        let (external, external_pitch) = unsafe {
8959            let cu = enc.cuda.clone();
8960            (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
8961            let (mut ptr_, mut pitch) = (0 as CUdeviceptr, 0usize);
8962            assert_eq!(
8963                (cu.cuMemAllocPitch_v2)(&mut ptr_, &mut pitch, w * 4, h, 16),
8964                CUresult::CUDA_SUCCESS
8965            );
8966            paint_surface(&cu, ptr_, pitch, w, h, paint);
8967            paint_surface(&cu, enc.input_device_ptr, enc.input_pitch, w, h, poison);
8968            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8969            (ptr_, pitch)
8970        };
8971        let pkt = enc
8972            .encode_cuda_pitch(external, external_pitch, false, 0, 20, true)
8973            .expect("external encode");
8974        let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
8975        let (mean, _) = decoded_means(&mut dec, &pkt, (0, 0, w as i32, h as i32));
8976        let want = ycbcr(paint.map(f64::from), BT709);
8977        println!("[external] decoded {mean:?} against the painted {want:?}");
8978        for i in 0..3 {
8979            assert!(
8980                (mean[i] - want[i]).abs() <= 8.0,
8981                "plane {i} came back {:.1}, not the painted {:.1}: the frame was staged, not read in place",
8982                mean[i],
8983                want[i]
8984            );
8985        }
8986        assert!(
8987            enc.encode_cuda_pitch(external, w * 4 - 4, false, 1, 20, false)
8988                .is_err(),
8989            "a pitch too short for the session's rows must be refused"
8990        );
8991        enc.release_external_input();
8992        unsafe {
8993            let cu = enc.cuda.clone();
8994            (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
8995            (cu.cuMemFree_v2)(external);
8996            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8997        }
8998    }
8999
9000    /// On a real GPU: a host frame asked to be hashed is hashed where it was uploaded, beside its
9001    /// encode, its band hashes taken with the frame: one a band, the same for the same picture,
9002    /// different only in the band a caret changed, and none for a frame that did not ask; and
9003    /// what that costs the encode. Ignored by default.
9004    #[test]
9005    #[ignore]
9006    fn gpu_an_uploaded_frame_is_hashed_beside_its_encode() {
9007        let (w, h, rows) = (1920usize, 1080usize, 32u32);
9008        let mut enc = host_session(&settings(w as i32, h as i32, 60.0)).expect("NVENC init");
9009        let mut picture = frame(w, h, 3);
9010        let encode = |enc: &mut NvencEncoder, picture: &[u8], n: u64, hash: bool| {
9011            if hash {
9012                assert!(enc.hash_next_upload(rows), "the driver takes the band hash");
9013            }
9014            let out = enc.encode_cpu_packed(picture, w * 4, false, n, 23, n == 0);
9015            assert!(out.is_ok_and(|o| !o.is_empty()), "frame {n} encodes");
9016            enc.take_upload_hashes()
9017        };
9018        let first = encode(&mut enc, &picture, 0, true).expect("hashes come with the frame");
9019        assert_eq!(first.len(), h.div_ceil(rows as usize));
9020        assert_eq!(
9021            encode(&mut enc, &picture, 1, true),
9022            Some(first.clone()),
9023            "one picture, one hash"
9024        );
9025        assert_eq!(encode(&mut enc, &picture, 2, false), None, "none unasked");
9026        for y in 200..220 {
9027            for x in 300..302 {
9028                picture[(y * w + x) * 4..][..3].copy_from_slice(&[0, 0, 0]);
9029            }
9030        }
9031        let caret = encode(&mut enc, &picture, 3, true).unwrap();
9032        let changed: Vec<usize> = (0..first.len()).filter(|&i| first[i] != caret[i]).collect();
9033        assert_eq!(changed, vec![200 / rows as usize], "a 2x20 caret");
9034
9035        let mut timed = |hash: bool, from: u64| {
9036            let t = std::time::Instant::now();
9037            for n in from..from + 120 {
9038                picture[(n as usize % h) * w * 4] ^= 0xff;
9039                encode(&mut enc, &picture, n, hash);
9040            }
9041            t.elapsed().as_secs_f64() * 1e3 / 120.0
9042        };
9043        let (plain, hashed) = (timed(false, 4), timed(true, 124));
9044        let (plain2, hashed2) = (timed(false, 244), timed(true, 364));
9045        eprintln!(
9046            "1080p host encode: {:.3} / {:.3} ms plain, {:.3} / {:.3} ms with the device hash",
9047            plain, plain2, hashed, hashed2
9048        );
9049    }
9050
9051    /// On a real GPU: the band hash reads a caret as the one band it is in and two pixels
9052    /// swapped as a change, but neither the alpha byte nor the pitch's padding as one; and what
9053    /// it costs a 1080p frame against the encode of it. Ignored by default.
9054    #[test]
9055    #[ignore]
9056    fn gpu_band_hash_reads_the_bands_that_changed() {
9057        let (w, h, rows) = (1920usize, 1080usize, 32usize);
9058        let pitch = w * 4 + 64;
9059        let mut enc = host_session(&settings(w as i32, h as i32, 60.0)).expect("NVENC init");
9060        let (cu, ctx) = (enc.cuda.clone(), enc.cuda_context);
9061        let picture = frame(w, h, 3);
9062        let mut host = vec![0u8; pitch * h];
9063        for y in 0..h {
9064            host[y * pitch..][..w * 4].copy_from_slice(&picture[y * w * 4..][..w * 4]);
9065        }
9066        let mut dev: CUdeviceptr = 0;
9067        unsafe {
9068            (cu.cuCtxPushCurrent_v2)(ctx);
9069            assert_eq!(
9070                (cu.cuMemAlloc_v2)(&mut dev, pitch * h),
9071                CUresult::CUDA_SUCCESS
9072            );
9073            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
9074        }
9075        let upload = |host: &[u8]| unsafe {
9076            (cu.cuCtxPushCurrent_v2)(ctx);
9077            assert_eq!(
9078                (cu.cuMemcpyHtoD_v2)(dev, host.as_ptr() as *const c_void, host.len()),
9079                CUresult::CUDA_SUCCESS
9080            );
9081            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
9082        };
9083        let changed = |a: &[u64], b: &[u64]| -> Vec<usize> {
9084            (0..a.len()).filter(|&i| a[i] != b[i]).collect()
9085        };
9086        upload(&host);
9087        let hash =
9088            |enc: &mut NvencEncoder| enc.band_hashes(dev, pitch, w as u32, h as u32, rows as u32);
9089        let jit = std::time::Instant::now();
9090        let first = hash(&mut enc).expect("the driver takes the band hash");
9091        let jit = jit.elapsed().as_secs_f64() * 1e3;
9092        assert_eq!(first.len(), h.div_ceil(rows));
9093        assert_eq!(hash(&mut enc).unwrap(), first, "one frame, one hash");
9094
9095        for y in 200..220 {
9096            for x in 300..302 {
9097                host[y * pitch + x * 4..][..3].copy_from_slice(&[0, 0, 0]);
9098            }
9099        }
9100        upload(&host);
9101        let caret = hash(&mut enc).unwrap();
9102        assert_eq!(changed(&first, &caret), vec![200 / rows], "a 2x20 caret");
9103
9104        for y in 0..h {
9105            for x in 0..w {
9106                host[y * pitch + x * 4 + 3] ^= 0x5a;
9107            }
9108            host[y * pitch + w * 4..][..64].fill(0xa5);
9109        }
9110        upload(&host);
9111        assert_eq!(
9112            hash(&mut enc).unwrap(),
9113            caret,
9114            "the alpha byte and the padding are not the picture"
9115        );
9116
9117        let (a, b) = (650 * pitch + 100 * 4, 650 * pitch + 101 * 4);
9118        assert_ne!(host[a..a + 3], host[b..b + 3]);
9119        for i in 0..4 {
9120            host.swap(a + i, b + i);
9121        }
9122        upload(&host);
9123        assert_eq!(
9124            changed(&caret, &hash(&mut enc).unwrap()),
9125            vec![650 / rows],
9126            "two pixels swapped"
9127        );
9128
9129        let mut time = |w: u32, h: u32| -> Vec<f64> {
9130            let mut ms: Vec<f64> = (0..300)
9131                .map(|_| {
9132                    let t = std::time::Instant::now();
9133                    enc.band_hashes(dev, pitch, w, h, rows as u32).unwrap();
9134                    t.elapsed().as_secs_f64() * 1e3
9135                })
9136                .collect();
9137            ms.sort_by(f64::total_cmp);
9138            ms
9139        };
9140        let (hashing, fixed) = (time(w as u32, h as u32), time(64, rows as u32));
9141        let mut encoding: Vec<f64> = (0..120u64)
9142            .map(|i| {
9143                let t = std::time::Instant::now();
9144                enc.encode_cuda_pitch(dev, pitch, false, i, 25, i == 0)
9145                    .expect("encode");
9146                t.elapsed().as_secs_f64() * 1e3
9147            })
9148            .collect();
9149        encoding.sort_by(f64::total_cmp);
9150        println!(
9151            "[band hash] 1080p: {:.3} ms median, {:.3} ms p99 a frame ({:.3} ms of it for a 64x{rows} \
9152             one), the first {jit:.1} ms with the module's load; the encode {:.3} ms median",
9153            hashing[150], hashing[297], fixed[150], encoding[60]
9154        );
9155        enc.release_external_input();
9156        unsafe {
9157            (cu.cuCtxPushCurrent_v2)(ctx);
9158            (cu.cuMemFree_v2)(dev);
9159            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
9160        }
9161    }
9162
9163    /// Test helper: fill a pitched device surface with one BGRA color.
9164    unsafe fn paint_surface(
9165        cuda: &CudaFunctions,
9166        dst: CUdeviceptr,
9167        dst_pitch: usize,
9168        w: usize,
9169        h: usize,
9170        rgb: [u8; 3],
9171    ) {
9172        let mut host = vec![255u8; w * h * 4];
9173        for px in host.as_chunks_mut::<4>().0 {
9174            px[..3].copy_from_slice(&[rgb[2], rgb[1], rgb[0]]);
9175        }
9176        let copy = CUDA_MEMCPY2D {
9177            srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
9178            srcHost: host.as_ptr() as *const c_void,
9179            srcPitch: w * 4,
9180            dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
9181            dstDevice: dst,
9182            dstPitch: dst_pitch,
9183            WidthInBytes: w * 4,
9184            Height: h,
9185            ..Default::default()
9186        };
9187        assert_eq!(
9188            (cuda.cuMemcpy2D_v2)(&copy),
9189            CUresult::CUDA_SUCCESS,
9190            "paint the surface"
9191        );
9192    }
9193
9194    /// A frame of two colors alternating by column or by row, so every 2x2 block averages to
9195    /// the gray between them.
9196    fn color_pair(w: usize, h: usize, a: [f64; 3], b: [f64; 3], by_column: bool) -> Vec<u8> {
9197        let mut f = vec![255u8; w * h * 4];
9198        for y in 0..h {
9199            for x in 0..w {
9200                let p = if (if by_column { x } else { y }) % 2 == 0 {
9201                    a
9202                } else {
9203                    b
9204                };
9205                f[(y * w + x) * 4..][..3].copy_from_slice(&[p[2] as u8, p[1] as u8, p[0] as u8]);
9206            }
9207        }
9208        f
9209    }
9210
9211    /// Encode one key frame of `bgra` and return the mean chroma of the decoded picture.
9212    fn encode_and_measure(enc: &mut NvencEncoder, bgra: &[u8]) -> (f64, f64) {
9213        use crate::webcam::decode::{Decoder as _, VideoDecoder};
9214        let w = enc.width() as usize;
9215        let pkt = enc
9216            .encode_cpu_packed(bgra, w * 4, false, 0, 20, true)
9217            .expect("packed encode");
9218        let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
9219        assert!(
9220            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
9221            "no picture from this access unit"
9222        );
9223        let v = dec.frame().expect("decoded frame");
9224        let (mut su, mut sv) = (0.0f64, 0.0f64);
9225        let n = (v.chroma_height() * v.chroma_width()) as f64;
9226        for r in 0..v.chroma_height() {
9227            for c in 0..v.chroma_width() {
9228                let i = r * v.uv_stride + c;
9229                su += f64::from(v.u[i]);
9230                sv += f64::from(v.v[i]);
9231            }
9232        }
9233        (su / n, sv / n)
9234    }
9235
9236    /// On a real GPU: what the chroma convert costs a frame, against NVENC converting the packed
9237    /// input itself, and how far its luma lands from the host convert's. Prints; ignored by default.
9238    #[test]
9239    #[ignore]
9240    fn gpu_bench_chroma_convert() {
9241        use crate::encoders::software::convert_to_yuv_mt;
9242        for (w, h) in [(1920usize, 1080usize), (3840, 2160)] {
9243            let st = settings(w as i32, h as i32, 60.0);
9244            let bgra = crate::encoders::chroma_siting::bgra(w, h);
9245            let mut kernel = host_session(&st).expect("NVENC init");
9246            assert!(kernel.csc.is_some());
9247            let with = bench_frames(&mut kernel, &bgra);
9248            let mut hardware = host_session(&st).expect("NVENC init");
9249            unsafe {
9250                let cu = hardware.cuda.clone();
9251                (cu.cuCtxPushCurrent_v2)(hardware.cuda_context);
9252                if let Some(csc) = hardware.csc.take() {
9253                    csc.release(&cu, &hardware.nvenc_funcs, hardware.encoder_session);
9254                }
9255                (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
9256            }
9257            let without = bench_frames(&mut hardware, &bgra);
9258            let (cw, ch) = (w / 2, h / 2);
9259            let (mut y, mut u, mut v) = (vec![0u8; w * h], vec![0u8; cw * ch], vec![0u8; cw * ch]);
9260            let mut host = f64::MAX;
9261            for _ in 0..20 {
9262                let t = std::time::Instant::now();
9263                convert_to_yuv_mt(
9264                    &bgra,
9265                    (w * 4) as u32,
9266                    w,
9267                    h,
9268                    false,
9269                    false,
9270                    false,
9271                    false,
9272                    &mut y,
9273                    &mut u,
9274                    &mut v,
9275                    (w, cw),
9276                    8,
9277                )
9278                .expect("host convert");
9279                host = host.min(t.elapsed().as_secs_f64() * 1000.0);
9280            }
9281            println!(
9282                "[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"
9283            );
9284        }
9285    }
9286
9287    /// Mean per-frame `encode_cpu_packed` time over 60 frames, after a warm-up.
9288    fn bench_frames(enc: &mut NvencEncoder, bgra: &[u8]) -> f64 {
9289        let w = enc.width() as usize;
9290        for i in 0..10 {
9291            enc.encode_cpu_packed(bgra, w * 4, false, i, 25, i == 0)
9292                .expect("warm-up");
9293        }
9294        let t = std::time::Instant::now();
9295        for i in 0..60u64 {
9296            enc.encode_cpu_packed(bgra, w * 4, false, 10 + i, 25, false)
9297                .expect("encode");
9298        }
9299        t.elapsed().as_secs_f64() * 1000.0 / 60.0
9300    }
9301
9302    #[test]
9303    #[ignore]
9304    fn gpu_init_above_default_headroom() {
9305        let s = settings(2160, 4096, 30.0);
9306        let mut enc = host_session(&s).expect("NVENC init portrait 4K");
9307        assert_eq!(enc.init_params.maxEncodeWidth, 4096);
9308        assert_eq!(enc.init_params.maxEncodeHeight, 4096);
9309        let f = frame(2160, 4096, 20);
9310        let pkt = enc
9311            .encode_cpu_argb(&f, 2160 * 4, 0, 25, true)
9312            .expect("encode portrait 4K");
9313        assert_eq!(wire_dims(&pkt), (2160, 4096));
9314    }
9315
9316    /// Thread CPU time, for the per-frame CPU cost of an encode path independent of how long
9317    /// the thread waited on the GPU.
9318    fn thread_cpu() -> std::time::Duration {
9319        let mut ts = libc::timespec {
9320            tv_sec: 0,
9321            tv_nsec: 0,
9322        };
9323        unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) };
9324        std::time::Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
9325    }
9326
9327    /// Test helper: run `f` `n` times and report wall and thread-CPU microseconds per call.
9328    fn per_frame(label: &str, n: usize, mut f: impl FnMut(usize)) -> (f64, f64) {
9329        let t0 = std::time::Instant::now();
9330        let c0 = thread_cpu();
9331        for i in 0..n {
9332            f(i);
9333        }
9334        let wall = t0.elapsed().as_secs_f64() * 1e6 / n as f64;
9335        let cpu = (thread_cpu() - c0).as_secs_f64() * 1e6 / n as f64;
9336        println!("{label}: {wall:.0} us wall/frame, {cpu:.0} us cpu/frame ({n} frames)");
9337        (wall, cpu)
9338    }
9339
9340    /// Test helper: the raw GBM device and GLES renderer of the render node named by
9341    /// `PIXELFLUX_TEST_RENDER_NODE`, else the NVIDIA GPU's, brought up exactly as the compositor
9342    /// brings them up, or `None` where GBM allocates nothing there, as `probe_wayland_gpu`
9343    /// finds (NVIDIA's GBM backend arrived in driver 495), which the caller skips.
9344    pub(crate) fn gpu_render() -> Option<(
9345        gbm::Device<std::fs::File>,
9346        smithay::backend::renderer::gles::GlesRenderer,
9347    )> {
9348        let node = std::env::var("PIXELFLUX_TEST_RENDER_NODE")
9349            .ok()
9350            .or_else(|| crate::auto_select_render_node(Some("nvidia")))
9351            .expect("no NVIDIA render node");
9352        let (gbm, renderer) =
9353            crate::gpu_render_init(std::path::Path::new(&node)).expect("GPU render init");
9354        let bo = gbm.create_buffer_object::<()>(
9355            64,
9356            64,
9357            gbm::Format::Argb8888,
9358            gbm::BufferObjectFlags::RENDERING,
9359        );
9360        if let Err(e) = bo {
9361            println!("{node}: GBM allocates nothing ({e})");
9362            return None;
9363        }
9364        Some((gbm, renderer))
9365    }
9366
9367    /// Background and block colors painted into test dmabufs, as `Color32F` components.
9368    pub(crate) const BG: [f32; 3] = [0.1, 0.2, 0.8];
9369    pub(crate) const FG: [f32; 3] = [0.9, 0.3, 0.1];
9370
9371    /// Limited-range Y/Cb/Cr of a painted color, whose components are 0..1.
9372    pub(crate) fn painted_ycbcr(rgb: [f32; 3]) -> [f64; 3] {
9373        use crate::encoders::chroma_siting::{BT709, ycbcr};
9374        ycbcr(rgb.map(|c| f64::from(c) * 255.0), BT709)
9375    }
9376
9377    /// Where the foreground block of a `seed`-painted `w×h` frame sits: a quarter-size block
9378    /// whose origin moves with the seed.
9379    pub(crate) fn block_rect(w: u32, h: u32, seed: u32) -> (i32, i32, i32, i32) {
9380        let x = ((seed * 37) % (w / 2)) as i32 & !1;
9381        let y = ((seed * 53) % (h / 2)) as i32 & !1;
9382        (x, y, (w / 4) as i32 & !1, (h / 4) as i32 & !1)
9383    }
9384
9385    /// Test helper: allocate a `w×h` ARGB8888 render-target dmabuf on `gbm` and paint it with
9386    /// the GLES renderer — `BG` everywhere and an `FG` block at `block_rect(seed)` — waiting for
9387    /// the render to land before returning, as the compositor does before encoding.
9388    pub(crate) fn painted_dmabuf(
9389        gbm: &gbm::Device<std::fs::File>,
9390        renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
9391        w: u32,
9392        h: u32,
9393        seed: u32,
9394    ) -> (gbm::BufferObject<()>, Dmabuf) {
9395        use gbm::{BufferObjectFlags, Format as GbmFormat};
9396        let bo = gbm
9397            .create_buffer_object::<()>(w, h, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
9398            .expect("GBM buffer");
9399        let mut dmabuf = crate::create_dmabuf_from_bo(&bo);
9400        paint_dmabuf(renderer, &mut dmabuf, w, h, seed);
9401        (bo, dmabuf)
9402    }
9403
9404    /// Test helper: paint `dmabuf` (`w×h`) as `painted_dmabuf` does, over what it held.
9405    pub(crate) fn paint_dmabuf(
9406        renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
9407        dmabuf: &mut Dmabuf,
9408        w: u32,
9409        h: u32,
9410        seed: u32,
9411    ) {
9412        use smithay::backend::renderer::{Bind, Color32F, Frame, Renderer};
9413        use smithay::utils::{Physical, Rectangle, Size, Transform};
9414        {
9415            let mut fb = renderer.bind(dmabuf).expect("bind dmabuf");
9416            let size: Size<i32, Physical> = (w as i32, h as i32).into();
9417            let mut frame = renderer
9418                .render(&mut fb, size, Transform::Normal)
9419                .expect("render");
9420            let full: Rectangle<i32, Physical> = Rectangle::from_size(size);
9421            frame
9422                .clear(Color32F::new(BG[0], BG[1], BG[2], 1.0), &[full])
9423                .expect("clear");
9424            let (x, y, bw, bh) = block_rect(w, h, seed);
9425            let block: Rectangle<i32, Physical> = Rectangle::new((x, y).into(), (bw, bh).into());
9426            frame
9427                .draw_solid(
9428                    block,
9429                    &[Rectangle::from_size(block.size)],
9430                    Color32F::new(FG[0], FG[1], FG[2], 1.0),
9431                )
9432                .expect("draw block");
9433            let sync = frame.finish().expect("finish");
9434            let _ = sync.wait();
9435        }
9436    }
9437
9438    /// Test helper: decode one H.264 access unit (the bytes behind the wire header)
9439    /// with the crate's H.264 decoder and return the mean Y/Cb/Cr inside `rect` and outside it.
9440    pub(crate) fn decoded_means(
9441        dec: &mut crate::webcam::decode::VideoDecoder,
9442        pkt: &[u8],
9443        rect: (i32, i32, i32, i32),
9444    ) -> ([f64; 3], [f64; 3]) {
9445        use crate::webcam::decode::Decoder;
9446        assert!(
9447            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
9448            "no picture from this access unit"
9449        );
9450        let v = dec.frame().expect("decoded frame");
9451        let (rx, ry, rw, rh) = rect;
9452        let inside = |x: usize, y: usize| {
9453            x as i32 >= rx && (x as i32) < rx + rw && y as i32 >= ry && (y as i32) < ry + rh
9454        };
9455        let mut acc = [[0f64; 3]; 2];
9456        let mut cnt = [0f64; 2];
9457        for y in 0..v.height {
9458            for x in 0..v.width {
9459                let k = if inside(x, y) { 0 } else { 1 };
9460                acc[k][0] += f64::from(v.y[y * v.y_stride + x]);
9461                acc[k][1] += f64::from(v.u[(y / 2) * v.uv_stride + x / 2]);
9462                acc[k][2] += f64::from(v.v[(y / 2) * v.uv_stride + x / 2]);
9463                cnt[k] += 1.0;
9464            }
9465        }
9466        let mean = |k: usize| [acc[k][0] / cnt[k], acc[k][1] / cnt[k], acc[k][2] / cnt[k]];
9467        (mean(0), mean(1))
9468    }
9469
9470    /// Assert decoded region means sit within `tol` of the limited-range values of the painted
9471    /// colors — a wrong pitch, byte order, or stale buffer lands far outside this.
9472    pub(crate) fn assert_painted(label: &str, block: [f64; 3], bg: [f64; 3], tol: f64) {
9473        let (eb, eg) = (painted_ycbcr(FG), painted_ycbcr(BG));
9474        for i in 0..3 {
9475            assert!(
9476                (block[i] - eb[i]).abs() <= tol,
9477                "{label}: block plane {i} = {:.1}, expected {:.1}",
9478                block[i],
9479                eb[i]
9480            );
9481            assert!(
9482                (bg[i] - eg[i]).abs() <= tol,
9483                "{label}: background plane {i} = {:.1}, expected {:.1}",
9484                bg[i],
9485                eg[i]
9486            );
9487        }
9488    }
9489
9490    /// Whether every cached dmabuf import of `enc` is registered with NVENC in place.
9491    fn all_direct(enc: &NvencEncoder) -> bool {
9492        !enc.dmabuf_cache.is_empty()
9493            && enc
9494                .dmabuf_cache
9495                .values()
9496                .all(|c| matches!(c.input, DmaBufInput::Direct { .. }))
9497    }
9498
9499    /// How the driver mapped the cached dmabuf imports of `enc`, for the test output.
9500    fn mapped_kind(enc: &NvencEncoder) -> &'static str {
9501        match enc
9502            .dmabuf_cache
9503            .values()
9504            .next()
9505            .map(|c| c.egl_frame.frame_type)
9506        {
9507            Some(CU_EGL_FRAME_TYPE_PITCH) => "pitch-linear",
9508            Some(CU_EGL_FRAME_TYPE_ARRAY) => "a CUDA array",
9509            _ => "an unknown frame kind",
9510        }
9511    }
9512
9513    /// On a real GPU with a render node: two GLES-painted dmabufs encode through the dmabuf path
9514    /// and decode to the painted colors at the painted positions, first with the direct
9515    /// registration enabled (in place when the driver maps the import pitch-linear, otherwise the
9516    /// copy arm) and then with it disabled — the two streams must agree, and the decoded content
9517    /// of both must match the paint. Prints which path the driver gave. Ignored by default; needs
9518    /// a render node of the NVIDIA GPU.
9519    #[test]
9520    #[ignore]
9521    fn gpu_dmabuf_direct_and_copy_paths_decode_to_the_paint() {
9522        use crate::webcam::decode::{Codec, VideoDecoder};
9523        let (w, h) = (1920u32, 1080u32);
9524        let s = settings(w as i32, h as i32, 60.0);
9525        let Some((gbm, mut renderer)) = gpu_render() else {
9526            return;
9527        };
9528        let egl_display = renderer.egl_context().display().get_display_handle().handle;
9529        let bufs: Vec<_> = (1..=2u32)
9530            .map(|seed| (seed, painted_dmabuf(&gbm, &mut renderer, w, h, seed)))
9531            .collect();
9532        let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
9533
9534        let run = |enc: &mut NvencEncoder, label: &str| -> Vec<Vec<u8>> {
9535            let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
9536            let mut out = Vec::new();
9537            for i in 0..6u64 {
9538                let (seed, (_, dmabuf)) = &bufs[(i % 2) as usize];
9539                let pkt = enc.encode(dmabuf, i, 25, i == 0).expect("dmabuf encode");
9540                assert_eq!(wire_dims(&pkt), (w as u16, h as u16));
9541                let (block, bg) = decoded_means(&mut dec, &pkt, block_rect(w, h, *seed));
9542                assert_painted(&format!("{label} frame {i}"), block, bg, 6.0);
9543                out.push(pkt[VIDEO_HEADER_LEN..].to_vec());
9544            }
9545            out
9546        };
9547
9548        // Painting the session's own staging surface a color neither frame contains proves the
9549        // direct arm never passes through it: a copied frame would decode to this instead.
9550        if all_direct(&enc) {
9551            unsafe {
9552                let cu = enc.cuda.clone();
9553                (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
9554                paint_surface(
9555                    &cu,
9556                    enc.input_device_ptr,
9557                    enc.input_pitch,
9558                    w as usize,
9559                    h as usize,
9560                    [240, 16, 200],
9561                );
9562                (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
9563            }
9564        }
9565        let direct = run(&mut enc, "direct");
9566        println!(
9567            "driver mapped the dmabuf {}: {}",
9568            mapped_kind(&enc),
9569            if all_direct(&enc) {
9570                "registered in place"
9571            } else {
9572                "direct registration unavailable, copy arm used"
9573            }
9574        );
9575
9576        // The copy arm is driven from a session that never registered an import in place: a
9577        // same-size reconfigure keeps the geometry, and so keeps the imports it cached, since
9578        // only a real resolution change drains them.
9579        let mut copy_only = NvencEncoder::new(&s, egl_display).expect("NVENC init");
9580        copy_only.direct_dmabuf = false;
9581        let copied = run(&mut copy_only, "copy");
9582        assert!(!all_direct(&copy_only));
9583        let identical = direct.iter().zip(&copied).all(|(a, b)| a == b);
9584        println!(
9585            "direct vs copy streams: {} ({} vs {} bytes)",
9586            if identical {
9587                "byte-identical"
9588            } else {
9589                "differ"
9590            },
9591            direct.iter().map(Vec::len).sum::<usize>(),
9592            copied.iter().map(Vec::len).sum::<usize>()
9593        );
9594        if let Ok(dir) = std::env::var("NVENC_TEST_DUMP_DIR") {
9595            std::fs::write(format!("{dir}/dmabuf-direct.h264"), direct.concat()).unwrap();
9596            std::fs::write(format!("{dir}/dmabuf-copy.h264"), copied.concat()).unwrap();
9597        }
9598    }
9599
9600    /// On a real GPU with a render node: a 10-bit HEVC session takes dmabufs through the convert
9601    /// at either chroma, registered in place and through the copy arm, and the two arms code
9602    /// the same stream. Skips a device that lists no 10-bit HEVC. `NVENC_TEST_DUMP_DIR` keeps
9603    /// the streams for a decoder that takes them. Ignored by default.
9604    #[test]
9605    #[ignore]
9606    fn gpu_dmabuf_ten_bit_arms_agree() {
9607        let (w, h) = (1920u32, 1080u32);
9608        let Some((gbm, mut renderer)) = gpu_render() else {
9609            return;
9610        };
9611        let egl_display = renderer.egl_context().display().get_display_handle().handle;
9612        let bufs: Vec<_> = (1..=2u32)
9613            .map(|seed| painted_dmabuf(&gbm, &mut renderer, w, h, seed))
9614            .collect();
9615        for fullcolor in [false, true] {
9616            let mut s = settings(w as i32, h as i32, 60.0);
9617            s.codec = Codec::H265;
9618            s.video_fullcolor = fullcolor;
9619            s.video_bit_depth = 10;
9620            let mut streams = Vec::new();
9621            for direct in [true, false] {
9622                let mut enc = match NvencEncoder::new(&s, egl_display) {
9623                    Ok(enc) => enc,
9624                    Err(e) if e.contains("engine") => {
9625                        println!("{e}");
9626                        return;
9627                    }
9628                    Err(e) => panic!("NVENC init: {e}"),
9629                };
9630                if enc.bit_depth() != 10 {
9631                    println!("this device codes no 10-bit HEVC");
9632                    return;
9633                }
9634                enc.direct_dmabuf = direct;
9635                let stream: Vec<u8> = (0..6u64)
9636                    .flat_map(|i| {
9637                        let (_, dmabuf) = &bufs[(i % 2) as usize];
9638                        let pkt = enc.encode(dmabuf, i, 25, i == 0).expect("dmabuf encode");
9639                        pkt[VIDEO_HEADER_LEN..].to_vec()
9640                    })
9641                    .collect();
9642                println!(
9643                    "4:4:4 {fullcolor} direct {direct}: mapped {}, {} bytes",
9644                    mapped_kind(&enc),
9645                    stream.len()
9646                );
9647                if let Ok(dir) = std::env::var("NVENC_TEST_DUMP_DIR") {
9648                    std::fs::write(
9649                        format!("{dir}/dmabuf-10bit-{fullcolor}-{direct}.h265"),
9650                        &stream,
9651                    )
9652                    .unwrap();
9653                }
9654                streams.push(stream);
9655            }
9656            assert_eq!(streams[0], streams[1], "4:4:4 {fullcolor}: the arms differ");
9657        }
9658    }
9659
9660    /// On a real GPU: a host frame handed over as BGRA (`rgba_input = false`) and the same image
9661    /// handed over as RGBA bytes (`rgba_input = true`) both decode to the painted colors — the
9662    /// input surface is re-registered in the other byte order in place — and the session keeps
9663    /// encoding across the switch. Ignored by default.
9664    #[test]
9665    #[ignore]
9666    fn gpu_packed_bgra_and_rgba_inputs_agree() {
9667        use crate::webcam::decode::{Codec, VideoDecoder};
9668        let (w, h) = (1280u32, 720u32);
9669        let s = settings(w as i32, h as i32, 60.0);
9670        let rect = block_rect(w, h, 3);
9671        let paint = |rgba: bool| -> Vec<u8> {
9672            let to_u8 = |c: f32| (c * 255.0).round() as u8;
9673            let mut f = vec![0u8; (w * h * 4) as usize];
9674            for y in 0..h as i32 {
9675                for x in 0..w as i32 {
9676                    let inside =
9677                        x >= rect.0 && x < rect.0 + rect.2 && y >= rect.1 && y < rect.1 + rect.3;
9678                    let c = if inside { FG } else { BG };
9679                    let px = &mut f[((y as u32 * w + x as u32) * 4) as usize..][..4];
9680                    let (r, g, b) = (to_u8(c[0]), to_u8(c[1]), to_u8(c[2]));
9681                    if rgba {
9682                        px.copy_from_slice(&[r, g, b, 255]);
9683                    } else {
9684                        px.copy_from_slice(&[b, g, r, 255]);
9685                    }
9686                }
9687            }
9688            f
9689        };
9690        let bgra = paint(false);
9691        let rgba = paint(true);
9692        let mut enc = host_session(&s).expect("NVENC init");
9693        let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
9694        let stride = (w * 4) as usize;
9695        // Byte order reaches the chroma convert as its own argument and the hardware conversion
9696        // as the packed surface's registered format, so both mechanisms are driven here: the
9697        // second pass is the path a GPU whose driver refuses the kernel takes.
9698        let check_stage = |enc: &mut NvencEncoder, dec: &mut VideoDecoder, tag: &str| {
9699            for (i, (buf, is_rgba)) in
9700                [(&bgra, false), (&rgba, true), (&bgra, false), (&rgba, true)]
9701                    .into_iter()
9702                    .enumerate()
9703            {
9704                let pkt = enc
9705                    .encode_cpu_packed(buf, stride, is_rgba, i as u64, 25, i == 0)
9706                    .expect("packed encode");
9707                if enc.csc.is_none() {
9708                    assert_eq!(
9709                        enc.input_format,
9710                        if is_rgba {
9711                            NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
9712                        } else {
9713                            NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
9714                        }
9715                    );
9716                }
9717                let (block, bg) = decoded_means(dec, &pkt, rect);
9718                assert_painted(&format!("{tag} frame {i} rgba={is_rgba}"), block, bg, 6.0);
9719            }
9720        };
9721        check_stage(&mut enc, &mut dec, "convert");
9722        unsafe {
9723            let cu = enc.cuda.clone();
9724            (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
9725            if let Some(csc) = enc.csc.take() {
9726                csc.release(&cu, &enc.nvenc_funcs, enc.encoder_session);
9727            }
9728            (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
9729        }
9730        check_stage(&mut enc, &mut dec, "hardware");
9731    }
9732
9733    /// On a real GPU: per-frame wall time of every NVENC preset, rate-control pass mode, and
9734    /// adaptive quantization, 1080p CBR on both codecs. Prints all; ignored by default.
9735    #[test]
9736    #[ignore]
9737    fn gpu_bench_tuning() {
9738        let (w, h) = (1920u32, 1080u32);
9739        let n: usize = std::env::var("NVENC_BENCH_FRAMES")
9740            .ok()
9741            .and_then(|v| v.parse().ok())
9742            .unwrap_or(240);
9743        let mut s = settings(w as i32, h as i32, 60.0);
9744        s.video_cbr_mode = true;
9745        s.video_bitrate_kbps = 8000;
9746        let frames: Vec<Vec<u8>> = (0..8u8)
9747            .map(|k| frame(w as usize, h as usize, 10 + 30 * k))
9748            .collect();
9749        let stride = (w * 4) as usize;
9750        let presets = [
9751            ("P1", NV_ENC_PRESET_P1_GUID),
9752            ("P2", NV_ENC_PRESET_P2_GUID),
9753            ("P3", NV_ENC_PRESET_P3_GUID),
9754            ("P4", NV_ENC_PRESET_P4_GUID),
9755            ("P5", NV_ENC_PRESET_P5_GUID),
9756            ("P6", NV_ENC_PRESET_P6_GUID),
9757            ("P7", NV_ENC_PRESET_P7_GUID),
9758        ];
9759        let passes = [
9760            ("single pass", NV_ENC_MULTI_PASS::NV_ENC_MULTI_PASS_DISABLED),
9761            (
9762                "two-pass quarter",
9763                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
9764            ),
9765            (
9766                "two-pass full",
9767                NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_FULL_RESOLUTION,
9768            ),
9769        ];
9770        let mut cases: Vec<(String, NvencTuning)> = Vec::new();
9771        for (name, preset) in presets {
9772            cases.push((
9773                format!("{name} two-pass quarter"),
9774                NvencTuning {
9775                    preset,
9776                    ..NvencTuning::default()
9777                },
9778            ));
9779        }
9780        for (name, multipass) in passes {
9781            cases.push((
9782                format!("P4 {name}"),
9783                NvencTuning {
9784                    multipass,
9785                    ..NvencTuning::default()
9786                },
9787            ));
9788        }
9789        cases.push((
9790            "P4 two-pass quarter spatial AQ".into(),
9791            NvencTuning {
9792                spatial_aq: true,
9793                ..NvencTuning::default()
9794            },
9795        ));
9796        cases.push((
9797            "P4 two-pass quarter temporal AQ".into(),
9798            NvencTuning {
9799                temporal_aq: true,
9800                ..NvencTuning::default()
9801            },
9802        ));
9803        for codec in [Codec::H264, Codec::H265] {
9804            s.codec = codec;
9805            for (label, tuning) in &cases {
9806                let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), *tuning) {
9807                    Ok(enc) => enc,
9808                    Err(e) => {
9809                        println!("{codec:?} {label}: {e}");
9810                        continue;
9811                    }
9812                };
9813                enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true)
9814                    .expect("warm-up");
9815                let mut bytes = 0usize;
9816                per_frame(&format!("{codec:?} {label}"), n, |i| {
9817                    bytes += enc
9818                        .encode_cpu_packed(&frames[i % 8], stride, false, 1 + i as u64, 25, false)
9819                        .expect("encode")
9820                        .len();
9821                });
9822                println!("    {} kbps", bytes * 8 * 60 / n / 1000);
9823            }
9824        }
9825    }
9826
9827    /// On a real GPU: the production tuning against the presets P1 and P2, one L0 reference, and
9828    /// each split-frame mode, on real content, per codec at 1080p, 1440p, and 2160p at CBR 8, 14,
9829    /// and 30 Mbit/s: a page of text scrolling four rows a frame and a texture panned six and
9830    /// three pixels. Per row the encode time at the median and the 99th percentile, the bitrate
9831    /// against the target, and the luma PSNR. `NVENC_BENCH_HEIGHTS` (1080,1440,2160) and
9832    /// `NVENC_BENCH_CODECS` (H264,H265,Av1) pick rows, `NVENC_BENCH_FRAMES` their length. Prints
9833    /// all; ignored by default.
9834    #[test]
9835    #[ignore]
9836    fn gpu_bench_tuning_on_content() {
9837        use crate::webcam::decode::VideoDecoder;
9838        let n: usize = std::env::var("NVENC_BENCH_FRAMES")
9839            .ok()
9840            .and_then(|v| v.parse().ok())
9841            .unwrap_or(90);
9842        let glyph = |c: u32, gx: usize, gy: usize| {
9843            let s = c.wrapping_mul(2654435761) >> 7;
9844            (s & 1 == 1 && gx == 1)
9845                || (s & 2 == 2 && gx == 5)
9846                || (s & 4 == 4 && gy == 1)
9847                || (s & 8 == 8 && gy == 6)
9848                || (s & 16 == 16 && gy == 11)
9849                || (s & 32 == 32 && gx * 2 == gy)
9850        };
9851        let only = |var: &str, item: String| {
9852            std::env::var(var).map_or(true, |v| v.split(',').any(|x| x == item))
9853        };
9854        use NV_ENC_SPLIT_ENCODE_MODE as Split;
9855        for (w, h, kbps) in [
9856            (1920usize, 1080usize, 8000),
9857            (2560, 1440, 14000),
9858            (3840, 2160, 30000),
9859        ] {
9860            if !only("NVENC_BENCH_HEIGHTS", h.to_string()) {
9861                continue;
9862            }
9863            let mut page = vec![255u8; w * 2 * h * 4];
9864            for y in 0..2 * h {
9865                let (cy, gy) = (y / 18, y % 18);
9866                if cy % 9 == 8 || gy >= 13 {
9867                    continue;
9868                }
9869                for x in 0..w {
9870                    let (cx, gx) = (x / 10, x % 10);
9871                    let c = (cx as u32).wrapping_mul(31).wrapping_add(cy as u32 * 977);
9872                    if gx < 7 && (cx + cy * 3) % 13 != 0 && glyph(c, gx, gy) {
9873                        page[(y * w + x) * 4..(y * w + x) * 4 + 3].copy_from_slice(&[40, 30, 20]);
9874                    }
9875                }
9876            }
9877            let (tw, th) = (w * 2, h * 2);
9878            let mut tex = vec![0u8; tw * th * 4];
9879            for y in 0..th {
9880                for x in 0..tw {
9881                    let (fx, fy) = (x as f32, y as f32);
9882                    let v = 128.0
9883                        + 50.0 * (fx / 97.0).sin() * (fy / 61.0).cos()
9884                        + 30.0 * (fx / 13.0 + fy / 29.0).sin()
9885                        + ((x as u32).wrapping_mul(2654435761) ^ (y as u32).wrapping_mul(40503))
9886                            as f32
9887                            / u32::MAX as f32
9888                            * 24.0;
9889                    let i = (y * tw + x) * 4;
9890                    tex[i] = v as u8;
9891                    tex[i + 1] = (v * 0.8) as u8;
9892                    tex[i + 2] = (255.0 - v) as u8;
9893                }
9894            }
9895            let mut bufs = [vec![0u8; w * h * 4], vec![0u8; w * h * 4]];
9896            let draw = |scene: usize, t: usize, out: &mut Vec<u8>| {
9897                for y in 0..h {
9898                    let row = &mut out[y * w * 4..(y + 1) * w * 4];
9899                    if scene == 0 {
9900                        let src = (y + t * 4) % (2 * h);
9901                        row.copy_from_slice(&page[src * w * 4..(src + 1) * w * 4]);
9902                    } else {
9903                        let (ox, oy) = ((t * 6) % w, (y + t * 3) % h);
9904                        row.copy_from_slice(&tex[(oy * tw + ox) * 4..(oy * tw + ox + w) * 4]);
9905                    }
9906                }
9907            };
9908            for codec in [Codec::H264, Codec::H265, Codec::Av1] {
9909                if !only("NVENC_BENCH_CODECS", format!("{codec:?}")) {
9910                    continue;
9911                }
9912                let mut rows: Vec<(String, NvencTuning)> = vec![
9913                    ("production".into(), NvencTuning::default()),
9914                    (
9915                        "P1".into(),
9916                        NvencTuning {
9917                            preset: NV_ENC_PRESET_P1_GUID,
9918                            ..NvencTuning::default()
9919                        },
9920                    ),
9921                    (
9922                        "P2".into(),
9923                        NvencTuning {
9924                            preset: NV_ENC_PRESET_P2_GUID,
9925                            ..NvencTuning::default()
9926                        },
9927                    ),
9928                    (
9929                        "P3 L0=1".into(),
9930                        NvencTuning {
9931                            ref_l0: Some(NV_ENC_NUM_REF_FRAMES::NV_ENC_NUM_REF_FRAMES_1),
9932                            ..NvencTuning::default()
9933                        },
9934                    ),
9935                ];
9936                if codec != Codec::H264 {
9937                    for (name, split) in [
9938                        ("split off", Split::NV_ENC_SPLIT_DISABLE_MODE),
9939                        ("split driver's", Split::NV_ENC_SPLIT_AUTO_MODE),
9940                        ("split forced", Split::NV_ENC_SPLIT_AUTO_FORCED_MODE),
9941                        ("split two", Split::NV_ENC_SPLIT_TWO_FORCED_MODE),
9942                        ("split three", Split::NV_ENC_SPLIT_THREE_FORCED_MODE),
9943                    ] {
9944                        rows.push((
9945                            format!("P3 {name}"),
9946                            NvencTuning {
9947                                split: Some(split),
9948                                ..NvencTuning::default()
9949                            },
9950                        ));
9951                    }
9952                }
9953                for (scene, scene_name) in [(0usize, "text"), (1, "pan")] {
9954                    for (label, tuning) in &rows {
9955                        let mut s = settings(w as i32, h as i32, 60.0);
9956                        s.codec = codec;
9957                        s.video_cbr_mode = true;
9958                        s.video_bitrate_kbps = kbps;
9959                        let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), *tuning) {
9960                            Ok(enc) => enc,
9961                            Err(e) => {
9962                                println!("{codec:?} {w}x{h} {label}: {e}");
9963                                continue;
9964                            }
9965                        };
9966                        let engines = unsafe {
9967                            query_cap(
9968                                &enc.nvenc_funcs,
9969                                enc.encoder_session,
9970                                codec_guid(codec).unwrap(),
9971                                NV_ENC_CAPS::NV_ENC_CAPS_NUM_ENCODER_ENGINES,
9972                            )
9973                        };
9974                        let mut dec = VideoDecoder::new(codec).unwrap();
9975                        let (mut times, mut bytes, mut psnr) =
9976                            (Vec::with_capacity(n), 0usize, 0f64);
9977                        for t in 0..n + 5 {
9978                            let k = t % 2;
9979                            draw(scene, t, &mut bufs[k]);
9980                            let t0 = std::time::Instant::now();
9981                            let out = enc
9982                                .encode_cpu_packed(&bufs[k], w * 4, false, t as u64, 25, t == 0)
9983                                .expect("encode");
9984                            let ms = t0.elapsed().as_secs_f64() * 1e3;
9985                            let p = luma_psnr(&mut dec, &out, &bufs[k], w, h);
9986                            if t >= 5 {
9987                                times.push(ms);
9988                                bytes += out.len();
9989                                psnr += p;
9990                            }
9991                        }
9992                        times.sort_by(|a, b| a.total_cmp(b));
9993                        println!(
9994                            "TUNING {codec:?} {w}x{h} {scene_name} {label} (engines {engines:?}): encode {:.2} ms p50 {:.2} p99, {:.2} Mbit/s against {:.1}, PSNR {:.2} dB",
9995                            times[n / 2],
9996                            times[(n * 99) / 100],
9997                            bytes as f64 * 8.0 * 60.0 / n as f64 / 1e6,
9998                            f64::from(kbps) / 1000.0,
9999                            psnr / n as f64
10000                        );
10001                    }
10002                }
10003            }
10004        }
10005    }
10006
10007    /// On a real GPU with a render node: per-frame wall and CPU cost of the dmabuf path with the
10008    /// in-place registration (when the driver maps the import pitch-linear) against the per-frame
10009    /// copy, 1080p, two painted buffers alternating. Prints both; ignored by default.
10010    #[test]
10011    #[ignore]
10012    fn gpu_bench_dmabuf_paths() {
10013        let (w, h) = (1920u32, 1080u32);
10014        let n: usize = std::env::var("NVENC_BENCH_FRAMES")
10015            .ok()
10016            .and_then(|v| v.parse().ok())
10017            .unwrap_or(300);
10018        let s = settings(w as i32, h as i32, 60.0);
10019        let Some((gbm, mut renderer)) = gpu_render() else {
10020            return;
10021        };
10022        let egl_display = renderer.egl_context().display().get_display_handle().handle;
10023        let bufs: Vec<_> = (1..=2u32)
10024            .map(|seed| painted_dmabuf(&gbm, &mut renderer, w, h, seed).1)
10025            .collect();
10026        let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
10027        for pass in 0..2 {
10028            enc.direct_dmabuf = pass == 0;
10029            enc.reconfigure_resolution(&s)
10030                .expect("reconfigure drains the import cache");
10031            enc.encode(&bufs[0], 0, 25, true).expect("warm-up");
10032            enc.encode(&bufs[1], 1, 25, false).expect("warm-up");
10033            let label = if all_direct(&enc) {
10034                format!("dmabuf registered in place ({})", mapped_kind(&enc))
10035            } else if pass == 0 {
10036                format!(
10037                    "dmabuf copy arm (direct registration unavailable, mapped {})",
10038                    mapped_kind(&enc)
10039                )
10040            } else {
10041                format!("dmabuf per-frame copy (mapped {})", mapped_kind(&enc))
10042            };
10043            per_frame(&label, n, |i| {
10044                enc.encode(&bufs[i % 2], 2 + i as u64, 25, false)
10045                    .expect("encode");
10046            });
10047        }
10048    }
10049
10050    /// On a real GPU: per-frame wall and CPU cost of the readback→NVENC hand-over, 1080p host
10051    /// frames: the packed upload with the hardware CSC (BGRA and RGBA), and that same packed
10052    /// upload as a synchronous copy. Prints all; ignored by default.
10053    #[test]
10054    #[ignore]
10055    fn gpu_bench_readback_upload() {
10056        let (w, h) = (1920u32, 1080u32);
10057        let n: usize = std::env::var("NVENC_BENCH_FRAMES")
10058            .ok()
10059            .and_then(|v| v.parse().ok())
10060            .unwrap_or(300);
10061        let s = settings(w as i32, h as i32, 60.0);
10062        let frames: Vec<Vec<u8>> = (0..4u8)
10063            .map(|k| frame(w as usize, h as usize, 10 + 40 * k))
10064            .collect();
10065        let stride = (w * 4) as usize;
10066        let mut enc = host_session(&s).expect("NVENC init");
10067
10068        enc.reconfigure_resolution(&s).expect("reconfigure");
10069        enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true)
10070            .expect("warm-up");
10071        per_frame(
10072            "encode_cpu_packed BGRA (pinned, async upload, hardware CSC)",
10073            n,
10074            |i| {
10075                enc.encode_cpu_packed(&frames[i % 4], stride, false, 1 + i as u64, 25, false)
10076                    .expect("packed");
10077            },
10078        );
10079
10080        enc.reconfigure_resolution(&s).expect("reconfigure");
10081        enc.encode_cpu_packed(&frames[0], stride, true, 0, 25, true)
10082            .expect("warm-up");
10083        per_frame(
10084            "encode_cpu_packed RGBA (pinned, async upload, hardware CSC)",
10085            n,
10086            |i| {
10087                enc.encode_cpu_packed(&frames[i % 4], stride, true, 1 + i as u64, 25, false)
10088                    .expect("packed");
10089            },
10090        );
10091
10092        enc.reconfigure_resolution(&s).expect("reconfigure");
10093        enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true)
10094            .expect("warm-up");
10095        per_frame(
10096            "packed BGRA with a synchronous cuMemcpy2D upload",
10097            n,
10098            |i| unsafe {
10099                let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
10100                let src = &frames[i % 4];
10101                enc.pin_host_source(src.as_ptr() as usize, src.len());
10102                let copy = CUDA_MEMCPY2D {
10103                    srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
10104                    srcHost: src.as_ptr() as *const c_void,
10105                    srcPitch: stride,
10106                    dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
10107                    dstDevice: enc.input_device_ptr,
10108                    dstPitch: enc.input_pitch,
10109                    WidthInBytes: stride,
10110                    Height: h as usize,
10111                    ..Default::default()
10112                };
10113                assert_eq!((enc.cuda.cuMemcpy2D_v2)(&copy), CUresult::CUDA_SUCCESS);
10114                enc.submit_frame(
10115                    enc.mapped_input_buffer,
10116                    enc.input_format,
10117                    1 + i as u64,
10118                    false,
10119                )
10120                .expect("submit");
10121                (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
10122            },
10123        );
10124    }
10125
10126    /// How a bitstream lock waits for the encode: blocking in the driver, spinning on
10127    /// `doNotWait` locks, or sleeping between them.
10128    #[derive(Clone, Copy, Debug)]
10129    enum LockWait {
10130        Block,
10131        Spin,
10132        Sleep(std::time::Duration),
10133    }
10134
10135    /// On a real GPU, the time from `nvEncEncodePicture` to a locked bitstream at 1080p under
10136    /// each lock strategy, with the CPU each spends and how many not-ready answers the driver
10137    /// gives: the spin marks when the encode really completes, the blocking lock shows what the
10138    /// driver's own wait adds, and each sleep interval what its quantization adds. Ignored by
10139    /// default.
10140    #[test]
10141    #[ignore]
10142    fn gpu_bench_lock_strategies() {
10143        let s = settings(1920, 1080, 60.0);
10144        let mut enc = host_session(&s).expect("NVENC init");
10145        let frames = [frame(1920, 1080, 10), frame(1920, 1080, 90)];
10146        let n = 240usize;
10147        let strategies = [
10148            LockWait::Spin,
10149            LockWait::Block,
10150            LockWait::Sleep(std::time::Duration::from_micros(50)),
10151            LockWait::Sleep(std::time::Duration::from_micros(200)),
10152            LockWait::Sleep(std::time::Duration::from_micros(1000)),
10153        ];
10154        for pass in 0..2 {
10155            for &strategy in &strategies {
10156                let mut waits: Vec<f64> = Vec::with_capacity(n);
10157                let mut busy_total = 0u64;
10158                let c0 = thread_cpu();
10159                for i in 0..n {
10160                    let (wait, busy) =
10161                        unsafe { lock_round_trip(&mut enc, &frames[i % 2], i as u64, strategy) };
10162                    waits.push(wait);
10163                    busy_total += busy;
10164                }
10165                let cpu_us = (thread_cpu() - c0).as_secs_f64() * 1e6 / n as f64;
10166                waits.sort_by(|a, b| a.partial_cmp(b).unwrap());
10167                let mean = waits.iter().sum::<f64>() / n as f64;
10168                let pct = |q: f64| waits[((n as f64 - 1.0) * q) as usize];
10169                let empty = EMPTY_SUCCESSES.swap(0, std::sync::atomic::Ordering::Relaxed);
10170                if pass == 1 {
10171                    println!(
10172                        "{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",
10173                        pct(0.5),
10174                        pct(0.95),
10175                        pct(0.99),
10176                        waits[n - 1],
10177                        busy_total as f64 / n as f64,
10178                        empty as f64 / n as f64
10179                    );
10180                }
10181            }
10182        }
10183    }
10184
10185    /// Successful `doNotWait` locks that came back with an empty bitstream, across the bench.
10186    static EMPTY_SUCCESSES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
10187
10188    /// Test helper: upload one frame synchronously, submit it, wait for its bitstream with
10189    /// `strategy`, and unlock it; the wait in microseconds and the not-ready answers seen.
10190    unsafe fn lock_round_trip(
10191        enc: &mut NvencEncoder,
10192        pixels: &[u8],
10193        frame_number: u64,
10194        strategy: LockWait,
10195    ) -> (f64, u64) {
10196        let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
10197        let copy = CUDA_MEMCPY2D {
10198            srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
10199            srcHost: pixels.as_ptr() as *const c_void,
10200            srcPitch: (enc.width * 4) as usize,
10201            dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
10202            dstDevice: enc.input_device_ptr,
10203            dstPitch: enc.input_pitch,
10204            WidthInBytes: (enc.width * 4) as usize,
10205            Height: enc.height as usize,
10206            ..Default::default()
10207        };
10208        assert_eq!((enc.cuda.cuMemcpy2D_v2)(&copy), CUresult::CUDA_SUCCESS);
10209        let output_bitstream = enc.bitstream_buffers[enc.current_buffer_idx];
10210        enc.current_buffer_idx = (enc.current_buffer_idx + 1) % enc.bitstream_buffers.len();
10211        let mut pic_params = NV_ENC_PIC_PARAMS {
10212            version: sv(NvStruct::PicParams),
10213            inputWidth: enc.width,
10214            inputHeight: enc.height,
10215            inputBuffer: enc.mapped_input_buffer,
10216            outputBitstream: output_bitstream,
10217            bufferFmt: enc.input_format,
10218            pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
10219            encodePicFlags: if frame_number == 0 {
10220                NV_ENC_PIC_FLAGS::NV_ENC_PIC_FLAG_FORCEIDR as u32
10221            } else {
10222                0
10223            },
10224            ..Default::default()
10225        };
10226        let t0 = std::time::Instant::now();
10227        let st =
10228            (enc.nvenc_funcs.nvEncEncodePicture.unwrap())(enc.encoder_session, &mut pic_params);
10229        assert_eq!(st, NVENCSTATUS::NV_ENC_SUCCESS);
10230        let mut negotiated = Negotiated::new(NV_ENC_LOCK_BITSTREAM {
10231            version: sv(NvStruct::LockBitstream),
10232            outputBitstream: output_bitstream,
10233            ..Default::default()
10234        });
10235        let lock_params = &mut negotiated.value;
10236        lock_params.set_doNotWait(
10237            matches!(strategy, LockWait::Block)
10238                .then_some(0)
10239                .unwrap_or(1),
10240        );
10241        let lock_fn = enc.nvenc_funcs.nvEncLockBitstream.unwrap();
10242        let mut busy = 0u64;
10243        let mut empty_successes = 0u64;
10244        loop {
10245            match lock_fn(enc.encoder_session, lock_params) {
10246                NVENCSTATUS::NV_ENC_SUCCESS if lock_params.bitstreamSizeInBytes > 0 => break,
10247                NVENCSTATUS::NV_ENC_SUCCESS => {
10248                    empty_successes += 1;
10249                    (enc.nvenc_funcs.nvEncUnlockBitstream.unwrap())(
10250                        enc.encoder_session,
10251                        output_bitstream,
10252                    );
10253                    if let LockWait::Sleep(d) = strategy {
10254                        std::thread::sleep(d);
10255                    }
10256                    if matches!(strategy, LockWait::Block) {
10257                        panic!("blocking lock returned an empty bitstream");
10258                    }
10259                }
10260                NVENCSTATUS::NV_ENC_ERR_LOCK_BUSY => {
10261                    busy += 1;
10262                    if let LockWait::Sleep(d) = strategy {
10263                        std::thread::sleep(d);
10264                    }
10265                }
10266                other => panic!("lock failed: {other:?}"),
10267            }
10268        }
10269        let wait = t0.elapsed().as_secs_f64() * 1e6;
10270        if empty_successes > 0 {
10271            EMPTY_SUCCESSES.fetch_add(empty_successes, std::sync::atomic::Ordering::Relaxed);
10272        }
10273        (enc.nvenc_funcs.nvEncUnlockBitstream.unwrap())(enc.encoder_session, output_bitstream);
10274        (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
10275        (wait, busy)
10276    }
10277}
10278
10279#[cfg(test)]
10280mod version_tests {
10281    use super::*;
10282
10283    /// Every version-tagged struct, in one fixed order, paired with its pinned compile-time
10284    /// `NV_ENC_*_VER` constant — the reference both version tests iterate.
10285    const ALL: [(NvStruct, u32); 13] = [
10286        (NvStruct::FunctionList, NV_ENCODE_API_FUNCTION_LIST_VER),
10287        (
10288            NvStruct::OpenSessionExParams,
10289            NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER,
10290        ),
10291        (NvStruct::Config, NV_ENC_CONFIG_VER),
10292        (NvStruct::RcParams, NV_ENC_RC_PARAMS_VER),
10293        (NvStruct::PresetConfig, NV_ENC_PRESET_CONFIG_VER),
10294        (NvStruct::InitializeParams, NV_ENC_INITIALIZE_PARAMS_VER),
10295        (NvStruct::ReconfigureParams, NV_ENC_RECONFIGURE_PARAMS_VER),
10296        (NvStruct::RegisterResource, NV_ENC_REGISTER_RESOURCE_VER),
10297        (NvStruct::MapInputResource, NV_ENC_MAP_INPUT_RESOURCE_VER),
10298        (
10299            NvStruct::CreateBitstreamBuffer,
10300            NV_ENC_CREATE_BITSTREAM_BUFFER_VER,
10301        ),
10302        (NvStruct::PicParams, NV_ENC_PIC_PARAMS_VER),
10303        (NvStruct::LockBitstream, NV_ENC_LOCK_BITSTREAM_VER),
10304        (NvStruct::CapsParam, NV_ENC_CAPS_PARAM_VER),
10305    ];
10306
10307    /// For the pinned nvcodec-sys version, `nvenc_struct_ver` must reproduce every
10308    /// compile-time `NV_ENC_*_VER` constant exactly — guaranteeing a current driver is stamped
10309    /// byte-for-byte identically — and the packed major/minor must round-trip `NVENCAPI_VERSION`.
10310    /// Fails loudly if the bundled header is bumped without extending the revision table.
10311    #[test]
10312    fn table_is_identity_for_pinned_version() {
10313        let maj = NVENCAPI_VERSION & 0xFF;
10314        let min = (NVENCAPI_VERSION >> 24) & 0xFF;
10315        for (s, base) in ALL {
10316            assert_eq!(nvenc_struct_ver(s, maj, min), base, "{:?}", s);
10317        }
10318        assert_eq!(maj | (min << 24), NVENCAPI_VERSION);
10319    }
10320
10321    /// A reconfigure's reset and IDR flags land where each version's own header puts them:
10322    /// behind initialize params of 1808 bytes below 12.2, and of 1800 from it.
10323    #[test]
10324    fn reconfigure_flags_sit_where_each_version_reads_them() {
10325        for (maj, min, at) in [
10326            (10, 0, 1816),
10327            (11, 0, 1816),
10328            (11, 1, 1816),
10329            (12, 0, 1816),
10330            (12, 1, 1816),
10331            (12, 2, 1808),
10332            (13, 0, 1808),
10333        ] {
10334            let params =
10335                reconfigure_params(&NV_ENC_INITIALIZE_PARAMS::default(), (maj, min), true, true);
10336            let bytes = unsafe {
10337                std::slice::from_raw_parts(
10338                    (&params as *const Negotiated<_>).cast::<u8>(),
10339                    std::mem::size_of_val(&params),
10340                )
10341            };
10342            let word = |at: usize| u32::from_ne_bytes(bytes[at..at + 4].try_into().unwrap());
10343            assert_eq!(
10344                (word(1808), word(1816)),
10345                if at == 1816 { (0, 3) } else { (3, 0) },
10346                "{maj}.{min}"
10347            );
10348        }
10349    }
10350
10351    /// A QP delta map's pointer and size land where each version's own header puts them: at
10352    /// 1696 and 1704 below 12.0, and at 1712 and 1720 from it.
10353    #[test]
10354    fn qp_map_sits_where_each_version_reads_it() {
10355        let mut map = vec![0i8; 40];
10356        for (maj, min, at) in [
10357            (10, 0, 1696),
10358            (11, 0, 1696),
10359            (11, 1, 1696),
10360            (12, 0, 1712),
10361            (12, 1, 1712),
10362            (12, 2, 1712),
10363            (13, 0, 1712),
10364        ] {
10365            let mut pic = NV_ENC_PIC_PARAMS::default();
10366            point_at_qp_map(&mut pic, &mut map, (maj, min));
10367            let bytes = unsafe {
10368                std::slice::from_raw_parts(
10369                    (&pic as *const NV_ENC_PIC_PARAMS).cast::<u8>(),
10370                    std::mem::size_of_val(&pic),
10371                )
10372            };
10373            let word = |at: usize| u64::from_ne_bytes(bytes[at..at + 8].try_into().unwrap());
10374            let other = if at == 1696 { 1712 } else { 1696 };
10375            assert_eq!(word(at), map.as_ptr() as u64, "{maj}.{min}");
10376            assert_eq!(word(at + 8) as u32, 40, "{maj}.{min}");
10377            assert_eq!((word(other), word(other + 8)), (0, 0), "{maj}.{min}");
10378        }
10379    }
10380
10381    /// A 10-bit HEVC or AV1 config carries its depth where each version's own header puts it: at
10382    /// 12.2 and 13.0 as the input and output depth fields (10 at 200 and 204 for HEVC, 112 and
10383    /// 116 for AV1), before it as the depth minus eight in the bitfield at byte 17 (HEVC's
10384    /// `pixelBitDepthMinus8` at bits 3-5, AV1's input and output at 4-6 and 7-9), leaving the
10385    /// depth fields clear.
10386    #[test]
10387    fn bit_depth_sits_where_each_version_reads_it() {
10388        let word = |c: &NV_ENC_CONFIG, at: usize| {
10389            let bytes = unsafe {
10390                std::slice::from_raw_parts((c as *const NV_ENC_CONFIG).cast::<u8>(), 3584)
10391            };
10392            let at = 168 + at;
10393            u32::from_ne_bytes(bytes[at..at + 4].try_into().unwrap())
10394        };
10395        for (codec, depths, bits) in [
10396            (Codec::H265, (200, 204), 2 << 11),
10397            (Codec::Av1, (112, 116), 2 << 12 | 2 << 15),
10398        ] {
10399            for (maj, min) in [(10, 0), (11, 1), (12, 0), (12, 1), (12, 2), (13, 0)] {
10400                if codec == Codec::Av1 && maj < 12 {
10401                    continue;
10402                }
10403                let mut c = NV_ENC_CONFIG::default();
10404                set_bit_depth(&mut c, codec, 10, (maj, min));
10405                let named = (maj, min) >= (12, 2);
10406                assert_eq!(
10407                    (word(&c, depths.0), word(&c, depths.1)),
10408                    if named { (10, 10) } else { (0, 0) },
10409                    "{codec:?} {maj}.{min}"
10410                );
10411                assert_eq!(
10412                    word(&c, 16),
10413                    if named { 0 } else { bits },
10414                    "{codec:?} {maj}.{min}"
10415                );
10416            }
10417        }
10418    }
10419
10420    /// The structs handed to the driver run as long as the longest layout a negotiable version
10421    /// gives them: the initialize params 1808 bytes and the reconfigure params around them 1824
10422    /// in the SDK 10.0 to 12.1 headers, and the lock params 1552 in the 12.1 one.
10423    #[test]
10424    fn negotiated_structs_hold_the_longest_layout() {
10425        assert_eq!(
10426            std::mem::size_of::<Negotiated<NV_ENC_INITIALIZE_PARAMS>>(),
10427            1808
10428        );
10429        assert_eq!(
10430            std::mem::size_of::<Negotiated<NV_ENC_RECONFIGURE_PARAMS>>(),
10431            1824
10432        );
10433        assert_eq!(
10434            std::mem::size_of::<Negotiated<NV_ENC_LOCK_BITSTREAM>>(),
10435            1552
10436        );
10437    }
10438
10439    /// `nvenc_struct_ver` must reproduce the exact `NV_ENC_*_VER` words each SDK defined, for
10440    /// every negotiable version 10.0 through 13.0.
10441    ///
10442    /// The expected words are hardcoded from `nvEncodeAPI.h` at the FFmpeg nv-codec-headers tags
10443    /// listed in `NvStruct::rev`, one row per SDK version in `ALL` order. (The n10.0.26.2 header
10444    /// spells the flag `1<<31` rather than `1u<<31`; the bit is the same.) This is what lets the
10445    /// 13.0-layout structs be stamped with an older SDK's word when the session down-negotiates.
10446    #[test]
10447    fn table_matches_historical_headers() {
10448        #[rustfmt::skip]
10449        let expected: [(u32, u32, [u32; 12]); 7] = [
10450            (10, 0, [0x7002000A, 0x7001000A, 0xF007000A, 0x7001000A, 0xF004000A, 0xF005000A, 0xF001000A,
10451                     0x7003000A, 0x7004000A, 0x7001000A, 0xF004000A, 0x7001000A]),
10452            (11, 0, [0x7002000B, 0x7001000B, 0xF007000B, 0x7001000B, 0xF004000B, 0xF005000B, 0xF001000B,
10453                     0x7003000B, 0x7004000B, 0x7001000B, 0xF004000B, 0x7001000B]),
10454            (11, 1, [0x7102000B, 0x7101000B, 0xF107000B, 0x7101000B, 0xF104000B, 0xF105000B, 0xF101000B,
10455                     0x7103000B, 0x7104000B, 0x7101000B, 0xF104000B, 0x7101000B]),
10456            (12, 0, [0x7002000C, 0x7001000C, 0xF008000C, 0x7001000C, 0xF004000C, 0xF005000C, 0xF001000C,
10457                     0x7004000C, 0x7004000C, 0x7001000C, 0xF006000C, 0x7002000C]),
10458            (12, 1, [0x7102000C, 0x7101000C, 0xF108000C, 0x7101000C, 0xF104000C, 0xF106000C, 0xF101000C,
10459                     0x7104000C, 0x7104000C, 0x7101000C, 0xF106000C, 0xF101000C]),
10460            (12, 2, [0x7202000C, 0x7201000C, 0xF209000C, 0x7201000C, 0xF205000C, 0xF207000C, 0xF202000C,
10461                     0x7205000C, 0x7204000C, 0x7201000C, 0xF207000C, 0xF202000C]),
10462            (13, 0, [0x7002000D, 0x7001000D, 0xF009000D, 0x7001000D, 0xF005000D, 0xF007000D, 0xF002000D,
10463                     0x7005000D, 0x7004000D, 0x7001000D, 0xF007000D, 0xF002000D]),
10464        ];
10465        for (maj, min, words) in expected {
10466            for ((s, _), want) in ALL.iter().zip(words) {
10467                assert_eq!(
10468                    nvenc_struct_ver(*s, maj, min),
10469                    want,
10470                    "{:?} at {}.{}",
10471                    s,
10472                    maj,
10473                    min
10474                );
10475            }
10476        }
10477    }
10478}
10479
10480#[cfg(test)]
10481mod decision_tests {
10482    use super::*;
10483
10484    /// A caps query that returns `Some(0)` means the GPU lacks 4:4:4, so a 4:4:4 request is met
10485    /// with 4:2:0 and flagged as a downgrade; `Some(1)` keeps 4:4:4; an unqueryable cap (`None`)
10486    /// leaves the request untouched rather than downgrading on missing information.
10487    #[test]
10488    fn caps_chroma_downgrade() {
10489        let d = decide_caps(true, 1920, 1080, Some(0), None, None);
10490        assert!(!d.fullcolor && d.downgraded_color && d.too_large.is_none());
10491
10492        let d = decide_caps(true, 1920, 1080, Some(1), None, None);
10493        assert!(d.fullcolor && !d.downgraded_color);
10494
10495        let d = decide_caps(true, 1920, 1080, None, None, None);
10496        assert!(d.fullcolor && !d.downgraded_color);
10497
10498        // A 4:2:0 request is never a downgrade whatever the cap says.
10499        let d = decide_caps(false, 1920, 1080, Some(0), None, None);
10500        assert!(!d.fullcolor && !d.downgraded_color);
10501    }
10502
10503    /// A capture beyond the driver's reported maximum dimensions is flagged `too_large` (the
10504    /// caller then declines NVENC and uses software); a capture within them, or one whose caps are
10505    /// unknown, is not.
10506    #[test]
10507    fn caps_dimension_gate() {
10508        assert_eq!(
10509            decide_caps(false, 5120, 2160, None, Some(4096), Some(4096)).too_large,
10510            Some((4096, 4096))
10511        );
10512        assert_eq!(
10513            decide_caps(false, 3840, 4320, None, Some(4096), Some(4096)).too_large,
10514            Some((4096, 4096))
10515        );
10516        assert!(
10517            decide_caps(false, 3840, 2160, None, Some(4096), Some(4096))
10518                .too_large
10519                .is_none()
10520        );
10521        assert!(
10522            decide_caps(false, 7680, 4320, None, None, None)
10523                .too_large
10524                .is_none()
10525        );
10526        // A zero cap is treated as unknown, not as "everything is too large".
10527        assert!(
10528            decide_caps(false, 3840, 2160, None, Some(0), Some(0))
10529                .too_large
10530                .is_none()
10531        );
10532    }
10533
10534    /// The resize headroom lifts the request to the 5.2-ceiling floor but never past the driver
10535    /// maximum, so initializing with headroom cannot itself exceed what the GPU supports.
10536    #[test]
10537    fn headroom_is_floored_and_capped() {
10538        assert_eq!(nvenc_headroom(1920, 4096, Some(8192)), 4096);
10539        assert_eq!(nvenc_headroom(3840, 4096, Some(4096)), 4096);
10540        assert_eq!(nvenc_headroom(6000, 4096, Some(8192)), 6000);
10541        assert_eq!(nvenc_headroom(1920, 4096, None), 4096);
10542        assert_eq!(nvenc_headroom(1920, 4096, Some(2048)), 2048);
10543    }
10544
10545    /// H.264 and HEVC advertise the current geometry's level, so a decoder that gates on the
10546    /// level takes a 1080p stream a 4K-headroom session would once have marked 5.2; a resize
10547    /// re-declares it and a live rate change between the common rates does not. AV1 holds the
10548    /// headroom level, which NVENC requires it to cover at init and every AV1 decoder accepts.
10549    #[test]
10550    fn nvenc_level_follows_geometry_except_av1() {
10551        // The 1080p60 level, the one older Apple and Intel decoders gate on, is well below the
10552        // 4K-headroom level the session used to pin: H.264 4.2 and HEVC 4.1 rather than 5.2/5.1.
10553        assert_eq!(nvenc_level(Codec::H264, 1920, 1080, 60, 0, true), 42);
10554        assert_eq!(nvenc_level(Codec::H265, 1920, 1080, 60, 0, true), 123);
10555        // The current level rises with the picture, back to the headroom level at 4K.
10556        assert_eq!(nvenc_level(Codec::H264, 3840, 2160, 60, 0, true), 52);
10557        assert_eq!(nvenc_level(Codec::H265, 3840, 2160, 60, 0, true), 153);
10558        // A picture counts in whole coding tree blocks: 720p at 144 fps is 4.1 by its samples.
10559        assert_eq!(h265_level(1280, 720, 144, 0, true), 123);
10560        assert_eq!(nvenc_level(Codec::H265, 1280, 720, 144, 0, true), 150);
10561        // The current level admits the current picture on every codec.
10562        for (w, h) in [(1280u32, 720u32), (1920, 1080), (3840, 2160)] {
10563            let macroblocks = (u64::from(w) / 16) * (u64::from(h) / 16);
10564            assert!(
10565                h264_max_macroblocks(nvenc_level(Codec::H264, w, h, 60, 0, true)) >= macroblocks,
10566                "H.264 level at {w}x{h} cannot hold the picture"
10567            );
10568            assert!(
10569                h265_max_picture(nvenc_level(Codec::H265, w, h, 60, 0, true))
10570                    >= u64::from(w) * u64::from(h),
10571                "HEVC level at {w}x{h} cannot hold the picture"
10572            );
10573        }
10574        // AV1 holds the headroom level whatever the capture, since NVENC checks it against
10575        // maxEncodeWidth x maxEncodeHeight at init.
10576        let pixels = u64::from(HEADROOM_WIDTH * HEADROOM_HEIGHT);
10577        for (w, h) in [(1280, 720), (1920, 1080), (3840, 2160)] {
10578            assert_eq!(
10579                nvenc_level(Codec::Av1, w, h, 60, 0, true),
10580                nvenc_level(Codec::Av1, HEADROOM_WIDTH, HEADROOM_HEIGHT, 60, 0, true),
10581                "AV1 level moved with the capture at {w}x{h}"
10582            );
10583            assert!(
10584                av1_max_picture(nvenc_level(Codec::Av1, w, h, 60, 0, true)) >= pixels,
10585                "AV1 level at {w}x{h} cannot hold the headroom"
10586            );
10587        }
10588        // A live rate change between the common rates keeps the level, so it carries no IDR.
10589        for codec in [Codec::H264, Codec::H265, Codec::Av1] {
10590            assert_eq!(
10591                nvenc_level(codec, 1920, 1080, 30, 0, true),
10592                nvenc_level(codec, 1920, 1080, 60, 0, true),
10593                "{codec:?} level moved between 30 and 60 fps"
10594            );
10595        }
10596        // A CBR target past the level's ceiling takes the first level that admits it: the
10597        // driver refuses the session otherwise, as an invalid level.
10598        assert_eq!(
10599            nvenc_level(Codec::H264, 1920, 1080, 60, 100_000_000, true),
10600            50
10601        );
10602        assert_eq!(
10603            nvenc_level(Codec::H265, 1920, 1080, 60, 60_000_000, true),
10604            150
10605        );
10606        assert_eq!(
10607            nvenc_level(Codec::H265, 1920, 1080, 60, 60_000_000, false),
10608            156
10609        );
10610        // An AV1 level holds its Annex A MaxBitrate, 5.1 40 Mbit/s, 5.2 60, 6.1 100, 6.2 160;
10611        // weighed by `NVENC_AV1_RATE`, two thirds of it: 5.1 26.7, 5.2 40, 6.1 66.7, 6.2 106.7.
10612        for (bps, annex_a, weighted) in [
10613            (26_666_666, 13, 13),
10614            (26_666_667, 13, 14),
10615            (40_000_000, 13, 14),
10616            (40_000_001, 14, 17),
10617            (45_000_000, 14, 17),
10618            (60_000_001, 17, 17),
10619            (66_666_667, 17, 18),
10620            (100_000_001, 18, 18),
10621            (106_666_666, 18, 18),
10622        ] {
10623            for (weigh, level) in [(false, annex_a), (true, weighted)] {
10624                assert_eq!(
10625                    nvenc_level(
10626                        Codec::Av1,
10627                        1920,
10628                        1080,
10629                        60,
10630                        level_rate(Codec::Av1, bps, weigh),
10631                        true
10632                    ),
10633                    level,
10634                    "AV1 at {bps} bit/s, weighed {weigh}"
10635                );
10636            }
10637        }
10638        assert_eq!(level_rate(Codec::H265, 60_000_000, true), 60_000_000);
10639    }
10640
10641    /// A frame splits across the engines for AV1 at any picture, in three strips on three, and
10642    /// for HEVC from 4K, never for H.264, on one engine, or before API 12.1, whose bits there are
10643    /// another flag's.
10644    #[test]
10645    fn split_mode_follows_the_codec_and_picture() {
10646        use NV_ENC_SPLIT_ENCODE_MODE::{
10647            NV_ENC_SPLIT_AUTO_FORCED_MODE as Forced, NV_ENC_SPLIT_AUTO_MODE as Driver,
10648            NV_ENC_SPLIT_THREE_FORCED_MODE as Three,
10649        };
10650        for (codec, w, h, engines, api, want) in [
10651            (Codec::Av1, 1280, 720, Some(2), (13, 0), Forced),
10652            (Codec::Av1, 3840, 2160, Some(3), (12, 1), Three),
10653            (Codec::Av1, 1920, 1080, Some(3), (13, 0), Three),
10654            (Codec::Av1, 1920, 1080, Some(4), (13, 0), Forced),
10655            (Codec::Av1, 1920, 1080, Some(3), (12, 0), Driver),
10656            (Codec::H265, 3840, 2160, Some(3), (13, 0), Forced),
10657            (Codec::H265, 1920, 1080, Some(3), (13, 0), Driver),
10658            (Codec::Av1, 1920, 1080, Some(1), (13, 0), Driver),
10659            (Codec::Av1, 1920, 1080, None, (13, 0), Driver),
10660            (Codec::H265, 2560, 1440, Some(2), (13, 0), Driver),
10661            (Codec::H265, 3840, 2160, Some(2), (13, 0), Forced),
10662            (Codec::H265, 2160, 3840, Some(2), (13, 0), Forced),
10663            (Codec::H265, 3840, 2160, Some(2), (12, 0), Driver),
10664            (Codec::H264, 3840, 2160, Some(2), (13, 0), Driver),
10665        ] {
10666            assert_eq!(
10667                split_mode(codec, w, h, engines, api),
10668                want,
10669                "{codec:?} {w}x{h} on {engines:?} engines, API {api:?}"
10670            );
10671        }
10672    }
10673
10674    /// A CBR target past every level's ceiling is held to the top one, which the driver opens,
10675    /// and one inside it passes as asked.
10676    #[test]
10677    fn cbr_targets_hold_to_the_top_level() {
10678        for (codec, kbps, bps) in [
10679            (Codec::Av1, 8_000, 8_000_000),
10680            (Codec::Av1, 106_666, 106_666_000),
10681            (Codec::Av1, 200_000, 106_666_666),
10682            (Codec::H265, 800_000, 800_000_000),
10683            (Codec::H265, 1_000_000, 800_000_000),
10684            (Codec::H264, 1_000_000, 1_000_000_000),
10685            (Codec::H264, 5_000_000, 1_000_000_000),
10686        ] {
10687            let s = CaptureSettings {
10688                codec,
10689                video_bitrate_kbps: kbps,
10690                ..Default::default()
10691            };
10692            assert_eq!(cbr_bps(&s), bps, "{codec:?} at {kbps} kbps");
10693        }
10694        assert_eq!(
10695            nvenc_level(
10696                Codec::Av1,
10697                1920,
10698                1080,
10699                240,
10700                u64::from(nvenc_rate_ceiling(Codec::Av1)),
10701                true
10702            ),
10703            18
10704        );
10705    }
10706
10707    /// AV1 Annex A MaxPicSize for a seq_level_idx the ladder can return.
10708    fn av1_max_picture(level: u32) -> u64 {
10709        match level {
10710            8 | 9 => 2_359_296,
10711            12..=15 => 8_912_896,
10712            _ => 35_651_584,
10713        }
10714    }
10715
10716    /// HEVC Annex A MaxLumaPs for a general_level_idc the ladder can return.
10717    fn h265_max_picture(level: u32) -> u64 {
10718        match level {
10719            123 => 2_228_224,
10720            150 | 153 | 156 => 8_912_896,
10721            _ => 35_651_584,
10722        }
10723    }
10724
10725    /// H.264 Annex A MaxFS for a level_idc the ladder can return.
10726    fn h264_max_macroblocks(level: u32) -> u64 {
10727        match level {
10728            41 => 8192,
10729            42 => 8704,
10730            50 => 22080,
10731            51 | 52 => 36864,
10732            _ => 139264,
10733        }
10734    }
10735
10736    /// Test helper: a mapped `CUeglFrame` of one plane with the given kind, geometry, and pitch,
10737    /// its first plane at `plane` (a device pointer for the pitch kind, an array handle for the
10738    /// array kind) in four 8-bit channels.
10739    fn egl_frame(frame_type: u32, w: u32, h: u32, pitch: u32, plane: usize) -> CUeglFrame {
10740        let mut f: CUeglFrame = unsafe { std::mem::zeroed() };
10741        f.frame_type = frame_type;
10742        f.plane_count = 1;
10743        f.width = w;
10744        f.height = h;
10745        f.pitch = pitch;
10746        f.num_channels = 4;
10747        f.cu_format = CU_AD_FORMAT_U8;
10748        f.frame.p_pitch = [plane as *mut c_void, ptr::null_mut(), ptr::null_mut()];
10749        f
10750    }
10751
10752    /// A pitch-linear mapping whose first plane covers the session geometry at a 4-byte-aligned
10753    /// pitch of at least `width * 4` is registered with NVENC in place as a device pointer at its
10754    /// own pitch; a null plane, a short or unaligned pitch, a frame smaller than the session, or a
10755    /// frame without planes each take the per-frame copy.
10756    #[test]
10757    fn pitch_linear_frame_direct_registration_rules() {
10758        let pitch_ok = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7680, 0x1000);
10759        assert_eq!(
10760            direct_plane(&pitch_ok, 1920, 1080),
10761            Some(DirectPlane::Pitch(7680))
10762        );
10763        let padded = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 8192, 0x1000);
10764        assert_eq!(
10765            direct_plane(&padded, 1920, 1080),
10766            Some(DirectPlane::Pitch(8192))
10767        );
10768        let larger = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 2048, 1200, 8192, 0x1000);
10769        assert_eq!(
10770            direct_plane(&larger, 1920, 1080),
10771            Some(DirectPlane::Pitch(8192))
10772        );
10773
10774        let null_plane = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7680, 0);
10775        assert_eq!(direct_plane(&null_plane, 1920, 1080), None);
10776        let short_pitch = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7676, 0x1000);
10777        assert_eq!(direct_plane(&short_pitch, 1920, 1080), None);
10778        let unaligned = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7682, 0x1000);
10779        assert_eq!(direct_plane(&unaligned, 1920, 1080), None);
10780        let smaller = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1280, 720, 7680, 0x1000);
10781        assert_eq!(direct_plane(&smaller, 1920, 1080), None);
10782        let mut no_planes = pitch_ok;
10783        no_planes.plane_count = 0;
10784        assert_eq!(direct_plane(&no_planes, 1920, 1080), None);
10785        assert_eq!(direct_plane(&pitch_ok, 0, 1080), None);
10786        let mut unknown_kind = pitch_ok;
10787        unknown_kind.frame_type = 7;
10788        assert_eq!(direct_plane(&unknown_kind, 1920, 1080), None);
10789    }
10790
10791    /// A CUDA-array mapping of four 8-bit channels covering the session geometry is registered in
10792    /// place as a CUDA array whose pitch word is the array's row width in bytes; an array of any
10793    /// other element layout, a null array, or one smaller than the session takes the copy.
10794    #[test]
10795    fn cuda_array_frame_direct_registration_rules() {
10796        let array = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1920, 1080, 0, 0x2000);
10797        assert_eq!(
10798            direct_plane(&array, 1920, 1080),
10799            Some(DirectPlane::Array(7680))
10800        );
10801        let wider = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 2048, 1080, 0, 0x2000);
10802        assert_eq!(
10803            direct_plane(&wider, 1920, 1080),
10804            Some(DirectPlane::Array(8192))
10805        );
10806
10807        let null_array = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1920, 1080, 0, 0);
10808        assert_eq!(direct_plane(&null_array, 1920, 1080), None);
10809        let mut one_channel = array;
10810        one_channel.num_channels = 1;
10811        assert_eq!(direct_plane(&one_channel, 1920, 1080), None);
10812        let mut wide_elements = array;
10813        wide_elements.cu_format = 3;
10814        assert_eq!(direct_plane(&wide_elements, 1920, 1080), None);
10815        let smaller = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1280, 720, 0, 0x2000);
10816        assert_eq!(direct_plane(&smaller, 1920, 1080), None);
10817    }
10818
10819    /// The dmabuf fourcc picks the NVENC packed format with the same byte order: XR24 / AR24 are
10820    /// B,G,R,A in memory and so NVENC `ARGB`; XB24 / AB24 are R,G,B,A and so `ABGR`; anything else
10821    /// has no packed 8-bit NVENC equivalent.
10822    #[test]
10823    fn fourcc_selects_nvenc_byte_order() {
10824        for code in [Fourcc::Argb8888, Fourcc::Xrgb8888] {
10825            assert_eq!(
10826                fourcc_nvenc_format(code),
10827                Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB)
10828            );
10829        }
10830        for code in [Fourcc::Abgr8888, Fourcc::Xbgr8888] {
10831            assert_eq!(
10832                fourcc_nvenc_format(code),
10833                Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR)
10834            );
10835        }
10836        for code in [
10837            Fourcc::Rgb565,
10838            Fourcc::Nv12,
10839            Fourcc::Argb2101010,
10840            Fourcc::Bgra8888,
10841            Fourcc::Rgba8888,
10842        ] {
10843            assert_eq!(fourcc_nvenc_format(code), None, "{code:?}");
10844        }
10845    }
10846
10847    /// The EGL import names X-formats by their alpha twin and leaves everything else alone.
10848    #[test]
10849    fn egl_import_names_alpha_twins() {
10850        assert_eq!(egl_import_fourcc(Fourcc::Xrgb8888), Fourcc::Argb8888);
10851        assert_eq!(egl_import_fourcc(Fourcc::Xbgr8888), Fourcc::Abgr8888);
10852        for code in [Fourcc::Argb8888, Fourcc::Abgr8888, Fourcc::Nv12] {
10853            assert_eq!(egl_import_fourcc(code), code);
10854        }
10855    }
10856}