1#![allow(unsafe_op_in_unsafe_fn)]
26#![allow(non_camel_case_types)]
27#![allow(non_snake_case)]
28
29use std::collections::HashMap;
30use std::ffi::{CStr, CString, c_char, c_void};
31use std::os::unix::io::AsRawFd;
32use std::ptr;
33use std::sync::Arc;
34use std::sync::atomic::{AtomicBool, Ordering};
35
36use libloading::{Library, Symbol};
37use smithay::backend::allocator::{Buffer, Fourcc, dmabuf::Dmabuf};
38
39use super::codec::{
40 Codec, FRAME_ANCHOR, FRAME_DELTA, FRAME_INTRA, FRAME_KEY, Hardware, VIDEO_HEADER_LEN,
41 av1_level, h264_dpb_frames, h264_level, h265_dpb_frames, h265_level, h265_tier,
42 push_video_header,
43};
44use super::frame_rate::FrameRate;
45use super::reference::{ANCHORS, Invalidation, Reference, ReferenceReport, ReferenceWindow};
46use super::sps::{WideFrameNum, h264_frame_num_range};
47use crate::RustCaptureSettings;
48use nvcodec_sys::cuda::*;
49use nvcodec_sys::*;
50
51type CUmodule = *mut c_void;
54type CUfunction = *mut c_void;
55
56const ARGB_TO_NV12_PTX: &[u8] = include_bytes!("argb_to_nv12.ptx");
61
62unsafe fn load_kernels(cuda: &CudaFunctions) -> Option<CUmodule> {
64 let mut ptx = ARGB_TO_NV12_PTX.to_vec();
65 ptx.push(0);
66 let mut module: CUmodule = ptr::null_mut();
67 ((cuda.cuModuleLoadData)(&mut module, ptx.as_ptr() as *const c_void) == CUresult::CUDA_SUCCESS)
68 .then_some(module)
69}
70
71type EGLDisplay = *const c_void;
74type EGLImageKHR = *mut c_void;
75type EGLint = i32;
76type EGLenum = u32;
77type EGLBoolean = u32;
78
79const EGL_NO_IMAGE_KHR: EGLImageKHR = ptr::null_mut();
80const EGL_LINUX_DMA_BUF_EXT: u32 = 0x3270;
81const EGL_DMA_BUF_PLANE0_FD_EXT: EGLint = 0x3272;
82const EGL_DMA_BUF_PLANE0_OFFSET_EXT: EGLint = 0x3273;
83const EGL_DMA_BUF_PLANE0_PITCH_EXT: EGLint = 0x3274;
84const EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT: EGLint = 0x3443;
85const EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT: EGLint = 0x3444;
86const EGL_WIDTH: EGLint = 0x3057;
87const EGL_HEIGHT: EGLint = 0x3056;
88const EGL_LINUX_DRM_FOURCC_EXT: EGLint = 0x3271;
89const EGL_NONE: EGLint = 0x3038;
90
91type CUgraphicsResource = *mut c_void;
94
95const CU_EGL_FRAME_TYPE_ARRAY: u32 = 0;
98const CU_EGL_FRAME_TYPE_PITCH: u32 = 1;
99const CU_AD_FORMAT_U8: u32 = 1;
101
102const CU_TRSF_READ_AS_INTEGER: u32 = 1;
105
106#[repr(C)]
109#[derive(Clone, Copy)]
110struct CUeglFrame {
111 frame: CUeglFrameUnion,
112 width: u32,
113 height: u32,
114 depth: u32,
115 pitch: u32,
116 plane_count: u32,
117 num_channels: u32,
118 frame_type: u32,
119 egl_color_format: u32,
120 cu_format: u32,
121}
122
123#[repr(C)]
126#[derive(Clone, Copy)]
127union CUeglFrameUnion {
128 p_array: [CUarray; 3],
129 p_pitch: [*mut c_void; 3],
130}
131
132type EglCreateImageKhrFn = unsafe extern "C" fn(
133 dpy: EGLDisplay,
134 ctx: *mut c_void,
135 target: EGLenum,
136 buffer: *mut c_void,
137 attrib_list: *const EGLint,
138) -> EGLImageKHR;
139type EglDestroyImageKhrFn = unsafe extern "C" fn(dpy: EGLDisplay, image: EGLImageKHR) -> EGLBoolean;
140
141struct EglFunctions {
144 _lib: Library,
145 eglGetProcAddress: unsafe extern "C" fn(procname: *const c_char) -> *mut c_void,
146 eglCreateImageKHR: EglCreateImageKhrFn,
147 eglDestroyImageKHR: EglDestroyImageKhrFn,
148}
149
150struct CudaFunctions {
153 _lib: Library,
154 cuInit: unsafe extern "C" fn(flags: u32) -> CUresult,
155 cuDeviceGet: unsafe extern "C" fn(device: *mut CUdevice, ordinal: i32) -> CUresult,
156 cuDeviceGetByPCIBusId:
157 unsafe extern "C" fn(dev: *mut CUdevice, pciBusId: *const c_char) -> CUresult,
158 cuDevicePrimaryCtxRetain: unsafe extern "C" fn(pctx: *mut CUcontext, dev: CUdevice) -> CUresult,
159 cuCtxPushCurrent_v2: unsafe extern "C" fn(ctx: CUcontext) -> CUresult,
160 cuCtxPopCurrent_v2: unsafe extern "C" fn(pctx: *mut CUcontext) -> CUresult,
161 cuDevicePrimaryCtxRelease_v2: unsafe extern "C" fn(dev: CUdevice) -> CUresult,
162 cuMemAlloc_v2: unsafe extern "C" fn(dptr: *mut CUdeviceptr, bytesize: usize) -> CUresult,
163 cuMemAllocPitch_v2: unsafe extern "C" fn(
164 dptr: *mut CUdeviceptr,
165 pPitch: *mut usize,
166 WidthInBytes: usize,
167 Height: usize,
168 ElementSizeBytes: u32,
169 ) -> CUresult,
170 cuMemFree_v2: unsafe extern "C" fn(dptr: CUdeviceptr) -> CUresult,
171 cuMemcpyHtoD_v2: unsafe extern "C" fn(
172 dstDevice: CUdeviceptr,
173 srcHost: *const c_void,
174 ByteCount: usize,
175 ) -> CUresult,
176 cuMemcpyDtoH_v2: unsafe extern "C" fn(
177 dstHost: *mut c_void,
178 srcDevice: CUdeviceptr,
179 ByteCount: usize,
180 ) -> CUresult,
181 cuMemcpy2D_v2: unsafe extern "C" fn(pCopy: *const CUDA_MEMCPY2D) -> CUresult,
182 cuMemcpy2DAsync_v2:
183 unsafe extern "C" fn(pCopy: *const CUDA_MEMCPY2D, hStream: CUstream) -> CUresult,
184 cuStreamSynchronize: unsafe extern "C" fn(hStream: CUstream) -> CUresult,
185 cuModuleLoadData: unsafe extern "C" fn(module: *mut CUmodule, image: *const c_void) -> CUresult,
186 cuModuleGetFunction: unsafe extern "C" fn(
187 hfunc: *mut CUfunction,
188 hmod: CUmodule,
189 name: *const c_char,
190 ) -> CUresult,
191 cuModuleUnload: unsafe extern "C" fn(hmod: CUmodule) -> CUresult,
192 cuTexObjectCreate: unsafe extern "C" fn(
193 tex: *mut CUtexObject,
194 res: *const CUDA_RESOURCE_DESC,
195 sampling: *const CUDA_TEXTURE_DESC,
196 view: *const c_void,
197 ) -> CUresult,
198 cuTexObjectDestroy: unsafe extern "C" fn(tex: CUtexObject) -> CUresult,
199 #[allow(clippy::type_complexity)]
200 cuLaunchKernel: unsafe extern "C" fn(
201 f: CUfunction,
202 gx: u32,
203 gy: u32,
204 gz: u32,
205 bx: u32,
206 by: u32,
207 bz: u32,
208 shared: u32,
209 stream: CUstream,
210 params: *mut *mut c_void,
211 extra: *mut *mut c_void,
212 ) -> CUresult,
213 cuMemHostRegister_v2:
214 unsafe extern "C" fn(p: *mut c_void, bytesize: usize, flags: u32) -> CUresult,
215 cuMemHostUnregister: unsafe extern "C" fn(p: *mut c_void) -> CUresult,
216 cuGraphicsEGLRegisterImage: unsafe extern "C" fn(
217 pCudaResource: *mut CUgraphicsResource,
218 image: EGLImageKHR,
219 flags: u32,
220 ) -> CUresult,
221 cuGraphicsUnregisterResource: unsafe extern "C" fn(resource: CUgraphicsResource) -> CUresult,
222 cuGraphicsResourceGetMappedEglFrame: unsafe extern "C" fn(
223 pEglFrame: *mut CUeglFrame,
224 resource: CUgraphicsResource,
225 index: u32,
226 mipLevel: u32,
227 ) -> CUresult,
228 cuDeviceGetCount: unsafe extern "C" fn(count: *mut i32) -> CUresult,
229 cuDeviceGetName: unsafe extern "C" fn(name: *mut c_char, len: i32, dev: CUdevice) -> CUresult,
230 cuDeviceGetUuid: unsafe extern "C" fn(uuid: *mut CUuuid, dev: CUdevice) -> CUresult,
231 cuGetErrorName: unsafe extern "C" fn(error: CUresult, pStr: *mut *const c_char) -> CUresult,
232 cuImportExternalSemaphore: Option<
235 unsafe extern "C" fn(
236 out: *mut CUexternalSemaphore,
237 desc: *const CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC,
238 ) -> CUresult,
239 >,
240 cuWaitExternalSemaphoresAsync: Option<
241 unsafe extern "C" fn(
242 semaphores: *const CUexternalSemaphore,
243 params: *const CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS,
244 count: u32,
245 stream: CUstream,
246 ) -> CUresult,
247 >,
248 cuDestroyExternalSemaphore: Option<unsafe extern "C" fn(sem: CUexternalSemaphore) -> CUresult>,
249}
250
251struct NvencLibrary {
261 _lib: Library,
262 create_instance:
263 unsafe extern "C" fn(functionList: *mut NV_ENCODE_API_FUNCTION_LIST) -> NVENCSTATUS,
264 get_max_version: Option<unsafe extern "C" fn(*mut u32) -> NVENCSTATUS>,
265}
266
267static NVENC_NEG_VER: std::sync::OnceLock<(u32, u32)> = std::sync::OnceLock::new();
270
271#[derive(Clone, Copy, Debug)]
284enum NvStruct {
285 FunctionList,
286 OpenSessionExParams,
287 Config,
288 RcParams,
289 PresetConfig,
290 InitializeParams,
291 ReconfigureParams,
292 RegisterResource,
293 MapInputResource,
294 CreateBitstreamBuffer,
295 PicParams,
296 LockBitstream,
297 CapsParam,
298}
299
300impl NvStruct {
301 fn rev(self, api: u32) -> (u32, bool) {
313 match self {
314 NvStruct::FunctionList => (2, false),
315 NvStruct::OpenSessionExParams => (1, false),
316 NvStruct::Config => match api {
317 0xC2.. => (9, true),
318 0xC0..=0xC1 => (8, true),
319 _ => (7, true),
320 },
321 NvStruct::RcParams => (1, false),
322 NvStruct::PresetConfig => (if api >= 0xC2 { 5 } else { 4 }, true),
323 NvStruct::InitializeParams => match api {
324 0xC2.. => (7, true),
325 0xC1 => (6, true),
326 _ => (5, true),
327 },
328 NvStruct::ReconfigureParams => (if api >= 0xC2 { 2 } else { 1 }, true),
329 NvStruct::RegisterResource => match api {
330 0xC2.. => (5, false),
331 0xC0..=0xC1 => (4, false),
332 _ => (3, false),
333 },
334 NvStruct::MapInputResource => (4, false),
335 NvStruct::CreateBitstreamBuffer => (1, false),
336 NvStruct::PicParams => match api {
337 0xC2.. => (7, true),
338 0xC0..=0xC1 => (6, true),
339 _ => (4, true),
340 },
341 NvStruct::LockBitstream => match api {
342 0xC2.. => (2, true),
343 0xC1 => (1, true),
344 0xC0 => (2, false),
345 _ => (1, false),
346 },
347 NvStruct::CapsParam => (1, false),
348 }
349 }
350}
351
352fn nvenc_struct_ver(s: NvStruct, maj: u32, min: u32) -> u32 {
365 let (rev, high_bit) = s.rev((maj << 4) | (min & 0xF));
366 (maj & 0xFF) | ((min & 0xF) << 24) | (rev << 16) | (0x7 << 28) | ((high_bit as u32) << 31)
367}
368
369#[inline]
373fn nvenc_cur_ver() -> (u32, u32) {
374 NVENC_NEG_VER
375 .get()
376 .copied()
377 .unwrap_or((NVENCAPI_VERSION & 0xFF, (NVENCAPI_VERSION >> 24) & 0xFF))
378}
379
380#[inline]
383fn sv(s: NvStruct) -> u32 {
384 let (m, n) = nvenc_cur_ver();
385 nvenc_struct_ver(s, m, n)
386}
387
388#[inline]
392fn neg_api() -> u32 {
393 let (m, n) = nvenc_cur_ver();
394 m | (n << 24)
395}
396
397#[repr(C)]
402struct Negotiated<T> {
403 value: T,
404 tail: [u32; 2],
405}
406
407impl<T> Negotiated<T> {
408 fn new(value: T) -> Self {
409 Self {
410 value,
411 tail: [0; 2],
412 }
413 }
414}
415
416fn reconfigure_params(
419 init: NV_ENC_INITIALIZE_PARAMS,
420 (maj, min): (u32, u32),
421 reset: bool,
422 force_idr: bool,
423) -> Negotiated<NV_ENC_RECONFIGURE_PARAMS> {
424 let mut p = Negotiated::new(NV_ENC_RECONFIGURE_PARAMS {
425 version: nvenc_struct_ver(NvStruct::ReconfigureParams, maj, min),
426 reInitEncodeParams: init,
427 ..Default::default()
428 });
429 if (maj << 4) | min < 0xC2 {
430 p.tail[0] = reset as u32 | (force_idr as u32) << 1;
431 } else {
432 p.value.set_resetEncoder(reset as u32);
433 p.value.set_forceIDR(force_idr as u32);
434 }
435 p
436}
437
438fn nvenc_negotiate(lib: &NvencLibrary) {
460 NVENC_NEG_VER.get_or_init(|| {
461 let pinned = (NVENCAPI_VERSION & 0xFF, (NVENCAPI_VERSION >> 24) & 0xFF);
462 let mut drv_max: u32 = 0;
463 if let Some(get_max) = lib.get_max_version {
464 let mut m: u32 = 0;
465 if unsafe { get_max(&mut m) } == NVENCSTATUS::NV_ENC_SUCCESS {
466 drv_max = m;
467 }
468 }
469 if let Ok(cap) = std::env::var("PIXELFLUX_NVENC_MAX_API") {
470 let mut it = cap.split('.');
471 if let (Some(a), Some(b)) = (it.next(), it.next())
472 && let (Ok(cm), Ok(cn)) = (a.parse::<u32>(), b.parse::<u32>())
473 {
474 let capv = (cm << 4) | (cn & 0xF);
475 if capv != 0 && (drv_max == 0 || capv < drv_max) {
476 drv_max = capv;
477 }
478 }
479 }
480 let candidates = [pinned, (12, 1), (12, 0), (11, 1), (11, 0), (10, 0)];
481 for (maj, min) in candidates {
482 let vv = (maj << 4) | min;
483 if drv_max != 0 && vv > drv_max {
484 continue;
485 }
486 let mut probe = NV_ENCODE_API_FUNCTION_LIST {
487 version: nvenc_struct_ver(NvStruct::FunctionList, maj, min),
488 ..Default::default()
489 };
490 let st = unsafe { (lib.create_instance)(&mut probe) };
491 if st == NVENCSTATUS::NV_ENC_SUCCESS
492 && probe.nvEncOpenEncodeSessionEx.is_some()
493 && probe.nvEncInitializeEncoder.is_some()
494 && probe.nvEncGetEncodePresetConfigEx.is_some()
495 && probe.nvEncEncodePicture.is_some()
496 && probe.nvEncLockBitstream.is_some()
497 {
498 crate::log::debug!("[pixelflux] NVENC API version negotiated: {}.{}", maj, min);
499 return (maj, min);
500 }
501 }
502 pinned
503 });
504}
505
506struct CachedDmaBuf {
511 identity: DmaBufIdentity,
512 egl_image: EGLImageKHR,
513 cuda_resource: CUgraphicsResource,
514 egl_frame: CUeglFrame,
515 input: DmaBufInput,
516 tex: CUtexObject,
519}
520
521#[derive(Clone, Copy)]
529struct ExternalInput {
530 device_ptr: CUdeviceptr,
531 pitch: usize,
532 width: u32,
533 height: u32,
534 format: NV_ENC_BUFFER_FORMAT,
535 registered: NV_ENC_REGISTERED_PTR,
536 mapped: NV_ENC_INPUT_PTR,
537}
538
539#[derive(Clone, Copy)]
547enum DmaBufInput {
548 Direct {
549 registered: NV_ENC_REGISTERED_PTR,
550 mapped: NV_ENC_INPUT_PTR,
551 format: NV_ENC_BUFFER_FORMAT,
552 },
553 Copy,
554}
555
556fn egl_import_fourcc(code: Fourcc) -> Fourcc {
563 match code {
564 Fourcc::Xrgb8888 => Fourcc::Argb8888,
565 Fourcc::Xbgr8888 => Fourcc::Abgr8888,
566 other => other,
567 }
568}
569
570fn fourcc_nvenc_format(code: Fourcc) -> Option<NV_ENC_BUFFER_FORMAT> {
571 match code {
572 Fourcc::Argb8888 | Fourcc::Xrgb8888 => {
573 Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB)
574 }
575 Fourcc::Abgr8888 | Fourcc::Xbgr8888 => {
576 Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR)
577 }
578 _ => None,
579 }
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
585enum DirectPlane {
586 Pitch(u32),
588 Array(u32),
592}
593
594fn direct_plane(frame: &CUeglFrame, width: u32, height: u32) -> Option<DirectPlane> {
602 if frame.plane_count < 1 || width == 0 || height == 0 {
603 return None;
604 }
605 if frame.width < width || frame.height < height {
606 return None;
607 }
608 match frame.frame_type {
609 CU_EGL_FRAME_TYPE_PITCH => {
610 let plane = unsafe { frame.frame.p_pitch[0] };
611 let pitch_ok = frame.pitch >= width.saturating_mul(4) && frame.pitch.is_multiple_of(4);
612 (!plane.is_null() && pitch_ok).then_some(DirectPlane::Pitch(frame.pitch))
613 }
614 CU_EGL_FRAME_TYPE_ARRAY => {
615 let array = unsafe { frame.frame.p_array[0] };
616 let packed_8bit = frame.cu_format == CU_AD_FORMAT_U8 && frame.num_channels == 4;
617 (!array.is_null() && packed_8bit).then_some(DirectPlane::Array(frame.width * 4))
618 }
619 _ => None,
620 }
621}
622
623#[derive(Debug, Clone, Copy, PartialEq, Eq)]
633struct DmaBufIdentity {
634 dev: u64,
635 ino: u64,
636 size: i64,
637 modifier: u64,
638 width: u32,
639 height: u32,
640}
641
642impl DmaBufIdentity {
643 fn probe(fd: i32, modifier: u64, width: u32, height: u32) -> Self {
647 let mut st: libc::stat = unsafe { std::mem::zeroed() };
648 let (dev, ino, size) = if unsafe { libc::fstat(fd, &mut st) } == 0 {
649 (st.st_dev as u64, st.st_ino as u64, st.st_size as i64)
650 } else {
651 (0, 0, 0)
652 };
653 Self {
654 dev,
655 ino,
656 size,
657 modifier,
658 width,
659 height,
660 }
661 }
662}
663
664#[derive(Debug, Clone, Copy, PartialEq, Eq)]
676struct CapsDecision {
677 fullcolor: bool,
678 downgraded_color: bool,
679 too_large: Option<(i32, i32)>,
680}
681
682fn decide_caps(
684 req_fullcolor: bool,
685 req_w: i32,
686 req_h: i32,
687 cap_yuv444: Option<i32>,
688 cap_width_max: Option<i32>,
689 cap_height_max: Option<i32>,
690) -> CapsDecision {
691 let downgraded_color = req_fullcolor && cap_yuv444 == Some(0);
692 let fullcolor = req_fullcolor && !downgraded_color;
693 let exceeds = |req: i32, cap: Option<i32>| cap.is_some_and(|m| m > 0 && req > m);
694 let too_large = if exceeds(req_w, cap_width_max) || exceeds(req_h, cap_height_max) {
695 Some((cap_width_max.unwrap_or(0), cap_height_max.unwrap_or(0)))
696 } else {
697 None
698 };
699 CapsDecision {
700 fullcolor,
701 downgraded_color,
702 too_large,
703 }
704}
705
706const HEADROOM_WIDTH: u32 = 4096;
711const HEADROOM_HEIGHT: u32 = 2160;
712
713const HEADROOM_FPS: u32 = 60;
716
717fn window_frames(codec: Codec, dpb: u32, anchored: bool) -> u32 {
723 dpb + u32::from(codec == Codec::H265 && anchored)
724}
725
726fn anchor_count(
737 codec: Codec,
738 api_major: u32,
739 invalidation: bool,
740 ltr: Option<i32>,
741 dpb: u32,
742 acknowledged: bool,
743) -> usize {
744 let fits =
745 |n: usize| invalidation && ltr.is_some_and(|l| l >= n as i32) && dpb as usize >= n + 2;
746 let count = match codec {
747 Codec::Av1 if api_major < 13 => 0,
748 Codec::H265 => ANCHORS,
749 Codec::Av1 if acknowledged && fits(ANCHORS) => ANCHORS,
750 _ => 1,
751 };
752 if fits(count) { count } else { 0 }
753}
754
755fn nvenc_headroom(size: u32, floor: u32, cap: Option<i32>) -> u32 {
759 let want = size.max(floor);
760 match cap {
761 Some(m) if m > 0 => want.min(m as u32),
762 _ => want,
763 }
764}
765
766const NVENC_AV1_RATE: (u64, u64) = (2, 3);
769
770static NVENC_AV1_WEIGHTED: AtomicBool = AtomicBool::new(false);
774
775fn level_rate(codec: Codec, bps: u64, weighted: bool) -> u64 {
778 if codec == Codec::Av1 && weighted {
779 (bps * NVENC_AV1_RATE.1).div_ceil(NVENC_AV1_RATE.0)
780 } else {
781 bps
782 }
783}
784
785fn nvenc_rate_ceiling(codec: Codec) -> u32 {
790 match codec {
791 Codec::Av1 => (160_000_000 * NVENC_AV1_RATE.0 / NVENC_AV1_RATE.1) as u32,
792 Codec::H265 => 800_000_000,
793 _ => 1_000_000_000,
794 }
795}
796
797fn split_mode(
812 codec: Codec,
813 width: u32,
814 height: u32,
815 engines: Option<i32>,
816 api: (u32, u32),
817) -> NV_ENC_SPLIT_ENCODE_MODE {
818 let forced = api >= (12, 1)
819 && engines.is_some_and(|n| n > 1)
820 && match codec {
821 Codec::Av1 => true,
822 Codec::H265 => width as u64 * height as u64 >= 3840 * 2160,
823 _ => false,
824 };
825 match (forced, codec, engines) {
826 (true, Codec::Av1, Some(3)) => NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_THREE_FORCED_MODE,
827 (true, ..) => NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_AUTO_FORCED_MODE,
828 _ => NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_AUTO_MODE,
829 }
830}
831
832fn cbr_bps(settings: &RustCaptureSettings) -> u32 {
835 (settings.video_bitrate_kbps.max(0) as u32)
836 .saturating_mul(1000)
837 .min(nvenc_rate_ceiling(settings.codec))
838}
839
840fn log_held_rate(settings: &RustCaptureSettings, bps: u32) {
842 let asked = settings.video_bitrate_kbps.max(0) as u64 * 1000;
843 if (bps as u64) < asked {
844 eprintln!(
845 "[NVENC] {} opens at {} kbps at most, its top level's ceiling: {} kbps is held there",
846 settings.codec.display(),
847 bps / 1000,
848 asked / 1000
849 );
850 }
851}
852
853fn cbr_vbv(settings: &RustCaptureSettings, bps: u32) -> u32 {
856 crate::encoders::vbv_bits(
857 bps,
858 settings.target_fps,
859 settings.keyframe_interval_s,
860 settings.video_vbv_multiplier,
861 )
862}
863
864fn set_cbr_rate(rc: &mut NV_ENC_RC_PARAMS, bps: u32, vbv: u32) {
869 rc.averageBitRate = bps;
870 rc.maxBitRate = bps;
871 rc.vbvBufferSize = vbv;
872 rc.vbvInitialDelay = vbv;
873}
874
875fn fill_band_map(
880 map: &mut Vec<i8>,
881 codec: Codec,
882 width: u32,
883 height: u32,
884 (from, to): (f64, f64),
885 delta: i32,
886) {
887 let block = match codec {
888 Codec::H264 => 16,
889 Codec::H265 => 32,
890 _ => 64,
891 };
892 let blocks = (width.div_ceil(block) * height.div_ceil(block)) as usize;
893 let first = ((from.clamp(0.0, 1.0) * blocks as f64).floor() as usize).min(blocks);
894 let last = ((to.clamp(0.0, 1.0) * blocks as f64).ceil() as usize).clamp(first, blocks);
895 map.clear();
896 map.resize(blocks, 0);
897 map[first..last].fill(delta.clamp(-(codec.quantizer_max().min(128) as i32), 0) as i8);
898}
899
900unsafe fn last_error(funcs: &NV_ENCODE_API_FUNCTION_LIST, session: *mut c_void) -> String {
904 funcs
905 .nvEncGetLastErrorString
906 .map(|f| f(session))
907 .filter(|p| !p.is_null())
908 .map(|p| CStr::from_ptr(p).to_string_lossy().into_owned())
909 .filter(|s| !s.is_empty())
910 .unwrap_or_else(|| "no error string".to_string())
911}
912
913unsafe fn query_cap(
917 funcs: &NV_ENCODE_API_FUNCTION_LIST,
918 session: *mut c_void,
919 codec: GUID,
920 cap: NV_ENC_CAPS,
921) -> Option<i32> {
922 let get = funcs.nvEncGetEncodeCaps?;
923 let mut param = NV_ENC_CAPS_PARAM {
924 version: sv(NvStruct::CapsParam),
925 capsToQuery: cap,
926 reserved: [0u32; 62],
927 };
928 let mut val: i32 = 0;
929 if get(session, codec, &mut param, &mut val) == NVENCSTATUS::NV_ENC_SUCCESS {
930 Some(val)
931 } else {
932 None
933 }
934}
935
936const NV_ENC_H264_PROFILE_HIGH_GUID: GUID = GUID {
938 Data1: 0x205b553d,
939 Data2: 0x5f01,
940 Data3: 0x4d9e,
941 Data4: [0x91, 0x84, 0xda, 0x32, 0x77, 0x5b, 0x55, 0x9b],
942};
943
944const NV_ENC_H264_PROFILE_HIGH_444_GUID: GUID = GUID {
946 Data1: 0x7ac663cb,
947 Data2: 0xa598,
948 Data3: 0x4960,
949 Data4: [0xb8, 0x44, 0x33, 0x9b, 0x26, 0x1a, 0x7d, 0x5c],
950};
951
952fn guid_eq(a: &GUID, b: &GUID) -> bool {
954 a.Data1 == b.Data1 && a.Data2 == b.Data2 && a.Data3 == b.Data3 && a.Data4 == b.Data4
955}
956
957fn codec_guid(codec: Codec) -> Option<GUID> {
960 match codec {
961 Codec::H264 => Some(NV_ENC_CODEC_H264_GUID),
962 Codec::H265 => Some(NV_ENC_CODEC_HEVC_GUID),
963 Codec::Av1 => Some(NV_ENC_CODEC_AV1_GUID),
964 Codec::Jpeg | Codec::Vp8 | Codec::Vp9 => None,
965 }
966}
967
968pub(crate) fn probe_codecs(encode_node_index: i32) -> Result<Vec<(Codec, super::Formats)>, String> {
977 let cuda = std::mem::ManuallyDrop::new(NvencEncoder::load_cuda()?);
978 let nvenc_lib = std::mem::ManuallyDrop::new(NvencEncoder::load_nvenc()?);
979 nvenc_negotiate(&nvenc_lib);
980 crate::nvgpufilter::install();
981 unsafe {
982 let res = (cuda.cuInit)(0);
983 if res != CUresult::CUDA_SUCCESS {
984 return Err(format!(
985 "Init CUDA failed: {}",
986 NvencEncoder::get_error_string(&cuda, res)
987 ));
988 }
989 let mut cu_device: CUdevice = 0;
990 let bound = NvencEncoder::get_pci_bus_id(encode_node_index.max(0))
991 .and_then(|id| CString::new(id).ok())
992 .is_some_and(|id| {
993 (cuda.cuDeviceGetByPCIBusId)(&mut cu_device, id.as_ptr()) == CUresult::CUDA_SUCCESS
994 });
995 if !bound && (cuda.cuDeviceGet)(&mut cu_device, 0) != CUresult::CUDA_SUCCESS {
996 return Err("Failed to get default CUDA device".into());
997 }
998 let mut cu_context: CUcontext = ptr::null_mut();
999 if (cuda.cuDevicePrimaryCtxRetain)(&mut cu_context, cu_device) != CUresult::CUDA_SUCCESS {
1000 return Err("Failed to retain the device's primary CUDA context".into());
1001 }
1002 if (cuda.cuCtxPushCurrent_v2)(cu_context) != CUresult::CUDA_SUCCESS {
1003 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1004 return Err("Failed to make the primary CUDA context current".into());
1005 }
1006 let result = probe_session_codecs(&nvenc_lib, cu_context);
1007 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1008 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1009 result
1010 }
1011}
1012
1013pub(crate) const SESSIONS_TAKEN: &str = "the device has no NVENC session to spare";
1016
1017fn session_refusal(status: NVENCSTATUS) -> String {
1021 match status {
1022 NVENCSTATUS::NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY | NVENCSTATUS::NV_ENC_ERR_OUT_OF_MEMORY => {
1023 SESSIONS_TAKEN.into()
1024 }
1025 _ => "Failed to open NVENC session".into(),
1026 }
1027}
1028
1029unsafe fn codes_ten_bit(
1034 function_list: &NV_ENCODE_API_FUNCTION_LIST,
1035 session: *mut c_void,
1036 codec: Codec,
1037 guid: GUID,
1038) -> bool {
1039 let (major, minor) = nvenc_cur_ver();
1040 matches!(codec, Codec::H265 | Codec::Av1)
1041 && (major << 4) | minor >= 0xC2
1042 && query_cap(
1043 function_list,
1044 session,
1045 guid,
1046 NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_10BIT_ENCODE,
1047 ) == Some(1)
1048}
1049
1050unsafe fn probe_session_codecs(
1053 nvenc_lib: &NvencLibrary,
1054 cu_context: CUcontext,
1055) -> Result<Vec<(Codec, super::Formats)>, String> {
1056 let mut function_list = NV_ENCODE_API_FUNCTION_LIST {
1057 version: sv(NvStruct::FunctionList),
1058 ..Default::default()
1059 };
1060 if (nvenc_lib.create_instance)(&mut function_list) != NVENCSTATUS::NV_ENC_SUCCESS {
1061 return Err("NvEncodeAPICreateInstance failed".into());
1062 }
1063 let (Some(open_fn), Some(destroy_fn)) = (
1064 function_list.nvEncOpenEncodeSessionEx,
1065 function_list.nvEncDestroyEncoder,
1066 ) else {
1067 return Err("the driver's NVENC function list has no session entry points".into());
1068 };
1069 let mut session_params = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
1070 version: sv(NvStruct::OpenSessionExParams),
1071 deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
1072 device: cu_context as *mut c_void,
1073 apiVersion: neg_api(),
1074 ..Default::default()
1075 };
1076 let mut session: *mut c_void = ptr::null_mut();
1077 match open_fn(&mut session_params, &mut session) {
1078 NVENCSTATUS::NV_ENC_SUCCESS => {}
1079 status => return Err(session_refusal(status)),
1080 }
1081 let codecs = Codec::VIDEO
1082 .into_iter()
1083 .filter_map(|codec| {
1084 let guid = codec_guid(codec)
1085 .filter(|guid| NvencEncoder::device_encodes(&function_list, session, guid))?;
1086 let fullcolor = codec.fullcolor()
1087 && query_cap(
1088 &function_list,
1089 session,
1090 guid,
1091 NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE,
1092 ) == Some(1);
1093 let ten_bit = codes_ten_bit(&function_list, session, codec, guid);
1094 Some((
1095 codec,
1096 super::Formats {
1097 fullcolor,
1098 ten_bit: [ten_bit, ten_bit && fullcolor],
1099 },
1100 ))
1101 })
1102 .collect();
1103 destroy_fn(session);
1104 Ok(codecs)
1105}
1106
1107const SLICE_MODE_COUNT: u32 = 3;
1110
1111const SLICES_PER_FRAME: u32 = 4;
1117
1118const AV1_REFERENCES: u32 = 4;
1123
1124const BITSTREAM_BUFFERS: usize = 1;
1130
1131#[derive(Clone, Copy, Debug)]
1145pub(crate) struct NvencTuning {
1146 pub preset: GUID,
1147 pub multipass: NV_ENC_MULTI_PASS,
1148 pub spatial_aq: bool,
1149 pub temporal_aq: bool,
1153 pub hevc_high_tier: bool,
1157 pub slices: u32,
1160 #[cfg(test)]
1164 pub hardware_csc: bool,
1165 #[cfg(test)]
1168 pub ref_l0: Option<NV_ENC_NUM_REF_FRAMES>,
1169 #[cfg(test)]
1171 pub split: Option<NV_ENC_SPLIT_ENCODE_MODE>,
1172}
1173
1174impl Default for NvencTuning {
1175 fn default() -> Self {
1176 Self {
1177 preset: NV_ENC_PRESET_P3_GUID,
1178 multipass: NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
1179 spatial_aq: false,
1180 temporal_aq: false,
1181 hevc_high_tier: true,
1182 slices: SLICES_PER_FRAME,
1183 #[cfg(test)]
1184 hardware_csc: false,
1185 #[cfg(test)]
1186 ref_l0: None,
1187 #[cfg(test)]
1188 split: None,
1189 }
1190 }
1191}
1192
1193fn profile_guid(codec: Codec, fullcolor: bool, bit_depth: u32) -> GUID {
1197 match (codec, fullcolor) {
1198 (Codec::H264, true) => NV_ENC_H264_PROFILE_HIGH_444_GUID,
1199 (Codec::H264, false) => NV_ENC_H264_PROFILE_HIGH_GUID,
1200 (Codec::H265, true) => NV_ENC_HEVC_PROFILE_FREXT_GUID,
1201 (Codec::H265, false) if bit_depth > 8 => NV_ENC_HEVC_PROFILE_MAIN10_GUID,
1202 (Codec::H265, false) => NV_ENC_HEVC_PROFILE_MAIN_GUID,
1203 _ => NV_ENC_AV1_PROFILE_MAIN_GUID,
1204 }
1205}
1206
1207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1225enum ConvertLayout {
1226 Nv12,
1227 P010,
1228 Yuv444P10,
1229}
1230
1231impl ConvertLayout {
1232 fn of(fullcolor: bool, bit_depth: u32) -> Option<Self> {
1235 match (fullcolor, bit_depth > 8) {
1236 (false, false) => Some(Self::Nv12),
1237 (false, true) => Some(Self::P010),
1238 (true, true) => Some(Self::Yuv444P10),
1239 (true, false) => None,
1240 }
1241 }
1242
1243 fn kernels(self) -> (&'static CStr, &'static CStr) {
1245 match self {
1246 Self::Nv12 => (c"argb_to_nv12", c"argb_tex_to_nv12"),
1247 Self::P010 => (c"argb_to_p010", c"argb_tex_to_p010"),
1248 Self::Yuv444P10 => (c"argb_to_yuv444p10", c"argb_tex_to_yuv444p10"),
1249 }
1250 }
1251
1252 fn extent(self, width: u32, height: u32) -> (usize, usize) {
1255 let (width, height) = (width as usize, height as usize);
1256 match self {
1257 Self::Nv12 => (width, height + height.div_ceil(2)),
1258 Self::P010 => (2 * width, height + height.div_ceil(2)),
1259 Self::Yuv444P10 => (2 * width, 3 * height),
1260 }
1261 }
1262
1263 fn format(self) -> NV_ENC_BUFFER_FORMAT {
1264 match self {
1265 Self::Nv12 => NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_NV12,
1266 Self::P010 => NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV420_10BIT,
1267 Self::Yuv444P10 => NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444_10BIT,
1268 }
1269 }
1270}
1271
1272struct ChromaConvert {
1273 module: CUmodule,
1274 kernel: CUfunction,
1275 kernel_tex: CUfunction,
1276 layout: ConvertLayout,
1277 surface: CUdeviceptr,
1278 pitch: usize,
1279 registered: NV_ENC_REGISTERED_PTR,
1280 mapped: NV_ENC_INPUT_PTR,
1281}
1282
1283impl ChromaConvert {
1284 unsafe fn new(
1289 cuda: &CudaFunctions,
1290 funcs: &NV_ENCODE_API_FUNCTION_LIST,
1291 session: *mut c_void,
1292 width: u32,
1293 height: u32,
1294 layout: ConvertLayout,
1295 ) -> Option<Self> {
1296 let module = load_kernels(cuda)?;
1297 let mut kernel: CUfunction = ptr::null_mut();
1298 let mut kernel_tex: CUfunction = ptr::null_mut();
1299 let (linear, texture) = layout.kernels();
1300 if (cuda.cuModuleGetFunction)(&mut kernel, module, linear.as_ptr())
1301 != CUresult::CUDA_SUCCESS
1302 || (cuda.cuModuleGetFunction)(&mut kernel_tex, module, texture.as_ptr())
1303 != CUresult::CUDA_SUCCESS
1304 {
1305 (cuda.cuModuleUnload)(module);
1306 return None;
1307 }
1308 let (mut nv12, mut pitch): (CUdeviceptr, usize) = (0, 0);
1309 let (row_bytes, rows) = layout.extent(width, height);
1314 if (cuda.cuMemAllocPitch_v2)(&mut nv12, &mut pitch, row_bytes, rows, 4)
1315 != CUresult::CUDA_SUCCESS
1316 {
1317 (cuda.cuModuleUnload)(module);
1318 return None;
1319 }
1320 let mut reg = NV_ENC_REGISTER_RESOURCE {
1321 version: sv(NvStruct::RegisterResource),
1322 resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
1323 width,
1324 height,
1325 resourceToRegister: nv12 as *mut c_void,
1326 pitch: pitch as u32,
1327 bufferFormat: layout.format(),
1328 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
1329 ..Default::default()
1330 };
1331 if (funcs.nvEncRegisterResource.unwrap())(session, &mut reg) != NVENCSTATUS::NV_ENC_SUCCESS
1332 {
1333 (cuda.cuMemFree_v2)(nv12);
1334 (cuda.cuModuleUnload)(module);
1335 return None;
1336 }
1337 let mut map = NV_ENC_MAP_INPUT_RESOURCE {
1338 version: sv(NvStruct::MapInputResource),
1339 registeredResource: reg.registeredResource,
1340 ..Default::default()
1341 };
1342 if (funcs.nvEncMapInputResource.unwrap())(session, &mut map) != NVENCSTATUS::NV_ENC_SUCCESS
1343 {
1344 (funcs.nvEncUnregisterResource.unwrap())(session, reg.registeredResource);
1345 (cuda.cuMemFree_v2)(nv12);
1346 (cuda.cuModuleUnload)(module);
1347 return None;
1348 }
1349 Some(ChromaConvert {
1350 module,
1351 kernel,
1352 kernel_tex,
1353 layout,
1354 surface: nv12,
1355 pitch,
1356 registered: reg.registeredResource,
1357 mapped: map.mappedResource,
1358 })
1359 }
1360
1361 fn input(&self) -> (NV_ENC_INPUT_PTR, NV_ENC_BUFFER_FORMAT) {
1363 (self.mapped, self.layout.format())
1364 }
1365
1366 unsafe fn run(
1370 &self,
1371 cuda: &CudaFunctions,
1372 src: CUdeviceptr,
1373 src_pitch: usize,
1374 width: u32,
1375 height: u32,
1376 swap_rb: bool,
1377 ) -> Result<(), String> {
1378 let (mut src, mut sp) = (src, src_pitch as i32);
1379 let (mut dst, mut dp) = (self.surface, self.pitch as i32);
1380 let (mut w, mut h, mut swap) = (width as i32, height as i32, i32::from(swap_rb));
1381 let mut params: [*mut c_void; 7] = [
1382 &mut src as *mut _ as *mut c_void,
1383 &mut sp as *mut _ as *mut c_void,
1384 &mut dst as *mut _ as *mut c_void,
1385 &mut dp as *mut _ as *mut c_void,
1386 &mut w as *mut _ as *mut c_void,
1387 &mut h as *mut _ as *mut c_void,
1388 &mut swap as *mut _ as *mut c_void,
1389 ];
1390 self.launch(cuda, self.kernel, &mut params, width, height)
1391 }
1392
1393 unsafe fn run_texture(
1396 &self,
1397 cuda: &CudaFunctions,
1398 tex: CUtexObject,
1399 width: u32,
1400 height: u32,
1401 swap_rb: bool,
1402 ) -> Result<(), String> {
1403 let mut tex = tex;
1404 let (mut dst, mut dp) = (self.surface, self.pitch as i32);
1405 let (mut w, mut h, mut swap) = (width as i32, height as i32, i32::from(swap_rb));
1406 let mut params: [*mut c_void; 6] = [
1407 &mut tex as *mut _ as *mut c_void,
1408 &mut dst as *mut _ as *mut c_void,
1409 &mut dp as *mut _ as *mut c_void,
1410 &mut w as *mut _ as *mut c_void,
1411 &mut h as *mut _ as *mut c_void,
1412 &mut swap as *mut _ as *mut c_void,
1413 ];
1414 self.launch(cuda, self.kernel_tex, &mut params, width, height)
1415 }
1416
1417 unsafe fn launch(
1420 &self,
1421 cuda: &CudaFunctions,
1422 kernel: CUfunction,
1423 params: &mut [*mut c_void],
1424 width: u32,
1425 height: u32,
1426 ) -> Result<(), String> {
1427 const BLOCK: u32 = 16;
1428 let grid = (
1429 width.div_ceil(2).div_ceil(BLOCK),
1430 height.div_ceil(2).div_ceil(BLOCK),
1431 );
1432 if (cuda.cuLaunchKernel)(
1433 kernel,
1434 grid.0,
1435 grid.1,
1436 1,
1437 BLOCK,
1438 BLOCK,
1439 1,
1440 0,
1441 ptr::null_mut(),
1442 params.as_mut_ptr(),
1443 ptr::null_mut(),
1444 ) != CUresult::CUDA_SUCCESS
1445 {
1446 return Err("the chroma convert kernel failed to launch".into());
1447 }
1448 Ok(())
1449 }
1450
1451 unsafe fn texture_for(cuda: &CudaFunctions, array: CUarray) -> CUtexObject {
1454 let mut res: CUDA_RESOURCE_DESC = std::mem::zeroed();
1455 res.resType = CUresourcetype::CU_RESOURCE_TYPE_ARRAY;
1456 res.res.array.hArray = array;
1457 let mut sampling: CUDA_TEXTURE_DESC = std::mem::zeroed();
1458 sampling.addressMode = [CUaddress_mode::CU_TR_ADDRESS_MODE_CLAMP; 3];
1459 sampling.filterMode = CUfilter_mode::CU_TR_FILTER_MODE_POINT;
1460 sampling.flags = CU_TRSF_READ_AS_INTEGER;
1461 let mut tex: CUtexObject = 0;
1462 if (cuda.cuTexObjectCreate)(&mut tex, &res, &sampling, ptr::null())
1463 != CUresult::CUDA_SUCCESS
1464 {
1465 return 0;
1466 }
1467 tex
1468 }
1469
1470 unsafe fn release(
1472 &self,
1473 cuda: &CudaFunctions,
1474 funcs: &NV_ENCODE_API_FUNCTION_LIST,
1475 session: *mut c_void,
1476 ) {
1477 (funcs.nvEncUnmapInputResource.unwrap())(session, self.mapped);
1478 (funcs.nvEncUnregisterResource.unwrap())(session, self.registered);
1479 (cuda.cuMemFree_v2)(self.surface);
1480 (cuda.cuModuleUnload)(self.module);
1481 }
1482}
1483
1484struct BandHash {
1487 module: CUmodule,
1488 kernel: CUfunction,
1489 sums: CUdeviceptr,
1490 capacity: usize,
1491}
1492
1493const HASH_THREADS: u32 = 256;
1498const HASH_SEGMENTS: u32 = 4;
1499
1500impl BandHash {
1501 unsafe fn new(cuda: &CudaFunctions) -> Option<Self> {
1502 let module = load_kernels(cuda)?;
1503 let mut kernel: CUfunction = ptr::null_mut();
1504 if (cuda.cuModuleGetFunction)(&mut kernel, module, c"band_hash".as_ptr())
1505 != CUresult::CUDA_SUCCESS
1506 {
1507 (cuda.cuModuleUnload)(module);
1508 return None;
1509 }
1510 Some(Self {
1511 module,
1512 kernel,
1513 sums: 0,
1514 capacity: 0,
1515 })
1516 }
1517
1518 unsafe fn run(
1521 &mut self,
1522 cuda: &CudaFunctions,
1523 src: CUdeviceptr,
1524 pitch: usize,
1525 width: u32,
1526 height: u32,
1527 rows: u32,
1528 ) -> Option<Vec<u64>> {
1529 let words = self.launch(cuda, src, pitch, width, height, rows)?;
1530 self.read(cuda, words)
1531 }
1532
1533 unsafe fn launch(
1536 &mut self,
1537 cuda: &CudaFunctions,
1538 src: CUdeviceptr,
1539 pitch: usize,
1540 width: u32,
1541 height: u32,
1542 rows: u32,
1543 ) -> Option<usize> {
1544 let bands = height.div_ceil(rows.max(1)) as usize;
1545 let words = bands * HASH_SEGMENTS as usize;
1546 if words > self.capacity {
1547 if self.sums != 0 {
1548 (cuda.cuMemFree_v2)(self.sums);
1549 }
1550 self.capacity = 0;
1551 if (cuda.cuMemAlloc_v2)(&mut self.sums, words * 8) != CUresult::CUDA_SUCCESS {
1552 self.sums = 0;
1553 return None;
1554 }
1555 self.capacity = words;
1556 }
1557 let (mut src, mut sp, mut out) = (src, pitch as i32, self.sums);
1558 let (mut w, mut h, mut r) = (width as i32, height as i32, rows.max(1) as i32);
1559 let mut params: [*mut c_void; 6] = [
1560 &mut src as *mut _ as *mut c_void,
1561 &mut sp as *mut _ as *mut c_void,
1562 &mut w as *mut _ as *mut c_void,
1563 &mut h as *mut _ as *mut c_void,
1564 &mut r as *mut _ as *mut c_void,
1565 &mut out as *mut _ as *mut c_void,
1566 ];
1567 ((cuda.cuLaunchKernel)(
1568 self.kernel,
1569 bands as u32,
1570 HASH_SEGMENTS,
1571 1,
1572 HASH_THREADS,
1573 1,
1574 1,
1575 0,
1576 ptr::null_mut(),
1577 params.as_mut_ptr(),
1578 ptr::null_mut(),
1579 ) == CUresult::CUDA_SUCCESS)
1580 .then_some(words)
1581 }
1582
1583 unsafe fn read(&mut self, cuda: &CudaFunctions, words: usize) -> Option<Vec<u64>> {
1585 let mut sums = vec![0u64; words];
1586 ((cuda.cuMemcpyDtoH_v2)(sums.as_mut_ptr() as *mut c_void, self.sums, words * 8)
1587 == CUresult::CUDA_SUCCESS)
1588 .then(|| {
1589 sums.chunks(HASH_SEGMENTS as usize)
1590 .map(|band| band.iter().fold(0u64, |a, &b| a.wrapping_add(b)))
1591 .collect()
1592 })
1593 }
1594
1595 unsafe fn release(self, cuda: &CudaFunctions) {
1596 if self.sums != 0 {
1597 (cuda.cuMemFree_v2)(self.sums);
1598 }
1599 (cuda.cuModuleUnload)(self.module);
1600 }
1601}
1602
1603pub struct NvencEncoder {
1629 encoder_session: *mut c_void,
1630 cuda_context: CUcontext,
1631 cuda_device: CUdevice,
1632 device_name: String,
1634 egl_display: EGLDisplay,
1635 codec: Codec,
1636 fullcolor: bool,
1637 bit_depth: u32,
1639 width: u32,
1640 height: u32,
1641 current_qp: u32,
1642 held_qp: Option<u32>,
1646 held_band: Option<(f64, f64)>,
1647 hold_refused: bool,
1648 held_bytes: usize,
1650 qp_map: Vec<i8>,
1651 last_quality: Option<u32>,
1654 last_bytes: Option<usize>,
1656 encode_config: NV_ENC_CONFIG,
1657 init_params: NV_ENC_INITIALIZE_PARAMS,
1658 input_device_ptr: CUdeviceptr,
1659 input_pitch: usize,
1660 input_format: NV_ENC_BUFFER_FORMAT,
1661 registered_input_resource: NV_ENC_REGISTERED_PTR,
1662 mapped_input_buffer: NV_ENC_INPUT_PTR,
1663 bitstream_buffers: Vec<NV_ENC_OUTPUT_PTR>,
1664 current_buffer_idx: usize,
1665 dmabuf_cache: HashMap<i32, CachedDmaBuf>,
1666 external_input: Option<ExternalInput>,
1667 pinned_hosts: HashMap<usize, usize>,
1668 cuda: Arc<CudaFunctions>,
1669 egl: Option<Arc<EglFunctions>>,
1671 _nvenc_lib: Arc<NvencLibrary>,
1672 nvenc_funcs: NV_ENCODE_API_FUNCTION_LIST,
1673 omit_stripe_headers: bool,
1674 node_index: i32,
1675 pin_uploads: bool,
1678 direct_dmabuf: bool,
1681 csc: Option<ChromaConvert>,
1684 dpb: u32,
1687 engines: Option<i32>,
1689 references: Option<ReferenceWindow>,
1693 last_reference: Reference,
1694 frame_num: WideFrameNum,
1698 blit_semaphore: CUexternalSemaphore,
1701 band_hash: Option<Option<BandHash>>,
1704 upload_hash: Option<u32>,
1708 upload_words: Option<usize>,
1709 upload_hashes: Option<Vec<u64>>,
1710}
1711
1712unsafe impl Send for NvencEncoder {}
1713
1714impl Drop for NvencEncoder {
1731 fn drop(&mut self) {
1732 unsafe {
1733 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
1734
1735 self.unmap_external_input();
1736 if let Some(csc) = self.csc.take() {
1737 csc.release(&self.cuda, &self.nvenc_funcs, self.encoder_session);
1738 }
1739 if let Some(Some(hash)) = self.band_hash.take() {
1740 hash.release(&self.cuda);
1741 }
1742 if !self.mapped_input_buffer.is_null() {
1743 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
1744 self.encoder_session,
1745 self.mapped_input_buffer,
1746 );
1747 }
1748 if !self.registered_input_resource.is_null() {
1749 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1750 self.encoder_session,
1751 self.registered_input_resource,
1752 );
1753 }
1754 if self.input_device_ptr != 0 {
1755 (self.cuda.cuMemFree_v2)(self.input_device_ptr);
1756 }
1757
1758 for &bs in &self.bitstream_buffers {
1759 (self.nvenc_funcs.nvEncDestroyBitstreamBuffer.unwrap())(self.encoder_session, bs);
1760 }
1761
1762 let imports: Vec<CachedDmaBuf> = self.dmabuf_cache.drain().map(|(_, c)| c).collect();
1763 for cache in imports {
1764 self.release_dmabuf_import(cache);
1765 }
1766
1767 for (base, len) in &self.pinned_hosts {
1768 if *len > 0 {
1769 (self.cuda.cuMemHostUnregister)(*base as *mut c_void);
1770 }
1771 }
1772
1773 if !self.blit_semaphore.is_null()
1774 && let Some(destroy) = self.cuda.cuDestroyExternalSemaphore
1775 {
1776 destroy(self.blit_semaphore);
1777 }
1778
1779 if !self.encoder_session.is_null() {
1780 (self.nvenc_funcs.nvEncDestroyEncoder.unwrap())(self.encoder_session);
1781 }
1782
1783 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1784 (self.cuda.cuDevicePrimaryCtxRelease_v2)(self.cuda_device);
1785 }
1786 }
1787}
1788
1789fn keep_primary_context(device: CUdevice) {
1796 static KEPT: std::sync::Mutex<Vec<CUdevice>> = std::sync::Mutex::new(Vec::new());
1797 static CUDA: std::sync::OnceLock<Option<CudaFunctions>> = std::sync::OnceLock::new();
1798 let mut kept = KEPT.lock().unwrap_or_else(|e| e.into_inner());
1799 if kept.contains(&device) {
1800 return;
1801 }
1802 let Some(cuda) = CUDA.get_or_init(|| NvencEncoder::load_cuda().ok()) else {
1803 return;
1804 };
1805 let mut context: CUcontext = ptr::null_mut();
1806 if unsafe { (cuda.cuDevicePrimaryCtxRetain)(&mut context, device) } == CUresult::CUDA_SUCCESS {
1807 kept.push(device);
1808 }
1809}
1810
1811fn nvenc_level(
1834 codec: Codec,
1835 width: u32,
1836 height: u32,
1837 fps: u32,
1838 bitrate_bps: u64,
1839 hevc_high_tier: bool,
1840) -> u32 {
1841 let fps = fps.max(HEADROOM_FPS);
1842 match codec {
1843 Codec::Av1 => av1_level(
1844 width.max(HEADROOM_WIDTH),
1845 height.max(HEADROOM_HEIGHT),
1846 fps,
1847 bitrate_bps,
1848 ),
1849 Codec::H265 => h265_level(
1850 width.next_multiple_of(32),
1851 height.next_multiple_of(32),
1852 fps,
1853 bitrate_bps,
1854 hevc_high_tier,
1855 ),
1856 _ => h264_level(width, height, fps, bitrate_bps),
1857 }
1858}
1859
1860impl NvencEncoder {
1861 fn load_egl() -> Result<EglFunctions, String> {
1868 unsafe {
1869 let lib_name = "libEGL.so.1";
1870 let lib = Library::new(lib_name)
1871 .or_else(|_| Library::new("libEGL.so"))
1872 .map_err(|e| format!("Could not load EGL library: {}", e))?;
1873
1874 let get_proc_addr_sym: Symbol<unsafe extern "C" fn(*const c_char) -> *mut c_void> = lib
1875 .get(b"eglGetProcAddress\0")
1876 .map_err(|e| format!("Missing symbol eglGetProcAddress: {}", e))?;
1877
1878 let eglGetProcAddress = *get_proc_addr_sym;
1879
1880 let load_extension = |name: &str| -> Result<*mut c_void, String> {
1881 let c_name = CString::new(name).unwrap();
1882 let addr = eglGetProcAddress(c_name.as_ptr());
1883 if addr.is_null() {
1884 Err(format!("EGL Extension not found: {}", name))
1885 } else {
1886 Ok(addr)
1887 }
1888 };
1889
1890 let create_addr = load_extension("eglCreateImageKHR")?;
1891 let destroy_addr = load_extension("eglDestroyImageKHR")?;
1892
1893 Ok(EglFunctions {
1894 _lib: lib,
1895 eglGetProcAddress,
1896 eglCreateImageKHR: std::mem::transmute::<*mut c_void, EglCreateImageKhrFn>(
1897 create_addr,
1898 ),
1899 eglDestroyImageKHR: std::mem::transmute::<*mut c_void, EglDestroyImageKhrFn>(
1900 destroy_addr,
1901 ),
1902 })
1903 }
1904 }
1905
1906 fn load_cuda() -> Result<CudaFunctions, String> {
1912 unsafe {
1913 let lib_name = if cfg!(windows) {
1914 "nvcuda.dll"
1915 } else {
1916 "libcuda.so.1"
1917 };
1918 let lib = Library::new(lib_name)
1919 .map_err(|e| format!("Could not load CUDA library ({}): {}", lib_name, e))?;
1920
1921 macro_rules! load {
1922 ($lib:expr, $name:expr) => {
1923 *$lib.get($name).map_err(|e| {
1924 format!(
1925 "Missing symbol {}: {}",
1926 std::str::from_utf8($name).unwrap(),
1927 e
1928 )
1929 })?
1930 };
1931 }
1932
1933 Ok(CudaFunctions {
1934 cuInit: load!(lib, b"cuInit\0"),
1935 cuDeviceGet: load!(lib, b"cuDeviceGet\0"),
1936 cuDeviceGetByPCIBusId: load!(lib, b"cuDeviceGetByPCIBusId\0"),
1937 cuDevicePrimaryCtxRetain: load!(lib, b"cuDevicePrimaryCtxRetain\0"),
1938 cuCtxPushCurrent_v2: load!(lib, b"cuCtxPushCurrent_v2\0"),
1939 cuCtxPopCurrent_v2: load!(lib, b"cuCtxPopCurrent_v2\0"),
1940 cuDevicePrimaryCtxRelease_v2: load!(lib, b"cuDevicePrimaryCtxRelease_v2\0"),
1941 cuMemAlloc_v2: load!(lib, b"cuMemAlloc_v2\0"),
1942 cuMemAllocPitch_v2: load!(lib, b"cuMemAllocPitch_v2\0"),
1943 cuMemFree_v2: load!(lib, b"cuMemFree_v2\0"),
1944 cuMemcpyHtoD_v2: load!(lib, b"cuMemcpyHtoD_v2\0"),
1945 cuMemcpyDtoH_v2: load!(lib, b"cuMemcpyDtoH_v2\0"),
1946 cuMemcpy2D_v2: load!(lib, b"cuMemcpy2D_v2\0"),
1947 cuMemcpy2DAsync_v2: load!(lib, b"cuMemcpy2DAsync_v2\0"),
1948 cuStreamSynchronize: load!(lib, b"cuStreamSynchronize\0"),
1949 cuModuleLoadData: load!(lib, b"cuModuleLoadData\0"),
1950 cuModuleGetFunction: load!(lib, b"cuModuleGetFunction\0"),
1951 cuModuleUnload: load!(lib, b"cuModuleUnload\0"),
1952 cuTexObjectCreate: load!(lib, b"cuTexObjectCreate\0"),
1953 cuTexObjectDestroy: load!(lib, b"cuTexObjectDestroy\0"),
1954 cuLaunchKernel: load!(lib, b"cuLaunchKernel\0"),
1955 cuMemHostRegister_v2: load!(lib, b"cuMemHostRegister_v2\0"),
1956 cuMemHostUnregister: load!(lib, b"cuMemHostUnregister\0"),
1957 cuGraphicsEGLRegisterImage: load!(lib, b"cuGraphicsEGLRegisterImage\0"),
1958 cuGraphicsUnregisterResource: load!(lib, b"cuGraphicsUnregisterResource\0"),
1959 cuGraphicsResourceGetMappedEglFrame: load!(
1960 lib,
1961 b"cuGraphicsResourceGetMappedEglFrame\0"
1962 ),
1963 cuDeviceGetCount: load!(lib, b"cuDeviceGetCount\0"),
1964 cuDeviceGetName: load!(lib, b"cuDeviceGetName\0"),
1965 cuDeviceGetUuid: load!(lib, b"cuDeviceGetUuid\0"),
1966 cuGetErrorName: load!(lib, b"cuGetErrorName\0"),
1967 cuImportExternalSemaphore: lib.get(b"cuImportExternalSemaphore\0").ok().map(|s| *s),
1968 cuWaitExternalSemaphoresAsync: lib
1969 .get(b"cuWaitExternalSemaphoresAsync\0")
1970 .ok()
1971 .map(|s| *s),
1972 cuDestroyExternalSemaphore: lib
1973 .get(b"cuDestroyExternalSemaphore\0")
1974 .ok()
1975 .map(|s| *s),
1976 _lib: lib,
1977 })
1978 }
1979 }
1980
1981 fn load_nvenc() -> Result<NvencLibrary, String> {
1987 unsafe {
1988 let lib_name = NVENC_DLL_NAME;
1989 let lib = Library::new(lib_name)
1990 .map_err(|e| format!("Could not load NVENC library ({}): {}", lib_name, e))?;
1991
1992 let create_instance = *lib
1993 .get(NV_ENCODE_API_CREATE_INSTANCE_FN_NAME)
1994 .map_err(|e| e.to_string())?;
1995 let get_max_version = lib
1996 .get::<NvEncodeApiGetMaxSupportedVersionFn>(
1997 NV_ENCODE_API_GET_MAX_SUPPORTED_VERSION_FN_NAME,
1998 )
1999 .map(|s| *s)
2000 .ok();
2001 Ok(NvencLibrary {
2002 create_instance,
2003 get_max_version,
2004 _lib: lib,
2005 })
2006 }
2007 }
2008
2009 unsafe fn get_error_string(cuda: &CudaFunctions, err: CUresult) -> String {
2013 let mut p_str: *const c_char = ptr::null();
2014 if (cuda.cuGetErrorName)(err, &mut p_str) == CUresult::CUDA_SUCCESS && !p_str.is_null() {
2015 CStr::from_ptr(p_str).to_string_lossy().into_owned()
2016 } else {
2017 format!("Unknown CUDA Error ({})", err.0)
2018 }
2019 }
2020
2021 unsafe fn probe_devices(cuda: &CudaFunctions) {
2024 let mut count = 0;
2025 if (cuda.cuDeviceGetCount)(&mut count) != CUresult::CUDA_SUCCESS {
2026 return;
2027 }
2028 crate::log::debug!("[NVENC] Found {} CUDA devices:", count);
2029 for i in 0..count {
2030 let mut dev = 0;
2031 (cuda.cuDeviceGet)(&mut dev, i);
2032 crate::log::debug!(
2033 "[NVENC] Device {}: {}",
2034 i,
2035 Self::device_name_of(cuda, dev)
2036 );
2037 }
2038 }
2039
2040 unsafe fn device_name_of(cuda: &CudaFunctions, dev: CUdevice) -> String {
2042 let mut name_buf = [0 as c_char; 256];
2043 if (cuda.cuDeviceGetName)(name_buf.as_mut_ptr(), 256, dev) != CUresult::CUDA_SUCCESS {
2044 return format!("CUDA device {dev}");
2045 }
2046 CStr::from_ptr(name_buf.as_ptr())
2047 .to_string_lossy()
2048 .into_owned()
2049 }
2050
2051 fn get_pci_bus_id(render_index: i32) -> Option<String> {
2054 let path = format!("/sys/class/drm/renderD{}/device", 128 + render_index);
2055 if let Ok(target) = std::fs::read_link(&path)
2056 && let Some(name) = target.file_name()
2057 && let Some(name_str) = name.to_str()
2058 {
2059 return Some(name_str.to_string());
2060 }
2061 None
2062 }
2063
2064 pub fn new(settings: &RustCaptureSettings, egl_display: *const c_void) -> Result<Self, String> {
2118 Self::new_tuned(settings, egl_display, NvencTuning::default())
2119 }
2120
2121 pub(crate) fn new_tuned(
2124 settings: &RustCaptureSettings,
2125 egl_display: *const c_void,
2126 tuning: NvencTuning,
2127 ) -> Result<Self, String> {
2128 let codec = settings.codec;
2129 let codec_guid =
2130 codec_guid(codec).ok_or_else(|| format!("NVENC has no {} encoder", codec.display()))?;
2131 crate::log::debug!("[NVENC] Initializing {}...", codec.display());
2132
2133 let egl = if egl_display.is_null() {
2134 None
2135 } else {
2136 Some(Arc::new(Self::load_egl()?))
2137 };
2138 let cuda = Arc::new(Self::load_cuda()?);
2139 let nvenc_lib = Arc::new(Self::load_nvenc()?);
2140 nvenc_negotiate(&nvenc_lib);
2141
2142 crate::nvgpufilter::install();
2143
2144 static LEAK_ONCE: std::sync::Once = std::sync::Once::new();
2145 LEAK_ONCE.call_once(|| {
2146 std::mem::forget(egl.clone());
2147 std::mem::forget(cuda.clone());
2148 std::mem::forget(nvenc_lib.clone());
2149 });
2150
2151 unsafe {
2152 let res = (cuda.cuInit)(0);
2153 if res != CUresult::CUDA_SUCCESS {
2154 return Err(format!(
2155 "Init CUDA failed: {}",
2156 Self::get_error_string(&cuda, res)
2157 ));
2158 }
2159
2160 Self::probe_devices(&cuda);
2161
2162 let mut cu_device: CUdevice = 0;
2163 let mut device_found = false;
2164
2165 if let Some(pci_bus_id) = Self::get_pci_bus_id(settings.encode_node_index.max(0)) {
2166 let c_pci_bus_id = CString::new(pci_bus_id.clone()).unwrap();
2167 if (cuda.cuDeviceGetByPCIBusId)(&mut cu_device, c_pci_bus_id.as_ptr())
2168 == CUresult::CUDA_SUCCESS
2169 {
2170 crate::log::debug!(
2171 "[NVENC] Bound to CUDA device via PCI Bus ID: {}",
2172 pci_bus_id
2173 );
2174 device_found = true;
2175 }
2176 }
2177
2178 if !device_found {
2179 let res = (cuda.cuDeviceGet)(&mut cu_device, 0);
2180 if res != CUresult::CUDA_SUCCESS {
2181 return Err("Failed to get default CUDA device".into());
2182 }
2183 }
2184 let device_name = Self::device_name_of(&cuda, cu_device);
2185
2186 let mut cu_context: CUcontext = ptr::null_mut();
2192 let res = (cuda.cuDevicePrimaryCtxRetain)(&mut cu_context, cu_device);
2193 if res != CUresult::CUDA_SUCCESS {
2194 return Err("Failed to retain the device's primary CUDA context".into());
2195 }
2196 if (cuda.cuCtxPushCurrent_v2)(cu_context) != CUresult::CUDA_SUCCESS {
2197 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2198 return Err("Failed to make the primary CUDA context current".into());
2199 }
2200
2201 let width = settings.width as u32;
2202 let height = settings.height as u32;
2203 let mut input_device_ptr: CUdeviceptr = 0;
2204 let mut input_pitch: usize = 0;
2205
2206 let res = (cuda.cuMemAllocPitch_v2)(
2207 &mut input_device_ptr,
2208 &mut input_pitch,
2209 (width * 4) as usize,
2210 height as usize,
2211 16,
2212 );
2213 if res != CUresult::CUDA_SUCCESS {
2214 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2215 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2216 return Err("Failed to allocate ARGB input buffer on GPU".into());
2217 }
2218
2219 let mut function_list = NV_ENCODE_API_FUNCTION_LIST {
2220 version: sv(NvStruct::FunctionList),
2221 ..Default::default()
2222 };
2223 if (nvenc_lib.create_instance)(&mut function_list) != NVENCSTATUS::NV_ENC_SUCCESS {
2224 (cuda.cuMemFree_v2)(input_device_ptr);
2225 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2226 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2227 return Err("NvEncodeAPICreateInstance failed".into());
2228 }
2229
2230 let mut session_params = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
2231 version: sv(NvStruct::OpenSessionExParams),
2232 deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
2233 device: cu_context as *mut c_void,
2234 apiVersion: neg_api(),
2235 ..Default::default()
2236 };
2237
2238 let mut encoder_session: *mut c_void = ptr::null_mut();
2239 let open_fn = function_list.nvEncOpenEncodeSessionEx.unwrap();
2240 let status = open_fn(&mut session_params, &mut encoder_session);
2241 if status != NVENCSTATUS::NV_ENC_SUCCESS {
2242 (cuda.cuMemFree_v2)(input_device_ptr);
2243 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2244 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2245 return Err(session_refusal(status));
2246 }
2247
2248 if !Self::device_encodes(&function_list, encoder_session, &codec_guid) {
2251 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2252 (cuda.cuMemFree_v2)(input_device_ptr);
2253 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2254 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2255 return Err(format!(
2256 "this GPU's NVENC has no {} engine",
2257 codec.display()
2258 ));
2259 }
2260
2261 let caps_444 = if codec.fullcolor() {
2265 query_cap(
2266 &function_list,
2267 encoder_session,
2268 codec_guid,
2269 NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE,
2270 )
2271 } else {
2272 Some(0)
2273 };
2274 let caps_wmax = query_cap(
2275 &function_list,
2276 encoder_session,
2277 codec_guid,
2278 NV_ENC_CAPS::NV_ENC_CAPS_WIDTH_MAX,
2279 );
2280 let caps_hmax = query_cap(
2281 &function_list,
2282 encoder_session,
2283 codec_guid,
2284 NV_ENC_CAPS::NV_ENC_CAPS_HEIGHT_MAX,
2285 );
2286 let caps = decide_caps(
2287 settings.video_fullcolor,
2288 width as i32,
2289 height as i32,
2290 caps_444,
2291 caps_wmax,
2292 caps_hmax,
2293 );
2294 if let Some((mw, mh)) = caps.too_large {
2295 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2296 (cuda.cuMemFree_v2)(input_device_ptr);
2297 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2298 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2299 return Err(format!(
2300 "NVENC maximum encode size {mw}x{mh} exceeded by {width}x{height}; using software"
2301 ));
2302 }
2303 if caps.downgraded_color {
2304 eprintln!(
2305 "[NVENC] {} 4:4:4 (YUV444) encoding unsupported on this GPU; encoding 4:2:0.",
2306 codec.display()
2307 );
2308 }
2309 if tuning.temporal_aq
2310 && query_cap(
2311 &function_list,
2312 encoder_session,
2313 codec_guid,
2314 NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_TEMPORAL_AQ,
2315 ) == Some(0)
2316 {
2317 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2318 (cuda.cuMemFree_v2)(input_device_ptr);
2319 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2320 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2321 return Err(format!(
2322 "{} temporal AQ is not supported by this GPU",
2323 codec.display()
2324 ));
2325 }
2326
2327 let is_444 = caps.fullcolor;
2328 let bit_depth = if settings.video_bit_depth >= 10
2329 && codes_ten_bit(&function_list, encoder_session, codec, codec_guid)
2330 {
2331 10
2332 } else {
2333 8
2334 };
2335 let invalidation = query_cap(
2336 &function_list,
2337 encoder_session,
2338 codec_guid,
2339 NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_REF_PIC_INVALIDATION,
2340 ) == Some(1);
2341
2342 let mut config = NV_ENC_CONFIG {
2343 version: sv(NvStruct::Config),
2344 ..Default::default()
2345 };
2346 let mut preset_config = NV_ENC_PRESET_CONFIG {
2347 version: sv(NvStruct::PresetConfig),
2348 presetCfg: config,
2349 ..Default::default()
2350 };
2351
2352 let get_preset_ex = function_list.nvEncGetEncodePresetConfigEx.unwrap();
2353 let preset_status = get_preset_ex(
2354 encoder_session,
2355 codec_guid,
2356 tuning.preset,
2357 NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
2358 &mut preset_config,
2359 );
2360 if preset_status != NVENCSTATUS::NV_ENC_SUCCESS {
2361 eprintln!(
2362 "[NVENC] nvEncGetEncodePresetConfigEx failed ({preset_status:?}): {}",
2363 last_error(&function_list, encoder_session)
2364 );
2365 }
2366
2367 config = preset_config.presetCfg;
2368 config.version = sv(NvStruct::Config);
2369 config.profileGUID = profile_guid(codec, is_444, bit_depth);
2370 if settings.video_cbr_mode {
2371 let bps = cbr_bps(settings);
2372 log_held_rate(settings, bps);
2373 config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
2374 config.rcParams.multiPass = tuning.multipass;
2375 set_cbr_rate(&mut config.rcParams, bps, cbr_vbv(settings, bps));
2376 let lo = codec.hardware_quantizer_bound(Hardware::Nvenc, settings.video_min_qp);
2377 if lo > 0 {
2378 config.rcParams.set_enableMinQP(1);
2379 config.rcParams.minQP.qpInterP = lo;
2380 config.rcParams.minQP.qpInterB = lo;
2381 config.rcParams.minQP.qpIntra = lo;
2382 }
2383 let hi = codec.hardware_quantizer_bound(Hardware::Nvenc, settings.video_max_qp);
2384 if hi > 0 {
2385 config.rcParams.set_enableMaxQP(1);
2386 config.rcParams.maxQP.qpInterP = hi;
2387 config.rcParams.maxQP.qpInterB = hi;
2388 config.rcParams.maxQP.qpIntra = hi;
2389 }
2390 } else {
2391 let q = codec.hardware_quantizer(Hardware::Nvenc, settings.video_crf);
2392 config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
2393 config.rcParams.constQP.qpInterP = q;
2394 config.rcParams.constQP.qpInterB = q;
2395 config.rcParams.constQP.qpIntra = q;
2396 }
2397 config.rcParams.set_enableAQ(tuning.spatial_aq as u32);
2398 config
2399 .rcParams
2400 .set_enableTemporalAQ(tuning.temporal_aq as u32);
2401 config.frameIntervalP = 1;
2402 config.gopLength = 0xFFFFFFFF;
2403 config.rcParams.set_zeroReorderDelay(1);
2404 config.rcParams.set_strictGOPTarget(1);
2405 config.rcParams.set_enableLookahead(0);
2406 config.rcParams.lookaheadDepth = 0;
2407 let rate = FrameRate::of(settings.target_fps);
2408 let level_at = |weighted| {
2409 nvenc_level(
2410 codec,
2411 width,
2412 height,
2413 rate.ceil(),
2414 level_rate(codec, config.rcParams.maxBitRate as u64, weighted),
2415 tuning.hevc_high_tier,
2416 )
2417 };
2418 let level = level_at(NVENC_AV1_WEIGHTED.load(Ordering::Relaxed));
2419 let weighted_level = level_at(true);
2420 let dpb = match codec {
2421 Codec::H265 => h265_dpb_frames(level, width, height),
2422 Codec::Av1 => AV1_REFERENCES,
2423 _ => h264_dpb_frames(level, width, height),
2424 };
2425 let dpb = if codec == Codec::Av1 {
2426 dpb
2427 } else {
2428 dpb.min(crate::encoders::reference_frames(settings))
2429 };
2430 let ltr = query_cap(
2431 &function_list,
2432 encoder_session,
2433 codec_guid,
2434 NV_ENC_CAPS::NV_ENC_CAPS_NUM_MAX_LTR_FRAMES,
2435 );
2436 let anchors = anchor_count(
2437 codec,
2438 nvenc_cur_ver().0,
2439 invalidation,
2440 ltr,
2441 dpb,
2442 settings.acknowledge_references,
2443 );
2444 Self::configure_codec(
2445 &mut config,
2446 codec,
2447 is_444,
2448 bit_depth,
2449 level,
2450 dpb,
2451 anchors,
2452 &tuning,
2453 );
2454 #[cfg(test)]
2455 if let Some(refs) = tuning.ref_l0 {
2456 match codec {
2457 Codec::H265 => config.encodeCodecConfig.hevcConfig.numRefL0 = refs,
2458 Codec::Av1 => config.encodeCodecConfig.av1Config.numFwdRefs = refs,
2459 _ => config.encodeCodecConfig.h264Config.numRefL0 = refs,
2460 }
2461 }
2462 let engines = query_cap(
2463 &function_list,
2464 encoder_session,
2465 codec_guid,
2466 NV_ENC_CAPS::NV_ENC_CAPS_NUM_ENCODER_ENGINES,
2467 );
2468 let split = split_mode(codec, width, height, engines, nvenc_cur_ver());
2469 #[cfg(test)]
2470 let split = tuning.split.unwrap_or(split);
2471
2472 let mut init_params = NV_ENC_INITIALIZE_PARAMS {
2473 version: sv(NvStruct::InitializeParams),
2474 encodeGUID: codec_guid,
2475 presetGUID: tuning.preset,
2476 tuningInfo: NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
2477 encodeWidth: width,
2478 encodeHeight: height,
2479 darWidth: width,
2480 darHeight: height,
2481 frameRateNum: rate.num,
2482 frameRateDen: rate.den,
2483 enablePTD: 1,
2484 encodeConfig: &mut config,
2485 maxEncodeWidth: nvenc_headroom(width, HEADROOM_WIDTH, caps_wmax),
2486 maxEncodeHeight: nvenc_headroom(height, HEADROOM_HEIGHT, caps_hmax),
2487 ..Default::default()
2488 };
2489 init_params.set_splitEncodeMode(split as u32);
2490
2491 let init_fn = function_list.nvEncInitializeEncoder.unwrap();
2492 let mut headroom_status =
2493 init_fn(encoder_session, &mut Negotiated::new(init_params).value);
2494 if headroom_status != NVENCSTATUS::NV_ENC_SUCCESS && weighted_level != level {
2495 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2499 encoder_session = ptr::null_mut();
2500 if open_fn(&mut session_params, &mut encoder_session) != NVENCSTATUS::NV_ENC_SUCCESS
2501 {
2502 (cuda.cuMemFree_v2)(input_device_ptr);
2503 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2504 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2505 return Err("Failed to reopen the AV1 session at the weighted level".into());
2506 }
2507 config.encodeCodecConfig.av1Config.level = weighted_level;
2508 init_params.encodeConfig = &mut config;
2509 headroom_status = init_fn(encoder_session, &mut Negotiated::new(init_params).value);
2510 if headroom_status == NVENCSTATUS::NV_ENC_SUCCESS {
2511 NVENC_AV1_WEIGHTED.store(true, Ordering::Relaxed);
2512 eprintln!(
2513 "[NVENC] The driver refused AV1 level {level} at {} kbps; declaring {weighted_level}",
2514 config.rcParams.maxBitRate / 1000
2515 );
2516 }
2517 }
2518 if headroom_status != NVENCSTATUS::NV_ENC_SUCCESS {
2519 eprintln!(
2520 "[NVENC] Init with {}x{} resize headroom failed ({headroom_status:?}): {}",
2521 init_params.maxEncodeWidth,
2522 init_params.maxEncodeHeight,
2523 last_error(&function_list, encoder_session)
2524 );
2525 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2528 encoder_session = ptr::null_mut();
2529 let reopened = open_fn(&mut session_params, &mut encoder_session)
2530 == NVENCSTATUS::NV_ENC_SUCCESS;
2531 init_params.maxEncodeWidth = width;
2532 init_params.maxEncodeHeight = height;
2533 init_params.encodeConfig = &mut config;
2534 let exact_status = if reopened {
2535 init_fn(encoder_session, &mut Negotiated::new(init_params).value)
2536 } else {
2537 NVENCSTATUS::NV_ENC_ERR_NO_ENCODE_DEVICE
2538 };
2539 if exact_status != NVENCSTATUS::NV_ENC_SUCCESS {
2540 let detail = if reopened {
2541 let d = last_error(&function_list, encoder_session);
2542 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2543 d
2544 } else {
2545 "could not reopen the session".to_string()
2546 };
2547 (cuda.cuMemFree_v2)(input_device_ptr);
2548 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2549 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2550 return Err(format!(
2551 "Failed to initialize {} encoder at {width}x{height} ({exact_status:?}): {detail}",
2552 codec.display()
2553 ));
2554 }
2555 eprintln!("[NVENC] Running without resize headroom.");
2556 }
2557
2558 init_params.encodeConfig = ptr::null_mut();
2559
2560 let mut reg_res = NV_ENC_REGISTER_RESOURCE {
2561 version: sv(NvStruct::RegisterResource),
2562 resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
2563 width,
2564 height,
2565 resourceToRegister: input_device_ptr as *mut c_void,
2566 pitch: input_pitch as u32,
2567 bufferFormat: NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB,
2568 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
2569 ..Default::default()
2570 };
2571
2572 let register_fn = function_list.nvEncRegisterResource.unwrap();
2573 if register_fn(encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
2574 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2575 (cuda.cuMemFree_v2)(input_device_ptr);
2576 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2577 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2578 return Err("Failed to register input buffer".into());
2579 }
2580
2581 let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
2582 version: sv(NvStruct::MapInputResource),
2583 registeredResource: reg_res.registeredResource,
2584 ..Default::default()
2585 };
2586 let map_fn = function_list.nvEncMapInputResource.unwrap();
2587 if map_fn(encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
2588 (function_list.nvEncUnregisterResource.unwrap())(
2589 encoder_session,
2590 reg_res.registeredResource,
2591 );
2592 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2593 (cuda.cuMemFree_v2)(input_device_ptr);
2594 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2595 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2596 return Err("Failed to map input buffer".into());
2597 }
2598
2599 let mut bitstream_buffers = Vec::new();
2600 let create_bs_fn = function_list.nvEncCreateBitstreamBuffer.unwrap();
2601 for _ in 0..BITSTREAM_BUFFERS {
2602 let mut bitstream_params = NV_ENC_CREATE_BITSTREAM_BUFFER {
2603 version: sv(NvStruct::CreateBitstreamBuffer),
2604 ..Default::default()
2605 };
2606 if create_bs_fn(encoder_session, &mut bitstream_params)
2607 != NVENCSTATUS::NV_ENC_SUCCESS
2608 {
2609 for &bs in &bitstream_buffers {
2610 (function_list.nvEncDestroyBitstreamBuffer.unwrap())(encoder_session, bs);
2611 }
2612 (function_list.nvEncUnmapInputResource.unwrap())(
2613 encoder_session,
2614 map_params.mappedResource,
2615 );
2616 (function_list.nvEncUnregisterResource.unwrap())(
2617 encoder_session,
2618 reg_res.registeredResource,
2619 );
2620 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2621 (cuda.cuMemFree_v2)(input_device_ptr);
2622 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2623 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2624 return Err("Failed to create bitstream buffer".into());
2625 }
2626 bitstream_buffers.push(bitstream_params.bitstreamBuffer);
2627 }
2628
2629 let layout = ConvertLayout::of(is_444, bit_depth);
2634 #[cfg(test)]
2635 let layout = layout.filter(|_| !tuning.hardware_csc);
2636 let csc = layout.and_then(|layout| {
2637 ChromaConvert::new(
2638 &cuda,
2639 &function_list,
2640 encoder_session,
2641 width,
2642 height,
2643 layout,
2644 )
2645 });
2646 if bit_depth > 8 && csc.is_none() {
2647 for buffer in &bitstream_buffers {
2648 (function_list.nvEncDestroyBitstreamBuffer.unwrap())(encoder_session, *buffer);
2649 }
2650 (function_list.nvEncUnmapInputResource.unwrap())(
2651 encoder_session,
2652 map_params.mappedResource,
2653 );
2654 (function_list.nvEncUnregisterResource.unwrap())(
2655 encoder_session,
2656 reg_res.registeredResource,
2657 );
2658 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
2659 (cuda.cuMemFree_v2)(input_device_ptr);
2660 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2661 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
2662 return Err("the driver took no 10-bit convert kernel".into());
2663 }
2664 keep_primary_context(cu_device);
2665 crate::log::debug!(
2666 "[NVENC] {} initialized (4:4:4 mode: {}, chroma convert: {}).",
2667 codec.display(),
2668 is_444,
2669 if csc.is_some() { "kernel" } else { "hardware" }
2670 );
2671
2672 Ok(Self {
2673 encoder_session,
2674 cuda_context: cu_context,
2675 cuda_device: cu_device,
2676 device_name,
2677 egl_display: egl_display as EGLDisplay,
2678 codec,
2679 fullcolor: is_444,
2680 bit_depth,
2681 width,
2682 height,
2683 current_qp: codec.hardware_quantizer(Hardware::Nvenc, settings.video_crf),
2684 held_qp: None,
2685 held_band: None,
2686 hold_refused: false,
2687 held_bytes: 0,
2688 qp_map: Vec::new(),
2689 last_quality: None,
2690 last_bytes: None,
2691 encode_config: config,
2692 init_params,
2693 input_device_ptr,
2694 input_pitch,
2695 input_format: NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB,
2696 registered_input_resource: reg_res.registeredResource,
2697 mapped_input_buffer: map_params.mappedResource,
2698 bitstream_buffers,
2699 current_buffer_idx: 0,
2700 dmabuf_cache: HashMap::new(),
2701 external_input: None,
2702 pinned_hosts: HashMap::new(),
2703 cuda,
2704 egl,
2705 _nvenc_lib: nvenc_lib,
2706 nvenc_funcs: function_list,
2707 omit_stripe_headers: settings.omit_stripe_headers,
2708 node_index: settings.encode_node_index.max(0),
2709 pin_uploads: std::env::var("PIXELFLUX_NVENC_PIN").as_deref() != Ok("0"),
2710 direct_dmabuf: std::env::var("PIXELFLUX_NVENC_DIRECT").as_deref() != Ok("0"),
2711 csc,
2712 dpb,
2713 engines,
2714 references: invalidation.then(|| {
2715 if anchors > 0 {
2716 let mut w =
2717 ReferenceWindow::with_anchors(window_frames(codec, dpb, true), anchors);
2718 if settings.acknowledge_references {
2719 w.set_acknowledged();
2720 }
2721 if codec == Codec::Av1 {
2722 w.lend_free_anchors();
2723 }
2724 w
2725 } else {
2726 ReferenceWindow::new(dpb)
2727 }
2728 }),
2729 last_reference: Reference::Untracked,
2730 frame_num: WideFrameNum::default(),
2731 blit_semaphore: ptr::null_mut(),
2732 band_hash: None,
2733 upload_hash: None,
2734 upload_words: None,
2735 upload_hashes: None,
2736 })
2737 }
2738 }
2739
2740 unsafe fn device_encodes(
2742 funcs: &NV_ENCODE_API_FUNCTION_LIST,
2743 session: *mut c_void,
2744 codec: &GUID,
2745 ) -> bool {
2746 let (Some(count_fn), Some(list_fn)) =
2747 (funcs.nvEncGetEncodeGUIDCount, funcs.nvEncGetEncodeGUIDs)
2748 else {
2749 return true;
2750 };
2751 let mut count = 0u32;
2752 if count_fn(session, &mut count) != NVENCSTATUS::NV_ENC_SUCCESS || count == 0 {
2753 return false;
2754 }
2755 let mut guids = vec![GUID::default(); count as usize];
2756 let mut listed = 0u32;
2757 if list_fn(session, guids.as_mut_ptr(), count, &mut listed) != NVENCSTATUS::NV_ENC_SUCCESS {
2758 return false;
2759 }
2760 guids
2761 .iter()
2762 .take(listed as usize)
2763 .any(|g| guid_eq(g, codec))
2764 }
2765
2766 #[allow(clippy::too_many_arguments)]
2788 fn configure_codec(
2789 config: &mut NV_ENC_CONFIG,
2790 codec: Codec,
2791 fullcolor: bool,
2792 bit_depth: u32,
2793 level: u32,
2794 dpb: u32,
2795 anchors: usize,
2796 tuning: &NvencTuning,
2797 ) {
2798 let depth = if bit_depth > 8 {
2799 NV_ENC_BIT_DEPTH::NV_ENC_BIT_DEPTH_10
2800 } else {
2801 NV_ENC_BIT_DEPTH::NV_ENC_BIT_DEPTH_8
2802 };
2803 let primaries = NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709;
2804 let transfer = NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709;
2805 let matrix = NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709;
2806 let vui = |vui: &mut NV_ENC_CONFIG_H264_VUI_PARAMETERS| {
2807 vui.videoSignalTypePresentFlag = 1;
2808 vui.videoFormat = NV_ENC_VUI_VIDEO_FORMAT::NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED;
2809 vui.colourDescriptionPresentFlag = 1;
2810 vui.colourPrimaries = primaries;
2811 vui.transferCharacteristics = transfer;
2812 vui.colourMatrix = matrix;
2813 vui.videoFullRangeFlag = 0;
2814 };
2815 unsafe {
2816 match codec {
2817 Codec::H265 => {
2818 let c = &mut config.encodeCodecConfig.hevcConfig;
2819 c.level = level;
2820 c.maxNumRefFramesInDPB = dpb;
2821 if anchors > 0 {
2822 c.set_enableLTR(1);
2823 c.ltrTrustMode = 0;
2824 c.ltrNumFrames = anchors as u32;
2825 }
2826 c.tier = if tuning.hevc_high_tier {
2827 h265_tier(level)
2828 } else {
2829 0
2830 };
2831 c.sliceMode = SLICE_MODE_COUNT;
2832 c.sliceModeData = tuning.slices;
2833 c.idrPeriod = 0xFFFFFFFF;
2834 c.set_chromaFormatIDC(if fullcolor { 3 } else { 1 });
2835 c.inputBitDepth = depth;
2836 c.outputBitDepth = depth;
2837 c.set_repeatSPSPPS(1);
2838 c.set_outputAUD(0);
2839 vui(&mut c.hevcVUIParameters);
2840 }
2841 Codec::Av1 => {
2842 let c = &mut config.encodeCodecConfig.av1Config;
2843 c.level = level;
2844 c.tier = 0;
2845 c.idrPeriod = 0xFFFFFFFF;
2846 c.set_chromaFormatIDC(1);
2847 c.inputBitDepth = depth;
2848 c.outputBitDepth = depth;
2849 c.maxNumRefFramesInDPB = dpb;
2850 if anchors > 0 {
2851 c.set_enableLTR(1);
2852 c.ltrNumFrames = anchors as u32;
2853 }
2854 c.set_repeatSeqHdr(1);
2855 c.set_outputAnnexBFormat(0);
2856 c.set_enableBitstreamPadding(0);
2857 c.numTileColumns = 1;
2858 c.numTileRows = 1;
2859 c.colorPrimaries = primaries;
2860 c.transferCharacteristics = transfer;
2861 c.matrixCoefficients = matrix;
2862 c.colorRange = 0;
2863 }
2864 _ => {
2865 let c = &mut config.encodeCodecConfig.h264Config;
2866 c.level = level;
2867 c.maxNumRefFrames = dpb;
2868 if anchors > 0 {
2869 c.set_enableLTR(1);
2870 c.ltrTrustMode = 0;
2871 c.ltrNumFrames = anchors as u32;
2872 }
2873 c.sliceMode = SLICE_MODE_COUNT;
2874 c.sliceModeData = tuning.slices;
2875 c.idrPeriod = 0xFFFFFFFF;
2876 c.chromaFormatIDC = if fullcolor { 3 } else { 1 };
2877 c.set_repeatSPSPPS(1);
2878 c.entropyCodingMode =
2879 NV_ENC_H264_ENTROPY_CODING_MODE::NV_ENC_H264_ENTROPY_CODING_MODE_CABAC;
2880 c.set_outputAUD(0);
2881 c.h264VUIParameters.bitstreamRestrictionFlag = 1;
2882 vui(&mut c.h264VUIParameters);
2883 }
2884 }
2885 }
2886 }
2887
2888 fn declared_level(&self) -> u32 {
2890 let config = &self.encode_config.encodeCodecConfig;
2891 unsafe {
2892 match self.codec {
2893 Codec::H265 => config.hevcConfig.level,
2894 Codec::Av1 => config.av1Config.level,
2895 _ => config.h264Config.level,
2896 }
2897 }
2898 }
2899
2900 fn level_for(&self, width: u32, height: u32, fps: u32) -> u32 {
2903 let high_tier = unsafe { self.encode_config.encodeCodecConfig.hevcConfig.tier == 1 };
2904 nvenc_level(
2905 self.codec,
2906 width,
2907 height,
2908 fps,
2909 level_rate(
2910 self.codec,
2911 self.encode_config.rcParams.maxBitRate as u64,
2912 NVENC_AV1_WEIGHTED.load(Ordering::Relaxed),
2913 ),
2914 high_tier,
2915 )
2916 }
2917
2918 fn set_level(&mut self, width: u32, height: u32, fps: u32) {
2920 let level = self.level_for(width, height, fps);
2921 match self.codec {
2922 Codec::H265 => self.encode_config.encodeCodecConfig.hevcConfig.level = level,
2923 Codec::Av1 => self.encode_config.encodeCodecConfig.av1Config.level = level,
2924 _ => self.encode_config.encodeCodecConfig.h264Config.level = level,
2925 }
2926 }
2927
2928 fn dpb_frames_at(&self, width: u32, height: u32, fps: u32) -> u32 {
2931 let level = self.level_for(width, height, fps);
2932 match self.codec {
2933 Codec::H265 => h265_dpb_frames(level, width, height),
2934 Codec::Av1 => AV1_REFERENCES,
2935 _ => h264_dpb_frames(level, width, height),
2936 }
2937 }
2938
2939 pub fn last_reference(&self) -> Reference {
2941 self.last_reference
2942 }
2943
2944 pub fn invalidate_reference(&mut self, frame_id: u16) -> bool {
2947 let Some(references) = &mut self.references else {
2948 return false;
2949 };
2950 match references.invalidate(frame_id) {
2951 Invalidation::Forget(pts) => unsafe {
2952 (self.nvenc_funcs.nvEncInvalidateRefFrames.unwrap())(self.encoder_session, pts)
2953 == NVENCSTATUS::NV_ENC_SUCCESS
2954 },
2955 Invalidation::KeyFrame | Invalidation::Ignored => true,
2956 }
2957 }
2958
2959 pub fn acknowledge_reference(&mut self, frame_id: u16, held: bool) {
2962 if let Some(references) = &mut self.references {
2963 references.acknowledge(frame_id, held);
2964 }
2965 }
2966
2967 pub fn take_report(&mut self, report: ReferenceReport) -> bool {
2970 match report {
2971 ReferenceReport::Lost(frame_id) => self.invalidate_reference(frame_id),
2972 ReferenceReport::Held(frame_id) => {
2973 self.acknowledge_reference(frame_id, true);
2974 true
2975 }
2976 ReferenceReport::Sent(frame_id) => {
2977 self.acknowledge_reference(frame_id, false);
2978 true
2979 }
2980 }
2981 }
2982
2983 fn retract_last(&mut self) -> bool {
2987 let (funcs, session) = (&self.nvenc_funcs, self.encoder_session);
2988 self.references.as_mut().is_some_and(|r| {
2989 r.retract(|pts| unsafe {
2990 (funcs.nvEncInvalidateRefFrames.unwrap())(session, pts)
2991 == NVENCSTATUS::NV_ENC_SUCCESS
2992 })
2993 })
2994 }
2995
2996 pub fn split_summary(&self) -> String {
2999 let forced = [
3000 NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_AUTO_FORCED_MODE,
3001 NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_THREE_FORCED_MODE,
3002 ]
3003 .map(|m| m as u32)
3004 .contains(&self.init_params.splitEncodeMode());
3005 match self.engines {
3006 Some(n) if n > 1 && forced => format!("split across {n} engines"),
3007 Some(n) if n > 1 && self.codec != Codec::H264 => {
3008 format!("split as the driver picks, {n} engines")
3009 }
3010 Some(n) => format!("{n} engine{}", if n == 1 { "" } else { "s" }),
3011 None => "engines unreported".into(),
3012 }
3013 }
3014
3015 pub fn device_name(&self) -> &str {
3018 &self.device_name
3019 }
3020
3021 pub fn codec(&self) -> Codec {
3022 self.codec
3023 }
3024
3025 pub fn is_full_range(&self) -> bool {
3027 false
3028 }
3029
3030 pub fn is_fullcolor(&self) -> bool {
3032 self.fullcolor
3033 }
3034
3035 pub fn bit_depth(&self) -> u32 {
3037 self.bit_depth
3038 }
3039
3040 pub fn reconfigure_resolution(
3070 &mut self,
3071 settings: &RustCaptureSettings,
3072 ) -> Result<bool, String> {
3073 let new_w = settings.width as u32;
3074 let new_h = settings.height as u32;
3075 let is_cbr = self.encode_config.rcParams.rateControlMode
3076 == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
3077 if settings.encode_node_index.max(0) != self.node_index {
3078 return Err("encode device changed".into());
3079 }
3080 if settings.codec != self.codec {
3081 return Err("codec changed".into());
3082 }
3083 if (settings.video_fullcolor && self.codec.fullcolor()) != self.fullcolor {
3084 return Err("chroma format changed".into());
3085 }
3086 if (settings.video_bit_depth >= 10) != (self.bit_depth > 8)
3087 && matches!(self.codec, Codec::H265 | Codec::Av1)
3088 {
3089 return Err("bit depth changed".into());
3090 }
3091 if settings.video_cbr_mode != is_cbr {
3092 return Err("rate-control mode changed".into());
3093 }
3094 if new_w == 0
3095 || new_h == 0
3096 || new_w > self.init_params.maxEncodeWidth
3097 || new_h > self.init_params.maxEncodeHeight
3098 {
3099 return Err(format!(
3100 "{}x{} outside reconfigure headroom {}x{}",
3101 new_w, new_h, self.init_params.maxEncodeWidth, self.init_params.maxEncodeHeight
3102 ));
3103 }
3104 if (new_w, new_h) == (self.width, self.height) {
3105 self.release_pinned_hosts();
3106 self.reconfigure_rate(settings);
3107 self.omit_stripe_headers = settings.omit_stripe_headers;
3108 return Ok(false);
3109 }
3110 let rate = FrameRate::of(settings.target_fps);
3111 let dpb = self.dpb_frames_at(new_w, new_h, rate.ceil()).min(self.dpb);
3112
3113 unsafe {
3114 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
3115 self.unmap_external_input();
3116 if let Some(csc) = self.csc.take() {
3117 csc.release(&self.cuda, &self.nvenc_funcs, self.encoder_session);
3118 }
3119 if !self.mapped_input_buffer.is_null() {
3120 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
3121 self.encoder_session,
3122 self.mapped_input_buffer,
3123 );
3124 self.mapped_input_buffer = ptr::null_mut();
3125 }
3126 if !self.registered_input_resource.is_null() {
3127 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3128 self.encoder_session,
3129 self.registered_input_resource,
3130 );
3131 self.registered_input_resource = ptr::null_mut();
3132 }
3133 if self.input_device_ptr != 0 {
3134 (self.cuda.cuMemFree_v2)(self.input_device_ptr);
3135 self.input_device_ptr = 0;
3136 }
3137 let imports: Vec<CachedDmaBuf> = self.dmabuf_cache.drain().map(|(_, c)| c).collect();
3138 for cache in imports {
3139 self.release_dmabuf_import(cache);
3140 }
3141 for (base, len) in self.pinned_hosts.drain() {
3142 if len > 0 {
3143 (self.cuda.cuMemHostUnregister)(base as *mut c_void);
3144 }
3145 }
3146
3147 if is_cbr {
3148 let bps = cbr_bps(settings);
3149 set_cbr_rate(
3150 &mut self.encode_config.rcParams,
3151 bps,
3152 cbr_vbv(settings, bps),
3153 );
3154 } else {
3155 let qp = self
3156 .codec
3157 .hardware_quantizer(Hardware::Nvenc, settings.video_crf);
3158 self.encode_config.rcParams.constQP.qpInterP = qp;
3159 self.encode_config.rcParams.constQP.qpInterB = qp;
3160 self.encode_config.rcParams.constQP.qpIntra = qp;
3161 self.current_qp = qp;
3162 }
3163 self.set_level(new_w, new_h, rate.ceil());
3164 match self.codec {
3165 Codec::H265 => {
3166 self.encode_config
3167 .encodeCodecConfig
3168 .hevcConfig
3169 .maxNumRefFramesInDPB = dpb
3170 }
3171 Codec::Av1 => {
3172 self.encode_config
3173 .encodeCodecConfig
3174 .av1Config
3175 .maxNumRefFramesInDPB = dpb
3176 }
3177 _ => {
3178 self.encode_config
3179 .encodeCodecConfig
3180 .h264Config
3181 .maxNumRefFrames = dpb
3182 }
3183 }
3184 self.init_params.encodeWidth = new_w;
3185 self.init_params.encodeHeight = new_h;
3186 self.init_params.darWidth = new_w;
3187 self.init_params.darHeight = new_h;
3188 self.init_params.frameRateNum = rate.num;
3189 self.init_params.frameRateDen = rate.den;
3190 self.init_params.set_splitEncodeMode(split_mode(
3191 self.codec,
3192 new_w,
3193 new_h,
3194 self.engines,
3195 nvenc_cur_ver(),
3196 ) as u32);
3197 if self.reconfigure(true, true) != NVENCSTATUS::NV_ENC_SUCCESS {
3198 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3199 return Err("NvEncReconfigureEncoder rejected the resolution change".into());
3200 }
3201 self.width = new_w;
3202 self.height = new_h;
3203 self.dpb = dpb;
3204 if let Some(references) = &mut self.references {
3205 references.set_capacity(window_frames(self.codec, dpb, references.anchored()));
3206 references.reset();
3207 }
3208
3209 let mut input_device_ptr: CUdeviceptr = 0;
3210 let mut input_pitch: usize = 0;
3211 let res = (self.cuda.cuMemAllocPitch_v2)(
3212 &mut input_device_ptr,
3213 &mut input_pitch,
3214 (new_w * 4) as usize,
3215 new_h as usize,
3216 16,
3217 );
3218 if res != CUresult::CUDA_SUCCESS {
3219 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3220 return Err("Failed to allocate ARGB input buffer on GPU".into());
3221 }
3222 let mut reg_res = NV_ENC_REGISTER_RESOURCE {
3223 version: sv(NvStruct::RegisterResource),
3224 resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
3225 width: new_w,
3226 height: new_h,
3227 resourceToRegister: input_device_ptr as *mut c_void,
3228 pitch: input_pitch as u32,
3229 bufferFormat: self.input_format,
3230 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
3231 ..Default::default()
3232 };
3233 let register_fn = self.nvenc_funcs.nvEncRegisterResource.unwrap();
3234 if register_fn(self.encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
3235 (self.cuda.cuMemFree_v2)(input_device_ptr);
3236 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3237 return Err("Failed to register input buffer".into());
3238 }
3239 let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
3240 version: sv(NvStruct::MapInputResource),
3241 registeredResource: reg_res.registeredResource,
3242 ..Default::default()
3243 };
3244 let map_fn = self.nvenc_funcs.nvEncMapInputResource.unwrap();
3245 if map_fn(self.encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
3246 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3247 self.encoder_session,
3248 reg_res.registeredResource,
3249 );
3250 (self.cuda.cuMemFree_v2)(input_device_ptr);
3251 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3252 return Err("Failed to map input buffer".into());
3253 }
3254 self.input_device_ptr = input_device_ptr;
3255 self.input_pitch = input_pitch;
3256 self.registered_input_resource = reg_res.registeredResource;
3257 self.mapped_input_buffer = map_params.mappedResource;
3258 if let Some(layout) = ConvertLayout::of(self.fullcolor, self.bit_depth) {
3259 self.csc = ChromaConvert::new(
3260 &self.cuda,
3261 &self.nvenc_funcs,
3262 self.encoder_session,
3263 new_w,
3264 new_h,
3265 layout,
3266 );
3267 if self.bit_depth > 8 && self.csc.is_none() {
3268 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3269 return Err("the driver took no 10-bit convert kernel".into());
3270 }
3271 }
3272 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3273 }
3274 self.omit_stripe_headers = settings.omit_stripe_headers;
3275 Ok(true)
3276 }
3277
3278 unsafe fn pin_host_source(&mut self, base: usize, len: usize) {
3283 if let std::collections::hash_map::Entry::Vacant(e) = self.pinned_hosts.entry(base) {
3284 let st = (self.cuda.cuMemHostRegister_v2)(base as *mut c_void, len, 0);
3285 e.insert(if st == CUresult::CUDA_SUCCESS { len } else { 0 });
3286 }
3287 }
3288
3289 pub fn release_pinned_hosts(&mut self) {
3296 if self.pinned_hosts.is_empty() {
3297 return;
3298 }
3299 unsafe {
3300 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
3301 for (base, len) in self.pinned_hosts.drain() {
3302 if len > 0 {
3303 (self.cuda.cuMemHostUnregister)(base as *mut c_void);
3304 }
3305 }
3306 let _ = (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3307 }
3308 }
3309
3310 unsafe fn release_dmabuf_import(&self, cache: CachedDmaBuf) {
3315 if cache.tex != 0 {
3316 (self.cuda.cuTexObjectDestroy)(cache.tex);
3317 }
3318 if let DmaBufInput::Direct {
3319 registered, mapped, ..
3320 } = cache.input
3321 {
3322 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(self.encoder_session, mapped);
3323 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(self.encoder_session, registered);
3324 }
3325 (self.cuda.cuGraphicsUnregisterResource)(cache.cuda_resource);
3326 if let Some(egl) = &self.egl {
3327 (egl.eglDestroyImageKHR)(self.egl_display, cache.egl_image);
3328 }
3329 }
3330
3331 unsafe fn set_input_format(&mut self, format: NV_ENC_BUFFER_FORMAT) -> Result<(), String> {
3338 if self.input_format == format && !self.registered_input_resource.is_null() {
3339 return Ok(());
3340 }
3341 if !self.mapped_input_buffer.is_null() {
3342 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
3343 self.encoder_session,
3344 self.mapped_input_buffer,
3345 );
3346 self.mapped_input_buffer = ptr::null_mut();
3347 }
3348 if !self.registered_input_resource.is_null() {
3349 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3350 self.encoder_session,
3351 self.registered_input_resource,
3352 );
3353 self.registered_input_resource = ptr::null_mut();
3354 }
3355 let mut reg_res = NV_ENC_REGISTER_RESOURCE {
3356 version: sv(NvStruct::RegisterResource),
3357 resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
3358 width: self.width,
3359 height: self.height,
3360 resourceToRegister: self.input_device_ptr as *mut c_void,
3361 pitch: self.input_pitch as u32,
3362 bufferFormat: format,
3363 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
3364 ..Default::default()
3365 };
3366 if (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res)
3367 != NVENCSTATUS::NV_ENC_SUCCESS
3368 {
3369 return Err(format!("Failed to register input buffer as {format:?}"));
3370 }
3371 let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
3372 version: sv(NvStruct::MapInputResource),
3373 registeredResource: reg_res.registeredResource,
3374 ..Default::default()
3375 };
3376 if (self.nvenc_funcs.nvEncMapInputResource.unwrap())(self.encoder_session, &mut map_params)
3377 != NVENCSTATUS::NV_ENC_SUCCESS
3378 {
3379 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
3380 self.encoder_session,
3381 reg_res.registeredResource,
3382 );
3383 return Err(format!("Failed to map input buffer as {format:?}"));
3384 }
3385 self.registered_input_resource = reg_res.registeredResource;
3386 self.mapped_input_buffer = map_params.mappedResource;
3387 self.input_format = format;
3388 Ok(())
3389 }
3390
3391 unsafe fn reconfigure(&mut self, reset: bool, force_idr: bool) -> NVENCSTATUS {
3394 self.init_params.encodeConfig = &mut self.encode_config;
3395 let mut params = reconfigure_params(self.init_params, nvenc_cur_ver(), reset, force_idr);
3396 (self.nvenc_funcs.nvEncReconfigureEncoder.unwrap())(self.encoder_session, &mut params.value)
3397 }
3398
3399 unsafe fn reconfigure_if_needed(&mut self, crf: u32) -> bool {
3409 if self.encode_config.rcParams.rateControlMode
3410 == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
3411 {
3412 return false;
3413 }
3414 let target_qp = self.codec.hardware_quantizer(Hardware::Nvenc, crf as i32);
3415 if self.current_qp != target_qp {
3416 self.encode_config.rcParams.constQP.qpInterP = target_qp;
3417 self.encode_config.rcParams.constQP.qpInterB = target_qp;
3418 self.encode_config.rcParams.constQP.qpIntra = target_qp;
3419 let status = self.reconfigure(false, false);
3420 if status == NVENCSTATUS::NV_ENC_SUCCESS {
3421 self.current_qp = target_qp;
3422 return true;
3423 }
3424 eprintln!(
3425 "[NVENC] Quantizer reconfigure refused ({status:?}): {}",
3426 last_error(&self.nvenc_funcs, self.encoder_session)
3427 );
3428 }
3429 false
3430 }
3431
3432 pub fn last_quality(&self) -> Option<u32> {
3435 self.last_quality
3436 }
3437
3438 pub fn last_size(&self) -> Option<usize> {
3440 self.last_bytes
3441 }
3442
3443 pub fn hold_quantizer(&mut self, crf: u32, band: Option<(f64, f64)>) {
3456 self.held_qp = Some(crf);
3457 self.held_band = band.filter(|_| self.band_size().is_some());
3458 }
3459
3460 pub fn band_size(&self) -> Option<usize> {
3464 matches!(self.codec, Codec::H264 | Codec::H265 | Codec::Av1).then_some(self.held_bytes)
3465 }
3466
3467 unsafe fn hold_rate(&mut self, q: u32, mapped: bool) -> Option<NV_ENC_RC_PARAMS> {
3476 let saved = self.encode_config.rcParams;
3477 let constant = saved.rateControlMode == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
3478 if constant && !mapped && saved.constQP.qpIntra == q && saved.constQP.qpInterP == q {
3479 return None;
3480 }
3481 let rc = &mut self.encode_config.rcParams;
3482 if mapped {
3483 rc.qpMapMode = NV_ENC_QP_MAP_MODE::NV_ENC_QP_MAP_DELTA;
3484 }
3485 rc.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
3486 rc.constQP.qpInterP = q;
3487 rc.constQP.qpInterB = q;
3488 rc.constQP.qpIntra = q;
3489 let status = self.reconfigure(false, false);
3490 if status != NVENCSTATUS::NV_ENC_SUCCESS {
3491 self.encode_config.rcParams = saved;
3492 if !std::mem::replace(&mut self.hold_refused, true) {
3493 eprintln!(
3494 "[NVENC] Holding a frame at quantizer {q} refused ({status:?}): {}",
3495 last_error(&self.nvenc_funcs, self.encoder_session)
3496 );
3497 }
3498 return None;
3499 }
3500 Some(saved)
3501 }
3502
3503 pub fn reconfigure_rate(&mut self, settings: &RustCaptureSettings) -> bool {
3516 unsafe {
3517 let mut changed = false;
3518 let mut level_raised = false;
3519 if self.encode_config.rcParams.rateControlMode
3520 == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
3521 {
3522 let bps = cbr_bps(settings);
3523 let vbv = cbr_vbv(settings, bps);
3524 let rc = &mut self.encode_config.rcParams;
3525 if rc.averageBitRate != bps || rc.maxBitRate != bps || rc.vbvBufferSize != vbv {
3526 log_held_rate(settings, bps);
3527 set_cbr_rate(rc, bps, vbv);
3528 changed = true;
3529 let fps = self
3530 .init_params
3531 .frameRateNum
3532 .div_ceil(self.init_params.frameRateDen.max(1));
3533 let (w, h) = (self.init_params.encodeWidth, self.init_params.encodeHeight);
3534 if self.level_for(w, h, fps) > self.declared_level() {
3535 self.set_level(w, h, fps);
3536 level_raised = true;
3537 }
3538 }
3539 }
3540 let rate = FrameRate::of(settings.target_fps);
3541 if (self.init_params.frameRateNum, self.init_params.frameRateDen)
3542 != (rate.num, rate.den)
3543 {
3544 self.init_params.frameRateNum = rate.num;
3545 self.init_params.frameRateDen = rate.den;
3546 let (w, h, fps) = (
3547 self.init_params.encodeWidth,
3548 self.init_params.encodeHeight,
3549 rate.ceil(),
3550 );
3551 if self.level_for(w, h, fps) > self.declared_level() {
3552 self.set_level(w, h, fps);
3553 level_raised = true;
3554 }
3555 changed = true;
3556 }
3557 if !changed {
3558 return true;
3559 }
3560 let mut status = self.reconfigure(false, level_raised);
3561 if status != NVENCSTATUS::NV_ENC_SUCCESS
3562 && self.codec == Codec::Av1
3563 && !NVENC_AV1_WEIGHTED.swap(true, Ordering::Relaxed)
3564 {
3565 let (w, h) = (self.init_params.encodeWidth, self.init_params.encodeHeight);
3566 let fps = self
3567 .init_params
3568 .frameRateNum
3569 .div_ceil(self.init_params.frameRateDen.max(1));
3570 let refused = self.declared_level();
3571 if self.level_for(w, h, fps) > refused {
3572 self.set_level(w, h, fps);
3573 level_raised = true;
3574 status = self.reconfigure(false, true);
3575 }
3576 if status == NVENCSTATUS::NV_ENC_SUCCESS {
3577 eprintln!(
3578 "[NVENC] The driver refused AV1 level {refused} at {} kbps; declaring {}",
3579 self.encode_config.rcParams.maxBitRate / 1000,
3580 self.declared_level()
3581 );
3582 } else {
3583 NVENC_AV1_WEIGHTED.store(false, Ordering::Relaxed);
3584 }
3585 }
3586 if status != NVENCSTATUS::NV_ENC_SUCCESS {
3587 eprintln!(
3588 "[NVENC] Rate reconfigure refused ({status:?}): {}",
3589 last_error(&self.nvenc_funcs, self.encoder_session)
3590 );
3591 return false;
3592 }
3593 if level_raised && let Some(references) = &mut self.references {
3594 references.reset();
3595 }
3596 true
3597 }
3598 }
3599
3600 unsafe fn submit_frame(
3616 &mut self,
3617 mapped_buffer: NV_ENC_INPUT_PTR,
3618 buffer_format: NV_ENC_BUFFER_FORMAT,
3619 frame_number: u64,
3620 force_idr: bool,
3621 ) -> Result<Vec<u8>, String> {
3622 let output_bitstream = self.bitstream_buffers[self.current_buffer_idx];
3623 self.current_buffer_idx = (self.current_buffer_idx + 1) % self.bitstream_buffers.len();
3624
3625 let mut force_idr =
3626 force_idr || self.references.as_ref().is_some_and(|r| !r.has_reference());
3627 if !force_idr
3628 && let Some(references) = &mut self.references
3629 && references.plan_anchor(false).is_some()
3630 {
3631 match references.settle() {
3632 Some(Invalidation::Forget(pts)) => {
3633 force_idr = (self.nvenc_funcs.nvEncInvalidateRefFrames.unwrap())(
3634 self.encoder_session,
3635 pts,
3636 ) != NVENCSTATUS::NV_ENC_SUCCESS;
3637 }
3638 Some(Invalidation::KeyFrame) => force_idr = true,
3639 _ => {}
3640 }
3641 }
3642 if !force_idr && let Some(references) = &mut self.references {
3643 match references.forget_stale() {
3644 Some(Invalidation::Forget(pts)) => {
3645 force_idr = (self.nvenc_funcs.nvEncInvalidateRefFrames.unwrap())(
3646 self.encoder_session,
3647 pts,
3648 ) != NVENCSTATUS::NV_ENC_SUCCESS;
3649 }
3650 Some(Invalidation::KeyFrame) => force_idr = true,
3651 _ => {}
3652 }
3653 }
3654 let anchor = self
3655 .references
3656 .as_ref()
3657 .and_then(|r| r.plan_anchor(force_idr));
3658 let named = self
3659 .references
3660 .as_ref()
3661 .filter(|_| !force_idr)
3662 .and_then(ReferenceWindow::predicting_anchor);
3663 let mut pic_params = NV_ENC_PIC_PARAMS {
3664 version: sv(NvStruct::PicParams),
3665 inputWidth: self.width,
3666 inputHeight: self.height,
3667 inputBuffer: mapped_buffer,
3668 outputBitstream: output_bitstream,
3669 bufferFmt: buffer_format,
3670 pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
3671 encodePicFlags: if force_idr {
3672 NV_ENC_PIC_FLAGS::NV_ENC_PIC_FLAG_FORCEIDR as u32
3673 } else {
3674 0
3675 },
3676 inputTimeStamp: self
3677 .references
3678 .as_ref()
3679 .map_or(0, ReferenceWindow::next_pts),
3680 ..Default::default()
3681 };
3682
3683 macro_rules! long_term {
3684 ($p:expr) => {{
3685 let p = $p;
3686 if let Some(slot) = named {
3687 p.set_ltrUseFrames(1);
3688 p.ltrUseFrameBitmap = 1 << slot;
3689 }
3690 if let Some(slot) = anchor {
3691 p.set_ltrMarkFrame(1);
3692 p.ltrMarkFrameIdx = slot as u32;
3693 }
3694 }};
3695 }
3696 match self.codec {
3697 Codec::Av1 => long_term!(&mut pic_params.codecPicParams.av1PicParams),
3698 Codec::H265 => long_term!(&mut pic_params.codecPicParams.hevcPicParams),
3699 _ => long_term!(&mut pic_params.codecPicParams.h264PicParams),
3700 }
3701 let held_qp = self.held_qp.take();
3702 let band = self.held_band.take();
3703 let held = held_qp.and_then(|crf| {
3704 let q = self.codec.hardware_quantizer(Hardware::Nvenc, crf as i32);
3705 let rest = self
3706 .codec
3707 .quantizer_max()
3708 .min(q + i8::MIN.unsigned_abs() as u32);
3709 match band {
3710 Some(share) if rest > q => {
3711 fill_band_map(
3712 &mut self.qp_map,
3713 self.codec,
3714 self.width,
3715 self.height,
3716 share,
3717 q as i32 - rest as i32,
3718 );
3719 let held = self.hold_rate(rest, true);
3720 if held.is_some() {
3721 pic_params.qpDeltaMap = self.qp_map.as_mut_ptr();
3722 pic_params.qpDeltaMapSize = self.qp_map.len() as u32;
3723 }
3724 held
3725 }
3726 _ => self.hold_rate(q, false),
3727 }
3728 });
3729 let mut result = self.encode_picture(
3730 &mut pic_params,
3731 output_bitstream,
3732 frame_number,
3733 held_qp.is_some(),
3734 anchor,
3735 );
3736 let retry = match (held_qp, held, &result) {
3737 (Some(crf), Some(rc), Ok(coded))
3738 if rc.rateControlMode == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
3739 && (force_idr
3740 || band.is_none()
3741 && coded.len() as f64
3742 > rc.averageBitRate as f64 / 8.0 * super::HELD_REFRESH_LIMIT_S) =>
3743 {
3744 let cap = (rc.averageBitRate as f64 / 8.0 * super::HELD_KEY_BUDGET_S) as usize;
3745 super::held_key_retry(crf, coded.len(), cap)
3746 }
3747 _ => None,
3748 };
3749 if let Some(coarser) = retry
3750 && self
3751 .hold_rate(
3752 self.codec
3753 .hardware_quantizer(Hardware::Nvenc, coarser as i32),
3754 false,
3755 )
3756 .is_some()
3757 && (force_idr || self.retract_last())
3758 {
3759 pic_params.inputTimeStamp = self
3760 .references
3761 .as_ref()
3762 .map_or(0, ReferenceWindow::next_pts);
3763 result = self.encode_picture(
3764 &mut pic_params,
3765 output_bitstream,
3766 frame_number,
3767 true,
3768 anchor,
3769 );
3770 }
3771 if let Some(rc) = held {
3772 self.encode_config.rcParams = rc;
3773 let status = self.reconfigure(false, false);
3774 if status != NVENCSTATUS::NV_ENC_SUCCESS {
3775 eprintln!(
3776 "[NVENC] Restoring the rate control after a held frame refused ({status:?}): {}",
3777 last_error(&self.nvenc_funcs, self.encoder_session)
3778 );
3779 }
3780 }
3781 result
3782 }
3783
3784 unsafe fn encode_picture(
3788 &mut self,
3789 pic_params: &mut NV_ENC_PIC_PARAMS,
3790 output_bitstream: NV_ENC_OUTPUT_PTR,
3791 frame_number: u64,
3792 held: bool,
3793 anchor: Option<u8>,
3794 ) -> Result<Vec<u8>, String> {
3795 let encode_fn = self.nvenc_funcs.nvEncEncodePicture.unwrap();
3796 let res = encode_fn(self.encoder_session, pic_params);
3797 if res != NVENCSTATUS::NV_ENC_SUCCESS {
3798 return Err(format!("Encode Picture failed: {:?}", res));
3799 }
3800 if let Some(rows) = self.upload_hash.take() {
3803 let (src, pitch, width, height) = (
3804 self.input_device_ptr,
3805 self.input_pitch,
3806 self.width,
3807 self.height,
3808 );
3809 let cuda = &self.cuda;
3810 self.upload_words = self
3811 .band_hash
3812 .get_or_insert_with(|| BandHash::new(cuda))
3813 .as_mut()
3814 .and_then(|h| h.launch(cuda, src, pitch, width, height, rows));
3815 }
3816
3817 let mut negotiated = Negotiated::new(NV_ENC_LOCK_BITSTREAM {
3818 version: sv(NvStruct::LockBitstream),
3819 outputBitstream: output_bitstream,
3820 ..Default::default()
3821 });
3822 let lock_params = &mut negotiated.value;
3823 lock_params.set_doNotWait(0);
3824 let lock_fn = self.nvenc_funcs.nvEncLockBitstream.unwrap();
3825 let status = lock_fn(self.encoder_session, lock_params);
3826 if status != NVENCSTATUS::NV_ENC_SUCCESS {
3827 return Err(format!("Lock Bitstream failed: {status:?}"));
3828 }
3829
3830 let data_ptr = lock_params.bitstreamBufferPtr as *const u8;
3831 let data_size = lock_params.bitstreamSizeInBytes as usize;
3832 let header_sz = if self.omit_stripe_headers {
3833 0
3834 } else {
3835 VIDEO_HEADER_LEN
3836 };
3837 let mut output = Vec::with_capacity(header_sz + data_size);
3838 if !held {
3839 self.last_quality = match lock_params.frameAvgQP {
3840 0 => None,
3841 q => Some(self.codec.hardware_quality_index(Hardware::Nvenc, q)),
3842 };
3843 self.last_bytes = Some(data_size);
3844 } else {
3845 self.held_bytes = data_size;
3846 }
3847 let frame_type = match lock_params.pictureType {
3848 NV_ENC_PIC_TYPE::NV_ENC_PIC_TYPE_IDR => FRAME_KEY,
3849 NV_ENC_PIC_TYPE::NV_ENC_PIC_TYPE_I => FRAME_INTRA,
3850 _ => FRAME_DELTA,
3851 };
3852 let mut wire_type = frame_type;
3853 if let Some(references) = &mut self.references {
3854 let shared = references.acknowledging() && references.predicts_from_shared();
3855 self.last_reference =
3856 references.record_marked(frame_number as u16, frame_type != FRAME_DELTA, anchor);
3857 if anchor.is_some() && frame_type == FRAME_DELTA && shared {
3858 wire_type |= FRAME_ANCHOR;
3859 }
3860 }
3861
3862 if !self.omit_stripe_headers {
3863 push_video_header(
3864 &mut output,
3865 self.codec,
3866 wire_type,
3867 frame_number as u16,
3868 0,
3869 self.width as u16,
3870 self.height as u16,
3871 self.last_reference,
3872 );
3873 }
3874
3875 if data_size > 0 && !data_ptr.is_null() {
3876 let slice = std::slice::from_raw_parts(data_ptr, data_size);
3877 if self.codec == Codec::H264 && self.references.is_some() {
3878 let mut widened = true;
3879 for nal in crate::encoders::codec::annexb_nals(slice) {
3880 widened &= self.frame_num.push_unit(&[0, 0, 0, 1], nal, &mut output);
3881 }
3882 if !widened && let Some(references) = &mut self.references {
3883 references.reset();
3886 }
3887 } else {
3888 output.extend_from_slice(slice);
3889 }
3890 }
3891 if frame_type == FRAME_KEY
3892 && self.codec == Codec::H264
3893 && let Some(references) = &mut self.references
3894 {
3895 references.set_frame_num_range(h264_frame_num_range(&output[header_sz..]).unwrap_or(0));
3896 }
3897
3898 (self.nvenc_funcs.nvEncUnlockBitstream.unwrap())(self.encoder_session, output_bitstream);
3899 Ok(output)
3900 }
3901
3902 pub fn encode(
3928 &mut self,
3929 dmabuf: &Dmabuf,
3930 frame_number: u64,
3931 crf: u32,
3932 force_idr: bool,
3933 ) -> Result<Vec<u8>, String> {
3934 self.encode_frame(dmabuf, frame_number, crf, force_idr, false)
3935 }
3936
3937 pub fn encode_after_blit(
3943 &mut self,
3944 dmabuf: &Dmabuf,
3945 frame_number: u64,
3946 crf: u32,
3947 force_idr: bool,
3948 ) -> Result<Vec<u8>, String> {
3949 self.encode_frame(dmabuf, frame_number, crf, force_idr, true)
3950 }
3951
3952 pub fn blit_semaphore_fd(&mut self) -> Result<std::os::fd::OwnedFd, String> {
3956 use std::os::fd::IntoRawFd;
3957 unsafe {
3958 let (Some(import), Some(_), Some(destroy)) = (
3959 self.cuda.cuImportExternalSemaphore,
3960 self.cuda.cuWaitExternalSemaphoresAsync,
3961 self.cuda.cuDestroyExternalSemaphore,
3962 ) else {
3963 return Err("the CUDA driver has no external semaphores".into());
3964 };
3965 let mut uuid = CUuuid { bytes: [0; 16] };
3966 if (self.cuda.cuDeviceGetUuid)(&mut uuid, self.cuda_device) != CUresult::CUDA_SUCCESS {
3967 return Err("the CUDA device has no UUID to find its Vulkan device by".into());
3968 }
3969 let (cuda_fd, server_fd) =
3970 super::blit_semaphore::opaque_fd_pair(uuid.bytes.map(|b| b.to_ne_bytes()[0]))?;
3971 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
3972 if !self.blit_semaphore.is_null() {
3973 destroy(self.blit_semaphore);
3974 self.blit_semaphore = ptr::null_mut();
3975 }
3976 let mut desc = CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC {
3977 type_: CUexternalSemaphoreHandleType::CU_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD,
3978 ..Default::default()
3979 };
3980 desc.handle.fd = cuda_fd.as_raw_fd();
3981 let mut semaphore: CUexternalSemaphore = ptr::null_mut();
3982 let r = import(&mut semaphore, &desc);
3983 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3984 if r != CUresult::CUDA_SUCCESS {
3985 return Err(format!(
3986 "CUDA refused the semaphore ({})",
3987 Self::get_error_string(&self.cuda, r)
3988 ));
3989 }
3990 let _ = cuda_fd.into_raw_fd();
3992 self.blit_semaphore = semaphore;
3993 Ok(server_fd)
3994 }
3995 }
3996
3997 fn encode_frame(
3998 &mut self,
3999 dmabuf: &Dmabuf,
4000 frame_number: u64,
4001 crf: u32,
4002 force_idr: bool,
4003 after_blit: bool,
4004 ) -> Result<Vec<u8>, String> {
4005 unsafe {
4006 self.reconfigure_if_needed(crf);
4007 let fd = dmabuf.handles().next().ok_or("No handles")?.as_raw_fd();
4008 let fmt = dmabuf.format();
4009 let modifier: u64 = fmt.modifier.into();
4010 let identity = DmaBufIdentity::probe(fd, modifier, self.width, self.height);
4011 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4012
4013 if after_blit {
4016 let wait = self.cuda.cuWaitExternalSemaphoresAsync;
4017 let queued = match wait {
4018 Some(wait) if !self.blit_semaphore.is_null() => {
4019 let params = CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS::default();
4020 wait(&self.blit_semaphore, ¶ms, 1, ptr::null_mut())
4021 }
4022 _ => CUresult::CUDA_ERROR_NOT_INITIALIZED,
4023 };
4024 if queued != CUresult::CUDA_SUCCESS {
4025 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4026 return Err(format!(
4027 "the wait for the server's blit could not be queued ({})",
4028 Self::get_error_string(&self.cuda, queued)
4029 ));
4030 }
4031 }
4032
4033 if self
4037 .dmabuf_cache
4038 .get(&fd)
4039 .is_some_and(|c| c.identity != identity)
4040 && let Some(stale) = self.dmabuf_cache.remove(&fd)
4041 {
4042 self.release_dmabuf_import(stale);
4043 }
4044
4045 if !self.dmabuf_cache.contains_key(&fd) {
4046 let Some(egl) = self.egl.clone() else {
4047 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4048 return Err("a session opened without an EGL display imports no dmabuf".into());
4049 };
4050 let stride = dmabuf.strides().next().unwrap_or(0) as i32;
4051 let offset = dmabuf.offsets().next().unwrap_or(0) as i32;
4052
4053 let attribs = [
4054 EGL_WIDTH,
4055 self.width as i32,
4056 EGL_HEIGHT,
4057 self.height as i32,
4058 EGL_LINUX_DRM_FOURCC_EXT,
4059 egl_import_fourcc(fmt.code) as i32,
4060 EGL_DMA_BUF_PLANE0_FD_EXT,
4061 fd,
4062 EGL_DMA_BUF_PLANE0_OFFSET_EXT,
4063 offset,
4064 EGL_DMA_BUF_PLANE0_PITCH_EXT,
4065 stride,
4066 EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
4067 (modifier & 0xFFFFFFFF) as i32,
4068 EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
4069 (modifier >> 32) as i32,
4070 EGL_NONE,
4071 ];
4072
4073 let egl_image = (egl.eglCreateImageKHR)(
4074 self.egl_display,
4075 ptr::null_mut(),
4076 EGL_LINUX_DMA_BUF_EXT,
4077 ptr::null_mut(),
4078 attribs.as_ptr(),
4079 );
4080 if egl_image == EGL_NO_IMAGE_KHR {
4081 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4082 return Err("Failed to create EGLImage".into());
4083 }
4084
4085 let mut cuda_resource: CUgraphicsResource = ptr::null_mut();
4086 if (self.cuda.cuGraphicsEGLRegisterImage)(&mut cuda_resource, egl_image, 1)
4087 != CUresult::CUDA_SUCCESS
4088 {
4089 (egl.eglDestroyImageKHR)(self.egl_display, egl_image);
4090 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4091 return Err("Failed to register EGLImage".into());
4092 }
4093
4094 let mut egl_frame: CUeglFrame = std::mem::zeroed();
4095 if (self.cuda.cuGraphicsResourceGetMappedEglFrame)(
4096 &mut egl_frame,
4097 cuda_resource,
4098 0,
4099 0,
4100 ) != CUresult::CUDA_SUCCESS
4101 {
4102 (self.cuda.cuGraphicsUnregisterResource)(cuda_resource);
4103 (egl.eglDestroyImageKHR)(self.egl_display, egl_image);
4104 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4105 return Err("Failed to map EGL frame".into());
4106 }
4107
4108 let input = match (
4109 self.direct_dmabuf,
4110 direct_plane(&egl_frame, self.width, self.height),
4111 fourcc_nvenc_format(fmt.code),
4112 ) {
4113 (true, Some(plane), Some(format)) => {
4114 self.register_direct_input(&egl_frame, plane, format)
4115 }
4116 _ => DmaBufInput::Copy,
4117 };
4118 crate::log::debug!(
4119 "[NVENC] dmabuf imported as a {} frame ({} planes, {}x{}, pitch {}, {} channels of element format {}): {}.",
4120 match egl_frame.frame_type {
4121 CU_EGL_FRAME_TYPE_PITCH => "pitch-linear",
4122 CU_EGL_FRAME_TYPE_ARRAY => "CUDA-array",
4123 _ => "unknown-kind",
4124 },
4125 egl_frame.plane_count,
4126 egl_frame.width,
4127 egl_frame.height,
4128 egl_frame.pitch,
4129 egl_frame.num_channels,
4130 egl_frame.cu_format,
4131 match input {
4132 DmaBufInput::Direct { .. } => "encoding in place",
4133 DmaBufInput::Copy => "copying per frame",
4134 }
4135 );
4136
4137 let tex = if self.csc.is_some() && egl_frame.frame_type == CU_EGL_FRAME_TYPE_ARRAY {
4140 ChromaConvert::texture_for(&self.cuda, egl_frame.frame.p_array[0])
4141 } else {
4142 0
4143 };
4144 self.dmabuf_cache.insert(
4145 fd,
4146 CachedDmaBuf {
4147 identity,
4148 egl_image,
4149 cuda_resource,
4150 egl_frame,
4151 input,
4152 tex,
4153 },
4154 );
4155 }
4156
4157 let (egl_frame, input, tex) = {
4158 let cached = self.dmabuf_cache.get(&fd).unwrap();
4159 (cached.egl_frame, cached.input, cached.tex)
4160 };
4161 let (mapped, format) = match input {
4162 DmaBufInput::Direct { mapped, format, .. } => (mapped, format),
4163 DmaBufInput::Copy => {
4164 let mut copy_params = CUDA_MEMCPY2D {
4165 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
4166 srcHost: ptr::null(),
4167 srcDevice: 0,
4168 srcArray: ptr::null_mut(),
4169 srcPitch: 0,
4170 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
4171 dstHost: ptr::null_mut(),
4172 dstDevice: self.input_device_ptr,
4173 dstArray: ptr::null_mut(),
4174 dstPitch: self.input_pitch,
4175 WidthInBytes: (self.width * 4) as usize,
4176 Height: self.height as usize,
4177 ..Default::default()
4178 };
4179 if egl_frame.frame_type == CU_EGL_FRAME_TYPE_ARRAY {
4180 copy_params.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_ARRAY;
4181 copy_params.srcArray = egl_frame.frame.p_array[0];
4182 } else {
4183 copy_params.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_DEVICE;
4184 copy_params.srcDevice = egl_frame.frame.p_pitch[0] as CUdeviceptr;
4185 copy_params.srcPitch = egl_frame.pitch as usize;
4186 }
4187 if let Some(format) = fourcc_nvenc_format(fmt.code)
4190 && let Err(e) = self.set_input_format(format)
4191 {
4192 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4193 return Err(e);
4194 }
4195 if (self.cuda.cuMemcpy2DAsync_v2)(©_params, ptr::null_mut())
4196 != CUresult::CUDA_SUCCESS
4197 {
4198 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4199 return Err("Sanitization copy failed".into());
4200 }
4201 (self.mapped_input_buffer, self.input_format)
4202 }
4203 };
4204
4205 let swap = format == NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR;
4209 let converted = match input {
4210 DmaBufInput::Copy => {
4211 self.convert_packed(self.input_device_ptr, self.input_pitch, swap)
4212 }
4213 DmaBufInput::Direct { .. } if egl_frame.frame_type == CU_EGL_FRAME_TYPE_PITCH => {
4214 self.convert_packed(
4215 egl_frame.frame.p_pitch[0] as CUdeviceptr,
4216 egl_frame.pitch as usize,
4217 swap,
4218 )
4219 }
4220 DmaBufInput::Direct { .. } => self.convert_texture(tex, swap),
4221 };
4222 let (mapped, format) = match converted {
4223 Ok(Some(converted)) => converted,
4224 Ok(None) => (mapped, format),
4225 Err(e) => {
4226 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4227 return Err(e);
4228 }
4229 };
4230
4231 let result = self.submit_frame(mapped, format, frame_number, force_idr);
4232 if result.is_err() {
4233 (self.cuda.cuStreamSynchronize)(ptr::null_mut());
4234 }
4235 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4236 result
4237 }
4238 }
4239
4240 unsafe fn register_direct_input(
4246 &mut self,
4247 frame: &CUeglFrame,
4248 plane: DirectPlane,
4249 format: NV_ENC_BUFFER_FORMAT,
4250 ) -> DmaBufInput {
4251 let (resource_type, resource, pitch) = match plane {
4252 DirectPlane::Pitch(pitch) => (
4253 NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
4254 frame.frame.p_pitch[0],
4255 pitch,
4256 ),
4257 DirectPlane::Array(row_bytes) => (
4258 NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDAARRAY,
4259 frame.frame.p_array[0] as *mut c_void,
4260 row_bytes,
4261 ),
4262 };
4263 let mut reg_res = NV_ENC_REGISTER_RESOURCE {
4264 version: sv(NvStruct::RegisterResource),
4265 resourceType: resource_type,
4266 width: self.width,
4267 height: self.height,
4268 resourceToRegister: resource,
4269 pitch,
4270 bufferFormat: format,
4271 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
4272 ..Default::default()
4273 };
4274 let st =
4275 (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res);
4276 if st != NVENCSTATUS::NV_ENC_SUCCESS {
4277 eprintln!(
4278 "[NVENC] dmabuf plane registration ({plane:?}) refused ({st:?}); copying per frame."
4279 );
4280 return DmaBufInput::Copy;
4281 }
4282 let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
4283 version: sv(NvStruct::MapInputResource),
4284 registeredResource: reg_res.registeredResource,
4285 ..Default::default()
4286 };
4287 let st = (self.nvenc_funcs.nvEncMapInputResource.unwrap())(
4288 self.encoder_session,
4289 &mut map_params,
4290 );
4291 if st != NVENCSTATUS::NV_ENC_SUCCESS {
4292 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
4293 self.encoder_session,
4294 reg_res.registeredResource,
4295 );
4296 eprintln!("[NVENC] dmabuf plane mapping refused ({st:?}); copying per frame.");
4297 return DmaBufInput::Copy;
4298 }
4299 DmaBufInput::Direct {
4300 registered: reg_res.registeredResource,
4301 mapped: map_params.mappedResource,
4302 format,
4303 }
4304 }
4305
4306 pub fn encode_cpu_argb(
4309 &mut self,
4310 argb: &[u8],
4311 src_stride: usize,
4312 frame_number: u64,
4313 crf: u32,
4314 force_idr: bool,
4315 ) -> Result<Vec<u8>, String> {
4316 self.encode_cpu_packed(argb, src_stride, false, frame_number, crf, force_idr)
4317 }
4318
4319 pub fn encode_cpu_packed(
4344 &mut self,
4345 pixels: &[u8],
4346 src_stride: usize,
4347 rgba_input: bool,
4348 frame_number: u64,
4349 crf: u32,
4350 force_idr: bool,
4351 ) -> Result<Vec<u8>, String> {
4352 unsafe {
4353 self.reconfigure_if_needed(crf);
4354 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4355
4356 let width_bytes = (self.width * 4) as usize;
4357 let rows = self.height as usize;
4358 let needed = if rows == 0 {
4359 0
4360 } else {
4361 src_stride * (rows - 1) + width_bytes
4362 };
4363 if src_stride < width_bytes || pixels.len() < needed {
4364 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4365 return Err(format!(
4366 "packed buffer too small: len={} need>={} (stride={}, {}x{})",
4367 pixels.len(),
4368 needed,
4369 src_stride,
4370 self.width,
4371 self.height
4372 ));
4373 }
4374
4375 let format = if rgba_input {
4376 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
4377 } else {
4378 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
4379 };
4380 if self.csc.is_none()
4383 && let Err(e) = self.set_input_format(format)
4384 {
4385 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4386 return Err(e);
4387 }
4388
4389 if self.pin_uploads {
4390 self.pin_host_source(pixels.as_ptr() as usize, pixels.len());
4391 }
4392
4393 let copy = CUDA_MEMCPY2D {
4394 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
4395 srcHost: pixels.as_ptr() as *const c_void,
4396 srcPitch: src_stride,
4397 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
4398 dstDevice: self.input_device_ptr,
4399 dstPitch: self.input_pitch,
4400 WidthInBytes: width_bytes,
4401 Height: rows,
4402 ..Default::default()
4403 };
4404 if (self.cuda.cuMemcpy2DAsync_v2)(©, ptr::null_mut()) != CUresult::CUDA_SUCCESS {
4405 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4406 return Err("packed host->device upload failed".into());
4407 }
4408
4409 let (mapped, submitted) =
4410 match self.convert_packed(self.input_device_ptr, self.input_pitch, rgba_input) {
4411 Ok(Some(converted)) => converted,
4412 Ok(None) => (self.mapped_input_buffer, self.input_format),
4413 Err(e) => {
4414 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4415 return Err(e);
4416 }
4417 };
4418 let result = self.submit_frame(mapped, submitted, frame_number, force_idr);
4419 if result.is_err() {
4420 (self.cuda.cuStreamSynchronize)(ptr::null_mut());
4421 }
4422 self.upload_hash = None;
4423 if let Some(words) = self.upload_words.take()
4424 && let Some(Some(hash)) = self.band_hash.as_mut()
4425 {
4426 self.upload_hashes = hash.read(&self.cuda, words);
4427 }
4428 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4429 result
4430 }
4431 }
4432
4433 unsafe fn convert_texture(
4438 &self,
4439 tex: CUtexObject,
4440 rgba_input: bool,
4441 ) -> Result<Option<(NV_ENC_INPUT_PTR, NV_ENC_BUFFER_FORMAT)>, String> {
4442 match self.csc.as_ref() {
4443 Some(csc) if tex != 0 => {
4444 csc.run_texture(&self.cuda, tex, self.width, self.height, rgba_input)?;
4445 Ok(Some(csc.input()))
4446 }
4447 _ if self.bit_depth > 8 => {
4448 Err("the driver gave no texture for the import a 10-bit session converts".into())
4449 }
4450 _ => Ok(None),
4451 }
4452 }
4453
4454 unsafe fn convert_packed(
4458 &self,
4459 src: CUdeviceptr,
4460 src_pitch: usize,
4461 rgba_input: bool,
4462 ) -> Result<Option<(NV_ENC_INPUT_PTR, NV_ENC_BUFFER_FORMAT)>, String> {
4463 match self.csc.as_ref() {
4464 Some(csc) => {
4465 csc.run(
4466 &self.cuda,
4467 src,
4468 src_pitch,
4469 self.width,
4470 self.height,
4471 rgba_input,
4472 )?;
4473 Ok(Some(csc.input()))
4474 }
4475 None => Ok(None),
4476 }
4477 }
4478
4479 pub fn width(&self) -> u32 {
4481 self.width
4482 }
4483
4484 pub fn height(&self) -> u32 {
4486 self.height
4487 }
4488
4489 pub(crate) fn push_context(&self) -> bool {
4493 unsafe { (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context) == CUresult::CUDA_SUCCESS }
4494 }
4495
4496 pub(crate) fn pop_context(&self) {
4498 unsafe {
4499 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4500 }
4501 }
4502
4503 pub fn release_external_input(&mut self) {
4509 if self.external_input.is_none() {
4510 return;
4511 }
4512 unsafe {
4513 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4514 self.unmap_external_input();
4515 let _ = (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4516 }
4517 }
4518
4519 unsafe fn unmap_external_input(&mut self) {
4521 if let Some(ext) = self.external_input.take() {
4522 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(self.encoder_session, ext.mapped);
4523 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
4524 self.encoder_session,
4525 ext.registered,
4526 );
4527 }
4528 }
4529
4530 unsafe fn register_external_input(
4537 &mut self,
4538 device_ptr: CUdeviceptr,
4539 pitch: usize,
4540 format: NV_ENC_BUFFER_FORMAT,
4541 ) -> Result<NV_ENC_INPUT_PTR, String> {
4542 if let Some(ext) = self.external_input
4543 && ext.device_ptr == device_ptr
4544 && ext.pitch == pitch
4545 && ext.format == format
4546 && ext.width == self.width
4547 && ext.height == self.height
4548 {
4549 return Ok(ext.mapped);
4550 }
4551 self.unmap_external_input();
4552 let mut reg_res = NV_ENC_REGISTER_RESOURCE {
4553 version: sv(NvStruct::RegisterResource),
4554 resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
4555 width: self.width,
4556 height: self.height,
4557 resourceToRegister: device_ptr as *mut c_void,
4558 pitch: pitch as u32,
4559 bufferFormat: format,
4560 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
4561 ..Default::default()
4562 };
4563 let st =
4564 (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res);
4565 if st != NVENCSTATUS::NV_ENC_SUCCESS {
4566 return Err(format!(
4567 "failed to register the captured frame as an NVENC input ({st:?})"
4568 ));
4569 }
4570 let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
4571 version: sv(NvStruct::MapInputResource),
4572 registeredResource: reg_res.registeredResource,
4573 ..Default::default()
4574 };
4575 let st = (self.nvenc_funcs.nvEncMapInputResource.unwrap())(
4576 self.encoder_session,
4577 &mut map_params,
4578 );
4579 if st != NVENCSTATUS::NV_ENC_SUCCESS {
4580 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
4581 self.encoder_session,
4582 reg_res.registeredResource,
4583 );
4584 return Err(format!(
4585 "failed to map the captured frame as an NVENC input ({st:?})"
4586 ));
4587 }
4588 self.external_input = Some(ExternalInput {
4589 device_ptr,
4590 pitch,
4591 width: self.width,
4592 height: self.height,
4593 format,
4594 registered: reg_res.registeredResource,
4595 mapped: map_params.mappedResource,
4596 });
4597 Ok(map_params.mappedResource)
4598 }
4599
4600 pub fn encode_cuda_pitch(
4615 &mut self,
4616 device_ptr: CUdeviceptr,
4617 pitch: usize,
4618 rgba: bool,
4619 frame_number: u64,
4620 crf: u32,
4621 force_idr: bool,
4622 ) -> Result<Vec<u8>, String> {
4623 unsafe {
4624 self.reconfigure_if_needed(crf);
4625 if pitch < self.width as usize * 4 || !pitch.is_multiple_of(4) {
4628 return Err(format!(
4629 "external input pitch {pitch} does not cover {}x{} at 4-byte alignment",
4630 self.width, self.height
4631 ));
4632 }
4633 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4634 let format = if rgba {
4635 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
4636 } else {
4637 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
4638 };
4639 let submitted = match self.convert_packed(device_ptr, pitch, rgba) {
4640 Ok(Some(converted)) => Ok(converted),
4641 Ok(None) => self
4642 .register_external_input(device_ptr, pitch, format)
4643 .map(|m| (m, format)),
4644 Err(e) => Err(e),
4645 };
4646 let (mapped, submitted) = match submitted {
4647 Ok(pair) => pair,
4648 Err(e) => {
4649 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4650 return Err(e);
4651 }
4652 };
4653 let result = self.submit_frame(mapped, submitted, frame_number, force_idr);
4654 if result.is_err() {
4655 (self.cuda.cuStreamSynchronize)(ptr::null_mut());
4656 }
4657 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4658 result
4659 }
4660 }
4661
4662 pub fn hash_next_upload(&mut self, rows: u32) -> bool {
4667 self.upload_hashes = None;
4668 self.upload_words = None;
4669 let available = unsafe {
4670 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4671 let cuda = &self.cuda;
4672 let loaded = self
4673 .band_hash
4674 .get_or_insert_with(|| BandHash::new(cuda))
4675 .is_some();
4676 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4677 loaded
4678 };
4679 self.upload_hash = available.then_some(rows);
4680 available
4681 }
4682
4683 pub fn take_upload_hashes(&mut self) -> Option<Vec<u64>> {
4685 self.upload_hashes.take()
4686 }
4687
4688 pub fn band_hashes(
4693 &mut self,
4694 device_ptr: CUdeviceptr,
4695 pitch: usize,
4696 width: u32,
4697 height: u32,
4698 rows: u32,
4699 ) -> Option<Vec<u64>> {
4700 if pitch < width as usize * 4 {
4701 return None;
4702 }
4703 unsafe {
4704 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
4705 let cuda = &self.cuda;
4706 let hashes = self
4707 .band_hash
4708 .get_or_insert_with(|| BandHash::new(cuda))
4709 .as_mut()
4710 .and_then(|h| h.run(cuda, device_ptr, pitch, width, height, rows));
4711 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
4712 hashes
4713 }
4714 }
4715}
4716
4717#[cfg(test)]
4718mod tests {
4719 use super::*;
4720
4721 #[test]
4725 fn every_codec_declares_the_conversion_matrix() {
4726 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
4727 for fullcolor in [false, true] {
4728 let mut config = NV_ENC_CONFIG {
4729 version: sv(NvStruct::Config),
4730 ..Default::default()
4731 };
4732 NvencEncoder::configure_codec(
4733 &mut config,
4734 codec,
4735 fullcolor,
4736 8,
4737 nvenc_level(codec, 1280, 720, 60, 0, true),
4738 1,
4739 0,
4740 &NvencTuning::default(),
4741 );
4742 let got = unsafe {
4743 match codec {
4744 Codec::H265 => {
4745 config
4746 .encodeCodecConfig
4747 .hevcConfig
4748 .hevcVUIParameters
4749 .colourMatrix
4750 }
4751 Codec::Av1 => config.encodeCodecConfig.av1Config.matrixCoefficients,
4752 _ => {
4753 config
4754 .encodeCodecConfig
4755 .h264Config
4756 .h264VUIParameters
4757 .colourMatrix
4758 }
4759 }
4760 };
4761 assert_eq!(
4762 got as u32,
4763 NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709 as u32,
4764 "{codec:?} 4:4:4={fullcolor}"
4765 );
4766 }
4767 }
4768 }
4769
4770 #[test]
4771 fn anchors_require_the_negotiated_api_and_device_capacity() {
4772 for acked in [false, true] {
4773 assert_eq!(anchor_count(Codec::Av1, 12, true, Some(6), 4, acked), 0);
4774 assert_eq!(anchor_count(Codec::Av1, 13, false, Some(6), 4, acked), 0);
4775 assert_eq!(anchor_count(Codec::Av1, 13, true, None, 4, acked), 0);
4776 assert_eq!(anchor_count(Codec::Av1, 13, true, Some(0), 4, acked), 0);
4777 assert_eq!(anchor_count(Codec::Av1, 13, true, Some(6), 2, acked), 0);
4778 assert_eq!(anchor_count(Codec::Av1, 13, true, Some(1), 3, acked), 1);
4779 for api in [11, 12, 13] {
4780 assert_eq!(
4781 anchor_count(Codec::H265, api, true, Some(8), 4, acked),
4782 ANCHORS
4783 );
4784 for codec in [Codec::H264, Codec::H265] {
4785 assert_eq!(anchor_count(codec, api, false, Some(8), 4, acked), 0);
4786 assert_eq!(anchor_count(codec, api, true, Some(0), 4, acked), 0);
4787 assert_eq!(anchor_count(codec, api, true, Some(8), 2, acked), 0);
4788 }
4789 }
4790 }
4791 assert_eq!(anchor_count(Codec::Av1, 13, true, Some(6), 4, false), 1);
4792 assert_eq!(
4793 anchor_count(Codec::Av1, 13, true, Some(6), 4, true),
4794 ANCHORS
4795 );
4796 for api in [11, 12, 13] {
4797 for acked in [false, true] {
4798 assert_eq!(anchor_count(Codec::H264, api, true, Some(8), 4, acked), 1);
4799 }
4800 }
4801 assert_eq!(anchor_count(Codec::Av1, 13, true, Some(8), 3, true), 1);
4803 assert_eq!(anchor_count(Codec::Av1, 13, true, Some(1), 4, true), 1);
4804 }
4805
4806 #[test]
4809 fn av1_anchor_configuration_matches_the_reference_window() {
4810 for anchors in [0, 1] {
4811 let mut config = NV_ENC_CONFIG::default();
4812 NvencEncoder::configure_codec(
4813 &mut config,
4814 Codec::Av1,
4815 false,
4816 8,
4817 13,
4818 AV1_REFERENCES,
4819 anchors,
4820 &NvencTuning::default(),
4821 );
4822 let av1 = unsafe { config.encodeCodecConfig.av1Config };
4823 assert_eq!(av1.enableLTR(), anchors as u32);
4824 assert_eq!(av1.ltrNumFrames, anchors as u32);
4825 assert_eq!(av1.maxNumRefFramesInDPB, AV1_REFERENCES);
4826 }
4827 }
4828
4829 #[test]
4832 fn a_band_covers_its_share_of_the_blocks() {
4833 let mut map = Vec::new();
4834 fill_band_map(&mut map, Codec::H264, 1920, 1080, (0.0, 1.0 / 64.0), -33);
4835 assert_eq!(map.len(), 120 * 68);
4836 assert_eq!(map.iter().filter(|&&d| d == -33).count(), 128);
4837 assert!(map[..128].iter().all(|&d| d == -33) && map[128..].iter().all(|&d| d == 0));
4838 fill_band_map(&mut map, Codec::H265, 1920, 1080, (0.5, 0.75), -80);
4839 assert_eq!(map.len(), 60 * 34);
4840 assert!(map[..1020].iter().all(|&d| d == 0) && map[1530..].iter().all(|&d| d == 0));
4841 assert!(
4842 map[1020..1530].iter().all(|&d| d == -51),
4843 "the delta is bounded to the quantizer range"
4844 );
4845 fill_band_map(&mut map, Codec::Av1, 1920, 1080, (0.0, 1.0), -211);
4846 assert_eq!(map.len(), 30 * 17);
4847 assert!(
4848 map.iter().all(|&d| d == -128),
4849 "an AV1 delta is bounded to what the map holds"
4850 );
4851 let covered = |a: f64, b: f64| {
4852 let mut m = Vec::new();
4853 fill_band_map(&mut m, Codec::H264, 1280, 720, (a, b), -1);
4854 m.iter().map(|&d| d != 0).collect::<Vec<bool>>()
4855 };
4856 let (x, y) = (covered(0.0, 0.3), covered(0.3, 1.0));
4857 assert!(
4858 x.iter().zip(&y).all(|(a, b)| *a || *b),
4859 "adjacent bands leave no block out"
4860 );
4861 }
4862}
4863
4864#[cfg(test)]
4865mod gpu_tests {
4866 use super::*;
4867 use crate::encoders::reference::REFERENCE_FRAMES;
4868
4869 fn settings(w: i32, h: i32, fps: f64) -> RustCaptureSettings {
4871 RustCaptureSettings {
4872 width: w,
4873 height: h,
4874 codec: Codec::H264,
4875 target_fps: fps,
4876 video_crf: 25,
4877 ..Default::default()
4878 }
4879 }
4880
4881 fn host_session(s: &RustCaptureSettings) -> Result<NvencEncoder, String> {
4886 NvencEncoder::new(s, ptr::null()).map(|mut enc| {
4887 enc.pin_uploads = false;
4888 enc
4889 })
4890 }
4891
4892 fn frame(w: usize, h: usize, seed: u8) -> Vec<u8> {
4895 let mut f = vec![0u8; w * h * 4];
4896 for (i, px) in f.as_chunks_mut::<4>().0.iter_mut().enumerate() {
4897 let v = ((i as u32).wrapping_mul(2654435761) >> 24) as u8;
4898 px[0] = v.wrapping_add(seed);
4899 px[1] = v ^ seed;
4900 px[2] = seed;
4901 px[3] = 255;
4902 }
4903 f
4904 }
4905
4906 fn reconfigure_raw(enc: &mut NvencEncoder) -> (NVENCSTATUS, String) {
4909 unsafe {
4910 let status = enc.reconfigure(false, false);
4911 let detail = if status == NVENCSTATUS::NV_ENC_SUCCESS {
4912 "accepted".to_string()
4913 } else {
4914 last_error(&enc.nvenc_funcs, enc.encoder_session)
4915 };
4916 (status, detail)
4917 }
4918 }
4919
4920 fn apart_without(codec: Codec, frames: &[Vec<u8>], lost: std::ops::Range<usize>) -> f64 {
4923 use crate::webcam::decode::{Decoder as _, VideoDecoder};
4924 let (mut whole, mut lossy) = (
4925 VideoDecoder::new(codec).unwrap(),
4926 VideoDecoder::new(codec).unwrap(),
4927 );
4928 for (i, f) in frames.iter().enumerate() {
4929 assert!(whole.decode(f).expect("decode"), "{codec:?} frame {i}");
4930 if !lost.contains(&i) {
4931 assert!(
4932 lossy.decode(f).expect("decode past the loss"),
4933 "{codec:?} frame {i}"
4934 );
4935 }
4936 }
4937 luma_apart(&whole.frame().unwrap(), &lossy.frame().unwrap())
4938 }
4939
4940 fn luma_apart(
4942 a: &crate::webcam::convert::I420View<'_>,
4943 b: &crate::webcam::convert::I420View<'_>,
4944 ) -> f64 {
4945 a.y.chunks(a.y_stride)
4946 .zip(b.y.chunks(b.y_stride))
4947 .take(a.height)
4948 .flat_map(|(ra, rb)| {
4949 ra[..a.width]
4950 .iter()
4951 .zip(&rb[..a.width])
4952 .map(|(&x, &y)| (x as f64 - y as f64).abs())
4953 })
4954 .sum::<f64>()
4955 / (a.width * a.height) as f64
4956 }
4957
4958 fn apart_keeping(codec: Codec, frames: &[Vec<u8>], keep: &[usize]) -> Option<f64> {
4962 use crate::webcam::decode::{Decoder as _, VideoDecoder};
4963 let (mut whole, mut part) = (
4964 VideoDecoder::new(codec).unwrap(),
4965 VideoDecoder::new(codec).unwrap(),
4966 );
4967 let mut got = false;
4968 for (i, f) in frames.iter().enumerate() {
4969 let _ = whole.decode(f);
4970 if keep.contains(&i) {
4971 got = part.decode(f).unwrap_or(false) && i == frames.len() - 1;
4972 }
4973 }
4974 if !got {
4975 return None;
4976 }
4977 Some(luma_apart(&whole.frame()?, &part.frame()?))
4978 }
4979
4980 fn moving_frame(w: usize, h: usize, step: usize) -> Vec<u8> {
4983 let mut f = frame(w, h, 10);
4984 let block = frame(256, 256, 200);
4985 let x0 = (step * 64) % (w - 256);
4986 for row in 0..256.min(h) {
4987 let dst = (row * w + x0) * 4;
4988 f[dst..dst + 256 * 4].copy_from_slice(&block[row * 256 * 4..(row + 1) * 256 * 4]);
4989 }
4990 f
4991 }
4992
4993 fn rbsp(nal: &[u8]) -> Vec<u8> {
4995 let mut out = Vec::with_capacity(nal.len());
4996 let mut zeros = 0usize;
4997 for &b in nal {
4998 if zeros >= 2 && b == 3 {
4999 zeros = 0;
5000 continue;
5001 }
5002 zeros = if b == 0 { zeros + 1 } else { 0 };
5003 out.push(b);
5004 }
5005 out
5006 }
5007
5008 fn luma_psnr(
5011 dec: &mut crate::webcam::decode::VideoDecoder,
5012 pkt: &[u8],
5013 src: &[u8],
5014 w: usize,
5015 h: usize,
5016 ) -> f64 {
5017 use crate::webcam::decode::Decoder;
5018 assert!(
5019 dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
5020 "no picture"
5021 );
5022 let v = dec.frame().expect("decoded frame");
5023 let mut se = 0f64;
5024 for y in 0..h {
5025 for x in 0..w {
5026 let p = &src[(y * w + x) * 4..(y * w + x) * 4 + 3];
5027 let luma = crate::encoders::chroma_siting::ycbcr(
5028 [p[2], p[1], p[0]].map(f64::from),
5029 crate::encoders::chroma_siting::BT709,
5030 )[0];
5031 let d = v.y[y * v.y_stride + x] as f64 - luma;
5032 se += d * d;
5033 }
5034 }
5035 let mse = se / (w * h) as f64;
5036 if mse == 0.0 {
5037 99.0
5038 } else {
5039 10.0 * (255.0f64 * 255.0 / mse).log10()
5040 }
5041 }
5042
5043 fn cbr_row(
5047 label: &str,
5048 enc: &mut NvencEncoder,
5049 codec: Codec,
5050 seq: &[&Vec<u8>],
5051 w: usize,
5052 h: usize,
5053 fps: usize,
5054 ) {
5055 use crate::webcam::decode::VideoDecoder;
5056 let n = seq.len() - 1;
5057 let first = enc
5058 .encode_cpu_packed(seq[0], w * 4, false, 0, 25, true)
5059 .expect("warm-up");
5060 let mut pkts: Vec<Vec<u8>> = Vec::with_capacity(n);
5061 per_frame(label, n, |i| {
5062 pkts.push(
5063 enc.encode_cpu_packed(seq[1 + i], w * 4, false, 1 + i as u64, 25, false)
5064 .expect("encode"),
5065 );
5066 });
5067 let sizes: Vec<usize> = pkts
5068 .iter()
5069 .map(|p| p.len().saturating_sub(VIDEO_HEADER_LEN))
5070 .collect();
5071 let bytes: usize = sizes.iter().sum();
5072 let mut dec = VideoDecoder::new(codec).expect("decoder");
5073 luma_psnr(&mut dec, &first, seq[0], w, h);
5074 let psnr: Vec<f64> = pkts
5075 .iter()
5076 .enumerate()
5077 .map(|(i, p)| luma_psnr(&mut dec, p, seq[1 + i], w, h))
5078 .collect();
5079 println!(
5080 " {} kbps, frames {}..{} kbit, luma PSNR {:.1} dB mean, {:.1} dB worst",
5081 bytes * 8 * fps / n / 1000,
5082 sizes.iter().min().unwrap() * 8 / 1000,
5083 sizes.iter().max().unwrap() * 8 / 1000,
5084 psnr.iter().sum::<f64>() / n as f64,
5085 psnr.iter().cloned().fold(f64::INFINITY, f64::min),
5086 );
5087 }
5088
5089 fn wire_dims(pkt: &[u8]) -> (u16, u16) {
5091 (
5092 u16::from_be_bytes([pkt[6], pkt[7]]),
5093 u16::from_be_bytes([pkt[8], pkt[9]]),
5094 )
5095 }
5096
5097 #[test]
5104 #[ignore]
5105 fn gpu_resolution_reconfigure_roundtrip() {
5106 let mut s = settings(1280, 720, 60.0);
5107 let t0 = std::time::Instant::now();
5108 let mut enc = host_session(&s).expect("NVENC init");
5109 let init_ms = t0.elapsed().as_secs_f64() * 1000.0;
5110
5111 let mut stream: Vec<u8> = Vec::new();
5112 let f720 = frame(1280, 720, 10);
5113 for i in 0..5u64 {
5114 let pkt = enc
5115 .encode_cpu_argb(&f720, 1280 * 4, i, 25, i == 0)
5116 .expect("encode 720p");
5117 assert_eq!(wire_dims(&pkt), (1280, 720));
5118 stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5119 }
5120
5121 assert!(
5122 !enc.reconfigure_resolution(&s)
5123 .expect("same-size reconfigure"),
5124 "unchanged dimensions must not reset the session"
5125 );
5126 let pkt = enc
5127 .encode_cpu_argb(&f720, 1280 * 4, 5, 25, false)
5128 .expect("encode after same-size reconfigure");
5129 assert_eq!(
5130 pkt[1] & 0x0f,
5131 FRAME_DELTA,
5132 "the stream continues without an IDR at unchanged dimensions"
5133 );
5134 assert!(
5135 pkt.len() > VIDEO_HEADER_LEN,
5136 "a locked bitstream carries the encoded picture"
5137 );
5138 stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5139
5140 s.width = 1920;
5141 s.height = 1080;
5142 let t1 = std::time::Instant::now();
5143 assert!(enc.reconfigure_resolution(&s).expect("grow reconfigure"));
5144 let grow_ms = t1.elapsed().as_secs_f64() * 1000.0;
5145 let f1080 = frame(1920, 1080, 40);
5146 let pkt = enc
5147 .encode_cpu_argb(&f1080, 1920 * 4, 6, 25, false)
5148 .expect("encode 1080p");
5149 assert_eq!(pkt[0], 0x04);
5150 assert_eq!(
5151 pkt[1] & 0x0f,
5152 FRAME_KEY,
5153 "first frame after a resize must be an IDR"
5154 );
5155 assert_eq!(wire_dims(&pkt), (1920, 1080));
5156 assert!(
5157 pkt.len() > VIDEO_HEADER_LEN,
5158 "a locked bitstream carries the encoded picture"
5159 );
5160 stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5161 for i in 7..10u64 {
5162 let pkt = enc
5163 .encode_cpu_argb(&f1080, 1920 * 4, i, 25, false)
5164 .expect("encode 1080p");
5165 assert_eq!(
5166 pkt[1] & 0x0f,
5167 FRAME_DELTA,
5168 "steady frames after the IDR are P frames"
5169 );
5170 stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5171 }
5172
5173 s.width = 640;
5174 s.height = 480;
5175 let t2 = std::time::Instant::now();
5176 assert!(enc.reconfigure_resolution(&s).expect("shrink reconfigure"));
5177 let shrink_ms = t2.elapsed().as_secs_f64() * 1000.0;
5178 let f480 = frame(640, 480, 70);
5179 let pkt = enc
5180 .encode_cpu_argb(&f480, 640 * 4, 10, 25, false)
5181 .expect("encode 480p");
5182 assert_eq!(pkt[1] & 0x0f, FRAME_KEY);
5183 assert_eq!(wire_dims(&pkt), (640, 480));
5184 stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5185
5186 s.width = 4100;
5187 s.height = 2400;
5188 assert!(enc.reconfigure_resolution(&s).is_err(), "beyond headroom");
5189 s.width = 640;
5190 s.height = 480;
5191 s.video_fullcolor = true;
5192 assert!(enc.reconfigure_resolution(&s).is_err(), "chroma flip");
5193 s.video_fullcolor = false;
5194 s.video_cbr_mode = true;
5195 assert!(enc.reconfigure_resolution(&s).is_err(), "RC mode flip");
5196 s.video_cbr_mode = false;
5197 let pkt = enc
5198 .encode_cpu_argb(&f480, 640 * 4, 11, 25, false)
5199 .expect("session survives rejected reconfigures");
5200 stream.extend_from_slice(&pkt[VIDEO_HEADER_LEN..]);
5201
5202 println!(
5203 "init={init_ms:.1}ms grow(720p->1080p)={grow_ms:.1}ms shrink(1080p->480p)={shrink_ms:.1}ms"
5204 );
5205 if let Ok(path) = std::env::var("NVENC_TEST_DUMP") {
5206 std::fs::write(&path, &stream).unwrap();
5207 println!("wrote {} bytes to {path}", stream.len());
5208 }
5209 }
5210
5211 #[test]
5219 #[ignore]
5220 fn gpu_codec_sessions_encode_and_decode() {
5221 use crate::encoders::codec::{
5222 FRAME_DELTA, FRAME_KEY, av1_is_key, h264_frame_type, h265_frame_type, parse_video_type,
5223 };
5224 use crate::webcam::decode::{Decoder, VideoDecoder};
5225 let (w, h) = (1280usize, 720usize);
5226 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5227 let mut s = settings(w as i32, h as i32, 60.0);
5228 s.codec = codec;
5229 let mut enc = match host_session(&s) {
5230 Ok(enc) => enc,
5231 Err(e) => {
5232 println!("{codec:?}: {e}");
5233 assert!(
5234 e.contains("engine"),
5235 "a missing codec must be refused as such: {e}"
5236 );
5237 continue;
5238 }
5239 };
5240 assert_eq!(enc.codec(), codec);
5241 assert!(enc.egl.is_none(), "a session on host frames loads no EGL");
5242 let mut dec = VideoDecoder::new(codec).expect("decoder");
5243 for i in 0..6u64 {
5244 let src = frame(w, h, 10 + i as u8);
5245 let pkt = enc
5246 .encode_cpu_argb(&src, w * 4, i, 25, i == 0)
5247 .expect("encode");
5248 let (wire_codec, kind) = parse_video_type(pkt[1]).expect("video type byte");
5249 assert_eq!(wire_codec, codec);
5250 assert_eq!(
5251 kind,
5252 if i == 0 { FRAME_KEY } else { FRAME_DELTA },
5253 "{codec:?} frame {i}"
5254 );
5255 let payload = &pkt[VIDEO_HEADER_LEN..];
5256 if codec != Codec::Av1 {
5257 assert!(
5258 payload.starts_with(&[0, 0, 0, 1]),
5259 "{codec:?} frame {i}: the bitstream starts right after the header"
5260 );
5261 }
5262 let read = match codec {
5263 Codec::H264 => h264_frame_type(payload),
5264 Codec::H265 => h265_frame_type(payload),
5265 _ => {
5266 if av1_is_key(payload) {
5267 FRAME_KEY
5268 } else {
5269 FRAME_DELTA
5270 }
5271 }
5272 };
5273 assert_eq!(
5274 read, kind,
5275 "{codec:?} frame {i}: the bitstream disagrees with pictureType"
5276 );
5277 assert!(
5278 dec.decode(payload).expect("decode"),
5279 "{codec:?} frame {i} decoded nothing"
5280 );
5281 let f = dec.frame().unwrap();
5282 assert_eq!((f.width, f.height), (w, h));
5283 }
5284 let key = enc
5285 .encode_cpu_argb(&frame(w, h, 99), w * 4, 6, 25, true)
5286 .expect("forced key");
5287 assert_eq!(parse_video_type(key[1]), Some((codec, FRAME_KEY)));
5288 let mut fresh = VideoDecoder::new(codec).expect("decoder");
5289 assert!(
5290 fresh.decode(&key[VIDEO_HEADER_LEN..]).expect("decode"),
5291 "{codec:?}: a forced key frame must decode alone"
5292 );
5293
5294 let mut full = s.clone();
5295 full.video_fullcolor = true;
5296 let enc = host_session(&full).expect("4:4:4 request");
5297 if codec.fullcolor() {
5298 println!("{codec:?} 4:4:4: {}", enc.is_fullcolor());
5299 } else {
5300 assert!(!enc.is_fullcolor(), "{codec:?} never carries 4:4:4");
5301 }
5302 }
5303 }
5304
5305 #[test]
5310 #[ignore]
5311 fn gpu_ten_bit_follows_the_device() {
5312 use crate::webcam::decode::{Decoder, VideoDecoder};
5313 let (w, h) = (1280usize, 720usize);
5314 for (codec, formats) in probe_codecs(0).expect("probe") {
5315 for fullcolor in [false, codec.fullcolor()] {
5316 let mut s = settings(w as i32, h as i32, 60.0);
5317 s.codec = codec;
5318 s.video_fullcolor = fullcolor;
5319 s.video_bit_depth = 10;
5320 let mut enc = host_session(&s).expect("session");
5321 let carried = formats.ten_bit[(fullcolor && formats.fullcolor) as usize];
5322 println!("{codec:?} 4:4:4 {fullcolor}: {}-bit", enc.bit_depth());
5323 assert_eq!(enc.bit_depth(), if carried { 10 } else { 8 }, "{codec:?}");
5324 assert_eq!(
5325 enc.csc.as_ref().map(|c| c.layout),
5326 ConvertLayout::of(enc.is_fullcolor(), enc.bit_depth()),
5327 "{codec:?}"
5328 );
5329 let key = enc
5330 .encode_cpu_argb(&frame(w, h, 1), w * 4, 0, 25, true)
5331 .expect("key frame");
5332 let decoded = VideoDecoder::new(codec)
5333 .expect("decoder")
5334 .decode(&key[VIDEO_HEADER_LEN..]);
5335 if carried {
5336 let refusal = format!("{:?}", decoded.expect_err("an 8-bit decoder"));
5337 assert!(refusal.contains("bit depth"), "{codec:?}: {refusal}");
5338 } else if !enc.is_fullcolor() {
5339 assert!(decoded.expect("decode"), "{codec:?}");
5340 }
5341 s.width = 1920;
5342 s.height = 1080;
5343 enc.reconfigure_resolution(&s).expect("resize");
5344 assert_eq!(enc.bit_depth(), if carried { 10 } else { 8 });
5345 enc.encode_cpu_argb(&frame(1920, 1080, 2), 1920 * 4, 1, 25, false)
5346 .expect("a frame after the resize");
5347 s.video_bit_depth = 8;
5348 assert_eq!(
5349 enc.reconfigure_resolution(&s).is_err(),
5350 carried,
5351 "{codec:?}: a depth change rebuilds the session"
5352 );
5353 }
5354 }
5355 }
5356
5357 #[test]
5365 #[ignore]
5366 fn gpu_predicts_past_a_lost_frame() {
5367 use crate::encoders::reference::Reference;
5368 use crate::encoders::sps::h264_max_num_ref_frames;
5369 let (w, h) = (1280usize, 720usize);
5370 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5371 let mut s = settings(w as i32, h as i32, 60.0);
5372 s.codec = codec;
5373 s.omit_stripe_headers = true;
5374 let mut enc = match host_session(&s) {
5375 Ok(enc) => enc,
5376 Err(e) => {
5377 println!("{codec:?}: {e}");
5378 continue;
5379 }
5380 };
5381 let encode = |enc: &mut NvencEncoder, i: usize, w: usize, h: usize| {
5382 let out = enc
5383 .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
5384 .expect("encode");
5385 (out, enc.last_reference())
5386 };
5387 let (first, reference) = encode(&mut enc, 0, w, h);
5388 if reference == Reference::Untracked {
5389 println!(
5390 "{codec:?}: this device cannot invalidate a reference, so nothing is tracked"
5391 );
5392 assert!(!enc.invalidate_reference(0));
5393 continue;
5394 }
5395 assert_eq!(reference, Reference::None);
5396 if codec == Codec::H264 {
5397 assert_eq!(
5398 h264_max_num_ref_frames(&first),
5399 Some(REFERENCE_FRAMES),
5400 "the SPS declares the DPB"
5401 );
5402 }
5403 let mut frames = vec![first];
5404 for i in 1..8 {
5405 let (out, reference) = encode(&mut enc, i, w, h);
5406 assert_eq!(
5407 reference,
5408 Reference::Frame(i as u16 - 1),
5409 "{codec:?} frame {i}"
5410 );
5411 frames.push(out);
5412 }
5413 assert!(
5414 enc.invalidate_reference(5),
5415 "{codec:?}: the device refused the invalidation"
5416 );
5417 let (out, reference) = encode(&mut enc, 8, w, h);
5418 let before_loss = if codec == Codec::Av1
5421 && enc
5422 .references
5423 .as_ref()
5424 .is_some_and(ReferenceWindow::anchored)
5425 {
5426 0
5427 } else {
5428 4
5429 };
5430 assert_eq!(reference, Reference::Frame(before_loss), "{codec:?}");
5431 frames.push(out);
5432 let (out, reference) = encode(&mut enc, 9, w, h);
5433 assert_eq!(reference, Reference::Frame(8), "{codec:?}");
5434 frames.push(out);
5435 let off = apart_without(codec, &frames, 5..8);
5436 println!("{codec:?}: frame 9 without frames 5-7 is {off:.3} off the complete decode");
5437 assert!(
5438 off < 0.5,
5439 "{codec:?}: the decoder that lost frames 5-7 shows frame 9 {off:.2} off the one that saw them"
5440 );
5441 s.width = 1920;
5444 s.height = 1080;
5445 assert!(
5446 enc.reconfigure_resolution(&s).expect("in-place grow"),
5447 "{codec:?}"
5448 );
5449 let dpb = enc.dpb as usize;
5450 let anchored = enc.references.as_ref().unwrap().anchored();
5451 let (out, reference) = encode(&mut enc, 10, 1920, 1080);
5452 assert_eq!(reference, Reference::None);
5453 if codec == Codec::H264 {
5454 assert_eq!(
5455 h264_max_num_ref_frames(&out),
5456 Some(4),
5457 "1080p at level 4.2 admits four"
5458 );
5459 }
5460 let mut frames = vec![out];
5461 for i in 11..=10 + dpb {
5462 let (out, reference) = encode(&mut enc, i, 1920, 1080);
5463 assert_eq!(
5464 reference,
5465 Reference::Frame(i as u16 - 1),
5466 "{codec:?} frame {i}"
5467 );
5468 frames.push(out);
5469 }
5470 assert!(
5471 enc.invalidate_reference(12),
5472 "{codec:?}: the device refused the invalidation"
5473 );
5474 let (out, reference) = encode(&mut enc, 11 + dpb, 1920, 1080);
5475 assert_eq!(
5476 reference,
5477 Reference::Frame(if anchored { 10 } else { 11 }),
5478 "{codec:?}: use the newest reference still held before the loss"
5479 );
5480 frames.push(out);
5481 let (out, reference) = encode(&mut enc, 12 + dpb, 1920, 1080);
5482 assert_eq!(reference, Reference::Frame(11 + dpb as u16), "{codec:?}");
5483 frames.push(out);
5484 let off = apart_without(codec, &frames, 2..dpb + 1);
5485 println!(
5486 "{codec:?}: at 1080p, frame {} without frames 12-{} is {off:.3} off the complete decode",
5487 12 + dpb,
5488 10 + dpb
5489 );
5490 assert!(
5491 off < 0.5,
5492 "{codec:?}: the decoder that lost frames 12-{} shows {off:.2} off the one that saw them",
5493 10 + dpb
5494 );
5495 assert!(enc.invalidate_reference(11));
5496 assert_eq!(
5497 encode(&mut enc, 13 + dpb, 1920, 1080).1,
5498 if anchored {
5499 Reference::Frame(10)
5500 } else {
5501 Reference::None
5502 },
5503 "{codec:?}: losing frame 11 requires the older anchor or a key frame"
5504 );
5505 assert!(enc.invalidate_reference(10));
5506 assert_eq!(
5507 encode(&mut enc, 14 + dpb, 1920, 1080).1,
5508 if anchored {
5509 Reference::None
5510 } else {
5511 Reference::Frame(13 + dpb as u16)
5512 },
5513 "{codec:?}: losing the anchor requires a key frame; a report before a new key frame is ignored"
5514 );
5515 }
5516 }
5517
5518 #[test]
5524 #[ignore]
5525 fn gpu_a_covered_loss_keeps_the_recovered_chain() {
5526 use crate::encoders::reference::Reference;
5527 let (w, h) = (1920usize, 1080usize);
5528 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5529 let mut s = settings(w as i32, h as i32, 60.0);
5530 s.codec = codec;
5531 s.omit_stripe_headers = true;
5532 let mut enc = match host_session(&s) {
5533 Ok(enc) => enc,
5534 Err(e) => {
5535 println!("{codec:?}: {e}");
5536 continue;
5537 }
5538 };
5539 let mut frames = Vec::new();
5540 for i in 0..=107usize {
5541 let (lost, expect) = match i {
5542 82 => (Some(80), Some(79)),
5543 99 => (Some(81), Some(98)),
5544 107 => (Some(106), Some(105)),
5545 _ => (None, None),
5546 };
5547 if let Some(lost) = lost {
5548 assert!(enc.invalidate_reference(lost), "{codec:?}: lost {lost}");
5549 }
5550 frames.push(
5551 enc.encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
5552 .expect("encode"),
5553 );
5554 if i == 0 && enc.last_reference() == Reference::Untracked {
5555 break;
5556 }
5557 if let Some(from) = expect {
5558 assert_eq!(
5559 enc.last_reference(),
5560 Reference::Frame(from),
5561 "{codec:?} {i}"
5562 );
5563 }
5564 }
5565 if enc.last_reference() == Reference::Untracked {
5566 println!("{codec:?}: this device cannot invalidate a reference");
5567 continue;
5568 }
5569 let received: Vec<usize> = (0..frames.len())
5570 .filter(|i| !matches!(i, 80 | 81 | 106))
5571 .collect();
5572 let off = apart_keeping(codec, &frames, &received);
5573 assert!(
5574 off.is_some_and(|x| x < 0.5),
5575 "{codec:?}: without frames 80, 81 and 106 the last frame is {off:?} off"
5576 );
5577 println!("{codec:?}: frame 99 predicts from 98 after the covered report, {off:?} off");
5578 }
5579 }
5580
5581 #[test]
5588 #[ignore]
5589 fn gpu_a_held_refresh_past_its_limit_is_coded_again() {
5590 use crate::encoders::reference::Reference;
5591 let (w, h) = (1280usize, 720usize);
5592 let still = frame(w, h, 10);
5593 let encode = |enc: &mut NvencEncoder, i: u64, held: Option<u32>| {
5594 if let Some(crf) = held {
5595 enc.hold_quantizer(crf, None);
5596 }
5597 let out = enc
5598 .encode_cpu_argb(&still, w * 4, i, 25, i == 0)
5599 .expect("encode");
5600 (out, enc.last_reference())
5601 };
5602 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5603 let mut s = settings(w as i32, h as i32, 60.0);
5604 s.codec = codec;
5605 s.omit_stripe_headers = true;
5606 s.video_cbr_mode = true;
5607 s.video_bitrate_kbps = 50_000;
5608 let mut enc = match host_session(&s) {
5609 Ok(enc) => enc,
5610 Err(e) => {
5611 println!("{codec:?}: {e}");
5612 continue;
5613 }
5614 };
5615 let mut frames: Vec<_> = (0..4).map(|i| encode(&mut enc, i, None).0).collect();
5616 if enc.last_reference() == Reference::Untracked {
5617 println!("{codec:?}: this device cannot invalidate a reference");
5618 continue;
5619 }
5620 let (attempt, _) = encode(&mut enc, 4, Some(20));
5623 assert!(enc.retract_last(), "{codec:?}: the device refused");
5624 frames.push(attempt);
5625 let (out, reference) = encode(&mut enc, 4, Some(40));
5626 assert_eq!(reference, Reference::Frame(3), "{codec:?}");
5627 frames.push(out);
5628 let (out, reference) = encode(&mut enc, 5, None);
5629 assert_eq!(reference, Reference::Frame(4), "{codec:?}");
5630 frames.push(out);
5631 let off = apart_without(codec, &frames, 4..5);
5632 assert!(
5633 off < 0.5,
5634 "{codec:?}: without the attempt the frames after it decode {off:.2} off"
5635 );
5636 assert!(enc.invalidate_reference(4), "{codec:?}");
5637 let (out, reference) = encode(&mut enc, 6, None);
5638 assert!(
5639 matches!(reference, Reference::Frame(id) if id < 4),
5640 "{codec:?}: {reference:?} after losing the frame sent in place of the attempt"
5641 );
5642 frames.push(out);
5643 let lost = apart_without(codec, &frames, 4..7);
5644 assert!(
5645 lost < 0.5,
5646 "{codec:?}: past the loss the frame decodes {lost:.2} off"
5647 );
5648 for (kbps, first) in [(100, 7u64), (1000, 11)] {
5652 s.video_bitrate_kbps = kbps;
5653 assert!(enc.reconfigure_rate(&s), "{codec:?}");
5654 for i in first..first + 3 {
5655 encode(&mut enc, i, None);
5656 }
5657 let pts = |enc: &NvencEncoder| enc.references.as_ref().map_or(0, |r| r.next_pts());
5658 let before = pts(&enc);
5659 let (out, reference) = encode(&mut enc, first + 3, Some(20));
5660 assert_eq!(reference, Reference::Frame(first as u16 + 2), "{codec:?}");
5661 let spent = out.len() as f64 / (kbps as f64 * 1000.0 / 8.0);
5662 let coded = pts(&enc) - before;
5663 println!(
5664 "{codec:?}: a held frame at {kbps} kbps goes out at {} bytes, {spent:.2} s of the target, encoded {coded} times",
5665 out.len()
5666 );
5667 assert!(spent <= crate::encoders::HELD_REFRESH_LIMIT_S, "{codec:?}");
5668 if kbps == 1000 {
5669 assert_eq!(coded, 1, "{codec:?}: a frame under the limit stands");
5670 }
5671 }
5672 }
5673 }
5674
5675 #[test]
5685 #[ignore]
5686 fn gpu_acknowledged_anchors_carry_a_consumer_left_behind() {
5687 use crate::encoders::codec::parse_video_type;
5688 use crate::encoders::reference::Reference;
5689 use crate::webcam::decode::{Decoder as _, VideoDecoder};
5690 let (w, h) = (1280usize, 720usize);
5691 let pinned = [(13..=40, false, 42, 12u16), (61..=100, true, 102, 60)];
5694 let alone = [(49..=90, false, 92, 48u16), (97..=150, false, 152, 96)];
5695 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5696 let mut s = settings(w as i32, h as i32, 60.0);
5697 s.codec = codec;
5698 s.acknowledge_references = true;
5699 let mut enc = match host_session(&s) {
5700 Ok(enc) => enc,
5701 Err(e) => {
5702 println!("{codec:?}: {e}");
5703 continue;
5704 }
5705 };
5706 let anchors = enc
5707 .references
5708 .as_ref()
5709 .map_or(0, ReferenceWindow::anchor_count);
5710 let (stretches, period, last) = match anchors {
5711 ANCHORS => (&pinned, 12, 120),
5712 1 => (&alone, 48, 160),
5713 _ => {
5714 println!("{codec:?}: this device or API keeps no long-term reference");
5715 continue;
5716 }
5717 };
5718 let (mut whole, mut part) = (
5719 VideoDecoder::new(codec).unwrap(),
5720 VideoDecoder::new(codec).unwrap(),
5721 );
5722 let mut holds = Vec::new();
5723 let mut run = None;
5724 let (mut worst, mut sizes) = (0f64, (0usize, 0usize, 0usize, 0usize));
5725 for i in 0..=last {
5726 let stretch = stretches.iter().find(|t| t.0.contains(&i));
5727 if let Some(&(_, _, _, from)) = stretches.iter().find(|t| t.2 == i) {
5728 let first = run.take().expect("a missing run");
5729 assert!(enc.take_report(ReferenceReport::Lost(first)), "{codec:?}");
5730 assert_eq!(
5731 enc.references
5732 .as_ref()
5733 .and_then(ReferenceWindow::newest_valid)
5734 .map(|f| f.0),
5735 Some(from),
5736 "{codec:?}: {first} reported lost before {i}"
5737 );
5738 }
5739 let out = enc
5740 .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
5741 .expect("encode");
5742 let (_, kind) = parse_video_type(out[1]).expect("video type byte");
5743 assert_eq!(
5744 kind == FRAME_KEY,
5745 i == 0,
5746 "{codec:?}: frame {i} is kind {kind}"
5747 );
5748 let reference = enc.last_reference();
5749 let can = match reference {
5750 Reference::None => true,
5751 Reference::Frame(r) => holds[r as usize],
5752 Reference::Untracked => false,
5753 };
5754 let anchor = out[1] & FRAME_ANCHOR != 0;
5755 if anchor {
5756 assert!(
5757 can,
5758 "{codec:?}: anchor {i} predicts from {reference:?}, held by one"
5759 );
5760 }
5761 assert_eq!(
5762 anchor,
5763 i > 0 && i % period == 0 && can,
5764 "{codec:?}: frame {i} predicting from {reference:?}"
5765 );
5766 let sent = can && stretch.is_none_or(|t| anchor && !t.1);
5767 let bits = &out[VIDEO_HEADER_LEN..];
5768 assert!(whole.decode(bits).unwrap_or(false), "{codec:?}: frame {i}");
5769 if sent {
5770 assert!(
5771 part.decode(bits).unwrap_or(false),
5772 "{codec:?}: frame {i} sent"
5773 );
5774 let off = luma_apart(&whole.frame().unwrap(), &part.frame().unwrap());
5775 assert!(off < 0.5, "{codec:?}: frame {i} sent is {off:.3} off");
5776 worst = worst.max(off);
5777 run = None;
5778 enc.acknowledge_reference(i as u16, true);
5779 } else if can {
5780 run = Some(i as u16);
5781 }
5782 holds.push(sent);
5783 if i > 0 {
5784 let bucket = if anchor { &mut sizes.2 } else { &mut sizes.0 };
5785 *bucket += out.len();
5786 if anchor { sizes.3 += 1 } else { sizes.1 += 1 }
5787 }
5788 }
5789 println!(
5790 "{codec:?}: worst {worst:.3} apart; {} B a frame, {} B an anchor ({} anchors)",
5791 sizes.0 / sizes.1.max(1),
5792 sizes.2 / sizes.3.max(1),
5793 sizes.3
5794 );
5795 }
5796 }
5797
5798 #[test]
5804 #[ignore]
5805 fn gpu_a_lost_anchor_lends_its_slot_to_the_recent_frames() {
5806 let (w, h) = (1280usize, 720usize);
5807 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
5808 let mut s = settings(w as i32, h as i32, 60.0);
5809 s.codec = codec;
5810 s.acknowledge_references = true;
5811 s.omit_stripe_headers = true;
5812 let mut enc = match host_session(&s) {
5813 Ok(enc) => enc,
5814 Err(e) => {
5815 println!("{codec:?}: {e}");
5816 continue;
5817 }
5818 };
5819 if !enc
5820 .references
5821 .as_ref()
5822 .is_some_and(ReferenceWindow::anchored)
5823 {
5824 println!("{codec:?}: this device or API keeps no long-term reference");
5825 continue;
5826 }
5827 let mut frames = Vec::new();
5828 let mut from = 0;
5829 for i in 0..=22usize {
5830 match i {
5831 16 => assert!(enc.take_report(ReferenceReport::Lost(12)), "{codec:?}"),
5832 21 => assert!(enc.take_report(ReferenceReport::Lost(19)), "{codec:?}"),
5833 _ => {}
5834 }
5835 let out = enc
5836 .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
5837 .expect("encode");
5838 if i <= 11 || (16..=17).contains(&i) {
5839 enc.acknowledge_reference(i as u16, true);
5840 }
5841 frames.push(out);
5842 if i == 21 {
5843 let Reference::Frame(r) = enc.last_reference() else {
5844 panic!("{codec:?}: {:?} past the loss", enc.last_reference());
5845 };
5846 from = r as usize;
5847 }
5848 }
5849 if codec == Codec::Av1 {
5850 assert_eq!(from, 18, "{codec:?}: past the loss of 19 and 20");
5851 }
5852 let keep: Vec<usize> = (0..=11).chain(16..=from).chain([21, 22]).collect();
5853 let off = apart_keeping(codec, &frames, &keep);
5854 println!("{codec:?}: past the loss from {from}, {off:?} apart");
5855 assert!(
5856 off.is_some_and(|off| off < 0.5),
5857 "{codec:?}: given the frames up to {from}, frame 22 decodes {off:?} apart"
5858 );
5859 }
5860 }
5861
5862 #[test]
5869 #[ignore]
5870 fn gpu_predicts_past_a_loss_deeper_than_the_recent_frames() {
5871 use crate::encoders::reference::Reference;
5872 let cases = [
5876 (Codec::H264, 60u16, 50u16, Some(48u16), Some(0u16), false),
5877 (Codec::H264, 60, 57, Some(48), Some(0), false),
5878 (Codec::H264, 80, 64, Some(48), Some(0), false),
5879 (Codec::H264, 80, 51, Some(48), Some(0), false),
5880 (Codec::H264, 80, 64, Some(48), Some(0), true),
5881 (Codec::H264, 60, 40, None, None, false),
5882 (Codec::Av1, 60, 50, Some(48), Some(0), false),
5883 (Codec::Av1, 60, 57, Some(48), Some(0), false),
5884 (Codec::Av1, 80, 64, Some(48), Some(0), false),
5885 (Codec::Av1, 80, 64, Some(48), Some(0), true),
5886 (Codec::Av1, 60, 40, None, None, false),
5887 (Codec::H265, 30, 28, Some(27), Some(26), false),
5888 (Codec::H265, 30, 27, Some(24), Some(12), false),
5889 (Codec::H265, 40, 37, Some(36), Some(24), false),
5890 (Codec::H265, 40, 30, Some(24), Some(12), false),
5891 (Codec::H265, 58, 40, Some(36), Some(24), false),
5892 (Codec::H265, 58, 37, Some(36), Some(24), false),
5893 (Codec::H265, 40, 20, None, None, false),
5894 ];
5895 let split = [
5896 (Codec::Av1, 60u16, 50u16, Some(48u16), Some(0u16), false),
5897 (Codec::H265, 30, 28, Some(27), Some(26), false),
5898 (Codec::H265, 40, 30, Some(24), Some(12), false),
5899 ];
5900 let fmt = |v: Option<f64>| v.map_or("no picture".to_string(), |x| format!("{x:.3}"));
5901 let sized = (cases.into_iter().map(|c| (1920usize, 1080usize, c)))
5902 .chain(split.into_iter().map(|c| (3840, 2160, c)));
5903 for (w, h, (codec, sent, lost, from, older, retarget)) in sized {
5904 let mut s = settings(w as i32, h as i32, 60.0);
5905 s.codec = codec;
5906 s.omit_stripe_headers = true;
5907 if retarget {
5908 s.video_cbr_mode = true;
5909 s.video_bitrate_kbps = 20_000;
5910 }
5911 let mut enc = match host_session(&s) {
5912 Ok(enc) => enc,
5913 Err(e) => {
5914 println!("{codec:?}: {e}");
5915 continue;
5916 }
5917 };
5918 if codec == Codec::Av1
5919 && !enc
5920 .references
5921 .as_ref()
5922 .is_some_and(ReferenceWindow::anchored)
5923 {
5924 println!("{codec:?}: this device or API keeps no long-term reference");
5925 continue;
5926 }
5927 let mut frames = Vec::new();
5928 for i in 0..=sent as usize {
5929 if retarget && i == lost as usize {
5930 s.video_bitrate_kbps = 8000;
5931 assert!(
5932 enc.reconfigure_rate(&s),
5933 "{codec:?}: the rate change was refused"
5934 );
5935 }
5936 frames.push(
5937 enc.encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
5938 .expect("encode"),
5939 );
5940 if i == 0 && enc.last_reference() == Reference::Untracked {
5941 break;
5942 }
5943 }
5944 if enc.last_reference() == Reference::Untracked {
5945 println!("{codec:?}: this device cannot invalidate a reference");
5946 continue;
5947 }
5948 let normal = frames[sent as usize].len();
5949 assert!(
5950 enc.invalidate_reference(lost),
5951 "{codec:?}: the device refused the invalidation"
5952 );
5953 let next = sent as usize + 1;
5954 frames.push(
5955 enc.encode_cpu_argb(&moving_frame(w, h, next), w * 4, next as u64, 25, false)
5956 .expect("encode"),
5957 );
5958 let reference = enc.last_reference();
5959 let keeping = |upto: usize| (0..=upto).chain([next]).collect::<Vec<_>>();
5960 let without_lost: Vec<usize> = (0..=next)
5961 .filter(|&i| i < lost as usize || i == next)
5962 .collect();
5963 println!(
5964 "{codec:?} {w}x{h}: frame {lost} of {sent} lost, frame {next} predicts from {reference:?}, {} B against {normal} B",
5965 frames[next].len()
5966 );
5967 if let Some(anchor) = from.filter(|&a| a > 0 && a < sent).map(usize::from) {
5969 let skipped: Vec<usize> = (0..=anchor + 1).filter(|&i| i != anchor).collect();
5970 let off = apart_keeping(codec, &frames[..=anchor + 1], &skipped);
5971 assert!(
5972 off.is_none_or(|x| x >= 0.5),
5973 "{codec:?}: frame {} decodes without {anchor} ({})",
5974 anchor + 1,
5975 fmt(off)
5976 );
5977 }
5978 match from {
5979 Some(anchor) => {
5980 assert_eq!(
5981 reference,
5982 Reference::Frame(anchor),
5983 "{codec:?}: lost {lost}"
5984 );
5985 let off = apart_keeping(codec, &frames, &without_lost);
5986 assert!(
5987 off.is_some_and(|x| x < 0.5),
5988 "{codec:?}: without {lost}-{sent} the frame is {} off",
5989 fmt(off)
5990 );
5991 let off = apart_keeping(codec, &frames, &keeping(anchor as usize));
5992 assert!(
5993 off.is_some_and(|x| x < 0.5),
5994 "{codec:?}: up to the anchor {anchor} the frame is {} off",
5995 fmt(off)
5996 );
5997 if let Some(older) = older {
5998 let off = apart_keeping(codec, &frames, &keeping(older as usize));
5999 let past_buffer = codec == Codec::H265
6000 && anchor == sent - (enc.dpb - ANCHORS as u32) as u16;
6001 if past_buffer && off.is_some_and(|x| x < 0.5) {
6002 println!(
6003 "{codec:?}: lost {lost}, the device predicts from before {anchor}, the frame past its buffer"
6004 );
6005 } else {
6006 assert!(
6007 off.is_none_or(|x| x >= 0.5),
6008 "{codec:?}: the frame decodes from anchor {older} alone ({})",
6009 fmt(off)
6010 );
6011 }
6012 }
6013 }
6014 None => assert_eq!(
6015 reference,
6016 Reference::None,
6017 "{codec:?}: lost {lost}, before every anchor"
6018 ),
6019 }
6020 }
6021 }
6022
6023 #[test]
6028 #[ignore]
6029 fn gpu_a_shrink_predicts_past_a_loss_deeper_than_its_buffer() {
6030 use crate::encoders::reference::Reference;
6031 use crate::encoders::sps::h264_max_num_ref_frames;
6032 for codec in [Codec::H264, Codec::H265] {
6033 let mut s = settings(1920, 1080, 60.0);
6034 s.codec = codec;
6035 s.omit_stripe_headers = true;
6036 let mut enc = match host_session(&s) {
6037 Ok(enc) => enc,
6038 Err(e) if codec == Codec::H265 => {
6039 println!("{codec:?}: {e}");
6040 continue;
6041 }
6042 Err(e) => panic!("{codec:?}: {e}"),
6043 };
6044 let encode = |enc: &mut NvencEncoder, i: usize, w: usize, h: usize| {
6045 let out = enc
6046 .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6047 .expect("encode");
6048 (out, enc.last_reference())
6049 };
6050 let (first, reference) = encode(&mut enc, 0, 1920, 1080);
6051 if reference == Reference::Untracked {
6052 println!("{codec:?}: this device cannot invalidate a reference");
6053 continue;
6054 }
6055 let declared = enc.dpb;
6056 if codec == Codec::H264 {
6057 assert_eq!(
6058 h264_max_num_ref_frames(&first),
6059 Some(4),
6060 "1080p at level 4.2 admits four"
6061 );
6062 }
6063 (s.width, s.height) = (1280, 720);
6064 assert!(
6065 enc.reconfigure_resolution(&s).expect("in-place shrink"),
6066 "{codec:?}"
6067 );
6068 assert_eq!(
6069 enc.dpb, declared,
6070 "{codec:?}: the driver never raises the buffer"
6071 );
6072 let mut frames = Vec::new();
6073 for i in 1..=8 {
6074 let (out, reference) = encode(&mut enc, i, 1280, 720);
6075 let want = if i == 1 {
6076 Reference::None
6077 } else {
6078 Reference::Frame(i as u16 - 1)
6079 };
6080 assert_eq!(reference, want, "{codec:?} frame {i}");
6081 frames.push(out);
6082 }
6083 assert!(
6084 enc.invalidate_reference(2),
6085 "{codec:?}: the device refused the invalidation"
6086 );
6087 let (out, reference) = encode(&mut enc, 9, 1280, 720);
6088 assert_eq!(
6089 reference,
6090 Reference::Frame(1),
6091 "{codec:?}: a loss six deep, from the resize's key frame"
6092 );
6093 frames.push(out);
6094 let off = apart_without(codec, &frames, 1..8);
6095 println!(
6096 "{codec:?}: at 720p on a {declared}-frame buffer, frame 9 without frames 2-8 is {off:.3} off the complete decode"
6097 );
6098 assert!(
6099 off < 0.5,
6100 "{codec:?}: the decoder that lost frames 2-8 shows {off:.2} off the one that saw them"
6101 );
6102 }
6103 }
6104
6105 #[test]
6112 #[ignore]
6113 fn gpu_bench_loss_recovery() {
6114 use crate::encoders::reference::Reference;
6115 use crate::webcam::decode::{Decoder as _, VideoDecoder};
6116 let (w, h) = (1920usize, 1080usize);
6117 for codec in [Codec::H264, Codec::H265] {
6118 let mut s = settings(w as i32, h as i32, 60.0);
6119 s.codec = codec;
6120 s.video_cbr_mode = true;
6121 s.video_bitrate_kbps = 8000;
6122 for depth in [0usize, 1, 2, 3, 5, 8, 12] {
6123 let mut enc = host_session(&s).expect("init");
6124 let mut dec = VideoDecoder::new(codec).unwrap();
6125 let mut pending: std::collections::VecDeque<Vec<u8>> = Default::default();
6126 let mut rng = 0x2545_f491_4f6c_dd1du64 ^ depth as u64;
6127 let (mut sizes, mut first, mut after, mut keys) =
6128 (Vec::new(), Vec::new(), Vec::new(), 0);
6129 let mut i = 0usize;
6130 for event in 0..10 {
6131 rng ^= rng << 13;
6132 rng ^= rng >> 7;
6133 rng ^= rng << 17;
6134 let gap = if event == 0 {
6135 60
6136 } else {
6137 20 + (rng % 37) as usize
6138 };
6139 for _ in 0..gap {
6140 let out = enc
6141 .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6142 .expect("encode");
6143 pending.push_back(out);
6144 i += 1;
6145 while pending.len() > depth + 1 {
6146 let out = pending.pop_front().unwrap();
6147 dec.decode(&out[VIDEO_HEADER_LEN..]).expect("decode");
6148 }
6149 }
6150 if depth == 0 {
6152 for out in pending.drain(..) {
6153 dec.decode(&out[VIDEO_HEADER_LEN..]).expect("decode");
6154 }
6155 } else {
6156 pending.clear();
6157 enc.invalidate_reference((i - 1 - depth) as u16);
6158 }
6159 let mut row = Vec::new();
6160 for k in 0..15 {
6161 let src = moving_frame(w, h, i);
6162 let out = enc
6163 .encode_cpu_argb(&src, w * 4, i as u64, 25, false)
6164 .expect("encode");
6165 if k == 0 {
6166 sizes.push(out.len() - VIDEO_HEADER_LEN);
6167 keys += usize::from(enc.last_reference() == Reference::None);
6168 }
6169 row.push(luma_psnr(&mut dec, &out, &src, w, h));
6170 i += 1;
6171 }
6172 first.push(row[0]);
6173 after.push(row.iter().sum::<f64>() / row.len() as f64);
6174 }
6175 sizes.sort();
6176 let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len() as f64;
6177 println!(
6178 "RECOVERY {codec:?} depth {depth}: {keys}/10 key frames, answer {} B median, \
6179 answer PSNR {:.2} dB mean ({:.2} worst), with the 14 after {:.2} dB mean",
6180 sizes[sizes.len() / 2],
6181 mean(&first),
6182 first.iter().cloned().fold(f64::INFINITY, f64::min),
6183 mean(&after),
6184 );
6185 }
6186 }
6187 }
6188
6189 #[test]
6194 #[ignore]
6195 fn gpu_predicts_past_a_loss_at_the_devices_frame_num_wrap() {
6196 use crate::encoders::reference::Reference;
6197 use crate::encoders::sps::h264_frame_num_range;
6198 let (w, h) = (1280usize, 720usize);
6199 let mut s = settings(w as i32, h as i32, 60.0);
6200 s.omit_stripe_headers = true;
6201 let mut enc = host_session(&s).expect("H.264 session");
6202 let encode = |enc: &mut NvencEncoder, i: usize| {
6203 let out = enc
6204 .encode_cpu_argb(&moving_frame(w, h, i), w * 4, i as u64, 25, i == 0)
6205 .expect("encode");
6206 (out, enc.last_reference())
6207 };
6208 let (first, reference) = encode(&mut enc, 0);
6209 if reference == Reference::Untracked {
6210 println!("this device cannot invalidate a reference, so nothing is tracked");
6211 return;
6212 }
6213 let range = h264_frame_num_range(&first).expect("the key frame carries the SPS") as usize;
6214 assert_eq!(range, 65536, "NVENC's 256 values, a byte wider");
6215 let mut frames = vec![first];
6216 for i in 1..=258 {
6217 frames.push(encode(&mut enc, i).0);
6218 }
6219 assert!(
6220 enc.invalidate_reference(256),
6221 "the device's wrap frame is reported lost"
6222 );
6223 let (out, reference) = encode(&mut enc, 259);
6224 assert!(
6225 matches!(reference, Reference::Frame(r) if r < 256),
6226 "{reference:?} past the loss"
6227 );
6228 frames.push(out);
6229 let off = apart_without(Codec::H264, &frames, 256..259);
6230 assert!(
6231 off < 0.5,
6232 "past the loss at the wrap the frame decodes {off:.2} off"
6233 );
6234 }
6235
6236 #[test]
6240 #[ignore]
6241 fn gpu_resolution_reconfigure_cbr() {
6242 let mut s = settings(1280, 720, 60.0);
6243 s.video_cbr_mode = true;
6244 s.video_bitrate_kbps = 4000;
6245 let mut enc = host_session(&s).expect("NVENC init");
6246 let f720 = frame(1280, 720, 10);
6247 for i in 0..3u64 {
6248 enc.encode_cpu_argb(&f720, 1280 * 4, i, 25, i == 0)
6249 .expect("encode 720p");
6250 }
6251 s.width = 1920;
6252 s.height = 1080;
6253 s.video_bitrate_kbps = 8000;
6254 enc.reconfigure_resolution(&s).expect("cbr resize+rate");
6255 assert_eq!(enc.encode_config.rcParams.averageBitRate, 8_000_000);
6256 let f1080 = frame(1920, 1080, 40);
6257 let pkt = enc
6258 .encode_cpu_argb(&f1080, 1920 * 4, 3, 25, false)
6259 .expect("encode 1080p");
6260 assert_eq!(pkt[1] & 0x0f, FRAME_KEY);
6261 assert_eq!(wire_dims(&pkt), (1920, 1080));
6262 }
6263
6264 #[test]
6268 #[ignore]
6269 fn gpu_sessions_keep_the_reference_frames_they_are_given() {
6270 use crate::encoders::sps::h264_max_num_ref_frames;
6271 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
6272 let mut s = settings(1280, 720, 60.0);
6273 s.codec = codec;
6274 s.omit_stripe_headers = true;
6275 s.acknowledge_references = true;
6276 s.video_reference_frames = 1;
6277 let mut enc = match host_session(&s) {
6278 Ok(enc) => enc,
6279 Err(e) => {
6280 println!("{codec:?}: {e}");
6281 continue;
6282 }
6283 };
6284 let key = enc
6285 .encode_cpu_argb(&frame(1280, 720, 10), 1280 * 4, 0, 25, true)
6286 .expect("encode");
6287 let anchors = enc
6288 .references
6289 .as_ref()
6290 .map_or(0, ReferenceWindow::anchor_count);
6291 match codec {
6292 Codec::Av1 => assert_eq!(enc.dpb, AV1_REFERENCES, "AV1"),
6293 _ => {
6294 assert_eq!(enc.dpb, 1, "{codec:?}");
6295 assert_eq!(anchors, 0, "{codec:?}");
6296 }
6297 }
6298 if codec == Codec::H264 {
6299 assert_eq!(h264_max_num_ref_frames(&key), Some(1));
6300 }
6301 }
6302 }
6303
6304 #[test]
6307 #[ignore]
6308 fn gpu_h264_bounds_reordering_at_zero() {
6309 use crate::encoders::sps::fixtures::assert_no_reorder;
6310 for fullcolor in [false, true] {
6311 let mut s = settings(1920, 1080, 60.0);
6312 s.video_fullcolor = fullcolor;
6313 s.omit_stripe_headers = true;
6314 let mut enc = match host_session(&s) {
6315 Ok(enc) => enc,
6316 Err(e) => {
6317 println!("fullcolor {fullcolor}: {e}");
6318 continue;
6319 }
6320 };
6321 let key = enc
6322 .encode_cpu_argb(&frame(1920, 1080, 10), 1920 * 4, 0, 25, true)
6323 .expect("encode 1080p");
6324 assert_no_reorder(&key, &format!("NVENC fullcolor {fullcolor} 1080p"));
6325 s.width = 1280;
6326 s.height = 720;
6327 if enc.reconfigure_resolution(&s).expect("in-place resize") {
6328 let key = enc
6329 .encode_cpu_argb(&frame(1280, 720, 20), 1280 * 4, 1, 25, false)
6330 .expect("encode 720p");
6331 assert_no_reorder(
6332 &key,
6333 &format!("NVENC fullcolor {fullcolor} 720p after the resize"),
6334 );
6335 }
6336 }
6337 }
6338
6339 #[test]
6344 #[ignore]
6345 fn gpu_a_refused_resize_is_rebuilt() {
6346 use crate::encoders::sps::h264_max_num_ref_frames;
6347 use crate::encoders::{FrameEncoder, FrameSource, select_frame_encoder};
6348 let mut s = settings(1920, 1080, 60.0);
6349 let mut enc = host_session(&s).expect("NVENC init");
6350 let key = enc
6351 .encode_cpu_argb(&frame(1920, 1080, 10), 1920 * 4, 0, 25, true)
6352 .expect("encode 1080p");
6353 assert_eq!(
6354 h264_max_num_ref_frames(&key[VIDEO_HEADER_LEN..]),
6355 Some(4),
6356 "1080p at level 4.2 admits four"
6357 );
6358 enc.dpb = REFERENCE_FRAMES;
6359 s.width = 1280;
6360 s.height = 720;
6361 let prior = Some(FrameEncoder::Nvenc(enc));
6362 let Some(FrameEncoder::Nvenc(mut enc)) =
6363 select_frame_encoder(&mut s, FrameSource::Host { rgba: false }, prior, "test")
6364 else {
6365 panic!("the ladder rebuilt no NVENC session");
6366 };
6367 assert_eq!(
6368 enc.references.as_ref().map_or(0, ReferenceWindow::next_pts),
6369 0,
6370 "a new session, not the refused one"
6371 );
6372 let key = enc
6373 .encode_cpu_argb(&frame(1280, 720, 20), 1280 * 4, 1, 25, false)
6374 .expect("encode 720p");
6375 assert_eq!(key[1] & 0x0f, FRAME_KEY);
6376 assert_eq!(wire_dims(&key), (1280, 720));
6377 assert_eq!(
6378 h264_max_num_ref_frames(&key[VIDEO_HEADER_LEN..]),
6379 Some(REFERENCE_FRAMES),
6380 "720p declares eight"
6381 );
6382 }
6383
6384 fn guarded<T>(value: T) -> &'static mut T {
6387 unsafe {
6388 let page = libc::sysconf(libc::_SC_PAGESIZE) as usize;
6389 let map = libc::mmap(
6390 ptr::null_mut(),
6391 2 * page,
6392 libc::PROT_READ | libc::PROT_WRITE,
6393 libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
6394 -1,
6395 0,
6396 );
6397 assert_ne!(map, libc::MAP_FAILED);
6398 assert_eq!(
6399 libc::mprotect(map.cast::<u8>().add(page).cast(), page, libc::PROT_NONE),
6400 0
6401 );
6402 let at = map
6403 .cast::<u8>()
6404 .add(page - std::mem::size_of::<T>())
6405 .cast::<T>();
6406 at.write(value);
6407 &mut *at
6408 }
6409 }
6410
6411 #[test]
6416 #[ignore]
6417 fn gpu_the_driver_stays_inside_the_structs_it_is_handed() {
6418 const PINNED: &str = "PIXELFLUX_TEST_NVENC_PINNED";
6419 if std::env::var_os(PINNED).is_none() {
6420 let name = format!(
6421 "{}::gpu_the_driver_stays_inside_the_structs_it_is_handed",
6422 module_path!().split_once("::").unwrap().1
6423 );
6424 for api in ["10.0", "11.0", "11.1", "12.0", "12.1", "13.0"] {
6425 let out = std::process::Command::new(std::env::current_exe().unwrap())
6426 .args([
6427 name.as_str(),
6428 "--exact",
6429 "--ignored",
6430 "--nocapture",
6431 "--test-threads=1",
6432 ])
6433 .env("PIXELFLUX_NVENC_MAX_API", api)
6434 .env(PINNED, "1")
6435 .output()
6436 .expect("the test binary runs");
6437 let stdout = String::from_utf8_lossy(&out.stdout);
6438 let report = stdout
6439 .lines()
6440 .find(|l| l.contains("stayed inside"))
6441 .unwrap_or_default();
6442 println!("{report}");
6443 assert!(
6444 out.status.success() && !report.is_empty(),
6445 "API {api}: {}\n{stdout}",
6446 out.status
6447 );
6448 }
6449 return;
6450 }
6451 let mut enc = host_session(&settings(1280, 720, 60.0)).expect("NVENC init");
6452 unsafe {
6453 let funcs = enc.nvenc_funcs;
6454 let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
6455 let mut open = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
6456 version: sv(NvStruct::OpenSessionExParams),
6457 deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
6458 device: enc.cuda_context as *mut c_void,
6459 apiVersion: neg_api(),
6460 ..Default::default()
6461 };
6462 let mut session = ptr::null_mut();
6463 assert_eq!(
6464 (funcs.nvEncOpenEncodeSessionEx.unwrap())(&mut open, &mut session),
6465 NVENCSTATUS::NV_ENC_SUCCESS
6466 );
6467 enc.init_params.encodeConfig = &mut enc.encode_config;
6468 let init = guarded(Negotiated::new(enc.init_params));
6469 let status = (funcs.nvEncInitializeEncoder.unwrap())(session, &mut init.value);
6470 (funcs.nvEncDestroyEncoder.unwrap())(session);
6471 assert_eq!(status, NVENCSTATUS::NV_ENC_SUCCESS, "initialize");
6472 let reconfigure = guarded(reconfigure_params(
6473 enc.init_params,
6474 nvenc_cur_ver(),
6475 true,
6476 true,
6477 ));
6478 let status = (funcs.nvEncReconfigureEncoder.unwrap())(
6479 enc.encoder_session,
6480 &mut reconfigure.value,
6481 );
6482 assert_eq!(status, NVENCSTATUS::NV_ENC_SUCCESS, "reconfigure");
6483 let output = enc.bitstream_buffers[0];
6484 let mut pic = NV_ENC_PIC_PARAMS {
6485 version: sv(NvStruct::PicParams),
6486 inputWidth: enc.width,
6487 inputHeight: enc.height,
6488 inputBuffer: enc.mapped_input_buffer,
6489 outputBitstream: output,
6490 bufferFmt: enc.input_format,
6491 pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
6492 ..Default::default()
6493 };
6494 assert_eq!(
6495 (funcs.nvEncEncodePicture.unwrap())(enc.encoder_session, &mut pic),
6496 NVENCSTATUS::NV_ENC_SUCCESS
6497 );
6498 let lock = guarded(Negotiated::new(NV_ENC_LOCK_BITSTREAM {
6499 version: sv(NvStruct::LockBitstream),
6500 outputBitstream: output,
6501 ..Default::default()
6502 }));
6503 let status = (funcs.nvEncLockBitstream.unwrap())(enc.encoder_session, &mut lock.value);
6504 assert_eq!(status, NVENCSTATUS::NV_ENC_SUCCESS, "lock");
6505 assert!(
6506 lock.value.bitstreamSizeInBytes > 0,
6507 "the locked bitstream carries the picture"
6508 );
6509 (funcs.nvEncUnlockBitstream.unwrap())(enc.encoder_session, output);
6510 (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
6511 }
6512 let (maj, min) = nvenc_cur_ver();
6513 println!("API {maj}.{min}: the driver stayed inside every struct");
6514 }
6515
6516 #[test]
6521 #[ignore]
6522 fn gpu_pinned_uploads_read_the_source_as_it_is_now() {
6523 use crate::webcam::decode::VideoDecoder;
6524 let (w, h) = (1280usize, 720usize);
6525 let mut enc = NvencEncoder::new(&settings(w as i32, h as i32, 60.0), ptr::null())
6526 .expect("NVENC init");
6527 assert!(enc.pin_uploads, "the production default pins");
6528 let mut dec = VideoDecoder::new(Codec::H264).expect("decoder");
6529 for (source, frames) in [(0, 0..4usize), (1, 4..8usize)] {
6530 if source == 1 {
6531 enc.release_pinned_hosts();
6532 }
6533 let mut shm = vec![0u8; w * h * 4];
6534 for i in frames {
6535 shm.copy_from_slice(&moving_frame(w, h, i));
6536 let pkt = enc
6537 .encode_cpu_packed(&shm, w * 4, false, i as u64, 25, i == 0)
6538 .expect("encode");
6539 let psnr = luma_psnr(&mut dec, &pkt, &moving_frame(w, h, i), w, h);
6540 assert!(
6541 psnr > 30.0,
6542 "source {source}, frame {i}: {psnr:.1} dB against the picture just painted"
6543 );
6544 }
6545 }
6546 }
6547
6548 #[test]
6558 #[ignore]
6559 fn gpu_a_session_reopened_under_a_full_gpu_keeps_its_context() {
6560 type Ctx = *mut c_void;
6561 let first = host_session(&settings(256, 128, 60.0)).expect("NVENC init");
6562 let device = first.cuda_device;
6563 drop(first);
6564 unsafe {
6565 let lib = Library::new("libcuda.so.1").expect("libcuda");
6566 let state: Symbol<unsafe extern "C" fn(CUdevice, *mut u32, *mut i32) -> CUresult> =
6567 lib.get(b"cuDevicePrimaryCtxGetState\0").unwrap();
6568 let create: Symbol<unsafe extern "C" fn(*mut Ctx, u32, CUdevice) -> CUresult> =
6569 lib.get(b"cuCtxCreate_v2\0").unwrap();
6570 let destroy: Symbol<unsafe extern "C" fn(Ctx) -> CUresult> =
6571 lib.get(b"cuCtxDestroy_v2\0").unwrap();
6572 let info: Symbol<unsafe extern "C" fn(*mut usize, *mut usize) -> CUresult> =
6573 lib.get(b"cuMemGetInfo_v2\0").unwrap();
6574 let alloc: Symbol<unsafe extern "C" fn(*mut u64, usize) -> CUresult> =
6575 lib.get(b"cuMemAlloc_v2\0").unwrap();
6576 let (mut flags, mut active) = (0u32, 0i32);
6577 assert_eq!(
6578 state(device, &mut flags, &mut active),
6579 CUresult::CUDA_SUCCESS
6580 );
6581
6582 let mut game: Ctx = ptr::null_mut();
6583 assert_eq!(create(&mut game, 0, device), CUresult::CUDA_SUCCESS);
6584 let (mut free, mut total) = (0usize, 0usize);
6585 let mut chunk = 256usize << 20;
6586 let mut taken = 0usize;
6587 while chunk >= 1 << 20 {
6588 let mut ptr = 0u64;
6589 info(&mut free, &mut total);
6590 if free < (48 << 20) + chunk || alloc(&mut ptr, chunk) != CUresult::CUDA_SUCCESS {
6591 chunk /= 2;
6592 } else {
6593 taken += chunk;
6594 }
6595 }
6596 info(&mut free, &mut total);
6597 println!(
6598 "took {} MiB; {} MiB of {} left",
6599 taken >> 20,
6600 free >> 20,
6601 total >> 20
6602 );
6603 let reopened = host_session(&settings(256, 128, 60.0)).map(|mut enc| {
6604 enc.encode_cpu_argb(&vec![0x40u8; 256 * 128 * 4], 256 * 4, 0, 25, true)
6605 .map(|out| out.len())
6606 });
6607 destroy(game);
6608 println!(
6609 "primary context active after the first session: {active}; reopened: {reopened:?}"
6610 );
6611 assert_eq!(
6612 active, 1,
6613 "the primary context outlived the session that opened it"
6614 );
6615 let bytes = reopened.expect("the session reopens").expect("and encodes");
6616 assert!(bytes > 0);
6617 }
6618 }
6619
6620 #[test]
6621 #[ignore]
6622 fn gpu_a_device_out_of_sessions_says_so() {
6623 let enc = host_session(&settings(256, 128, 60.0)).expect("NVENC init");
6624 let mut sessions = Vec::new();
6625 let refused = unsafe {
6626 loop {
6627 let mut open = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
6628 version: sv(NvStruct::OpenSessionExParams),
6629 deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
6630 device: enc.cuda_context as *mut c_void,
6631 apiVersion: neg_api(),
6632 ..Default::default()
6633 };
6634 let mut session = ptr::null_mut();
6635 match (enc.nvenc_funcs.nvEncOpenEncodeSessionEx.unwrap())(&mut open, &mut session) {
6636 NVENCSTATUS::NV_ENC_SUCCESS => sessions.push(session),
6637 status => break Some(status),
6638 }
6639 if sessions.len() == 64 {
6640 break None;
6641 }
6642 }
6643 };
6644 let taken = probe_codecs(0);
6645 let opened = sessions.len();
6646 for session in sessions.drain(..) {
6647 unsafe { (enc.nvenc_funcs.nvEncDestroyEncoder.unwrap())(session) };
6648 }
6649 let Some(status) = refused else {
6650 println!("this device took 65 sessions at once: no cap to reach");
6651 return;
6652 };
6653 assert_eq!(
6654 session_refusal(status),
6655 SESSIONS_TAKEN,
6656 "session {} refused: {status:?}",
6657 opened + 2
6658 );
6659 assert_eq!(
6660 taken,
6661 Err(SESSIONS_TAKEN.to_string()),
6662 "every session taken"
6663 );
6664 assert!(
6665 probe_codecs(0).is_ok_and(|codecs| !codecs.is_empty()),
6666 "a freed session is listed again"
6667 );
6668 }
6669
6670 #[test]
6675 #[ignore]
6676 fn gpu_frame_rate_drop_keeps_the_level_the_buffer_needs() {
6677 for codec in [Codec::H264, Codec::H265] {
6678 let mut s = settings(1920, 1080, 120.0);
6679 s.codec = codec;
6680 s.video_cbr_mode = true;
6681 s.video_bitrate_kbps = 8000;
6682 let mut enc = host_session(&s).expect("NVENC init");
6683 let level = enc.declared_level();
6684 let frames: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(1920, 1080, i)).collect();
6685 let kbit = |enc: &mut NvencEncoder, range: std::ops::Range<u64>| {
6686 let n = (range.end - range.start) as f64;
6687 let bytes: usize = range
6688 .map(|i| {
6689 enc.encode_cpu_argb(&frames[i as usize % 16], 1920 * 4, i, 25, i == 0)
6690 .expect("encode")
6691 .len()
6692 - VIDEO_HEADER_LEN
6693 })
6694 .sum();
6695 bytes as f64 * 8.0 / n / 1000.0
6696 };
6697 kbit(&mut enc, 0..20);
6698 let at120 = kbit(&mut enc, 20..80);
6699 s.target_fps = 60.0;
6700 assert!(
6701 enc.reconfigure_rate(&s),
6702 "{codec:?}: the driver refused 60 fps"
6703 );
6704 assert_eq!(
6705 enc.declared_level(),
6706 level,
6707 "{codec:?}: the level the buffer needs stays"
6708 );
6709 let at60 = kbit(&mut enc, 80..200);
6710 println!("{codec:?}: {at120:.1} kbit a frame at 120 fps, {at60:.1} at 60");
6711 assert!(
6712 at60 > 1.4 * at120,
6713 "{codec:?}: halving the frame rate left {at60:.1} kbit a frame against {at120:.1}"
6714 );
6715 }
6716 }
6717
6718 #[test]
6722 #[ignore]
6723 fn gpu_frame_rate_reaches_the_driver_as_its_fraction() {
6724 let f = frame(640, 360, 30);
6725 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
6726 for (num, den) in [(60000u32, 1001u32), (120000, 1001), (144000, 1001), (60, 1)] {
6727 let fps = num as f64 / den as f64;
6728 let mut s = settings(640, 360, fps);
6729 s.codec = codec;
6730 s.video_cbr_mode = true;
6731 s.video_bitrate_kbps = 8000;
6732 let mut enc = match host_session(&s) {
6733 Ok(enc) => enc,
6734 Err(e) if codec == Codec::Av1 && e.contains("engine") => {
6735 println!("{codec:?}: {e}");
6736 break;
6737 }
6738 Err(e) => panic!("{codec:?} at {num}/{den}: {e}"),
6739 };
6740 assert_eq!(
6741 (enc.init_params.frameRateNum, enc.init_params.frameRateDen),
6742 (num, den),
6743 "{codec:?}"
6744 );
6745 let pkt = enc
6746 .encode_cpu_argb(&f, 640 * 4, 0, 25, true)
6747 .expect("encode");
6748 if codec == Codec::H264 {
6749 let (tick, scale) = crate::encoders::sps::h264_timing(&pkt[VIDEO_HEADER_LEN..])
6750 .expect("SPS timing");
6751 assert_eq!(
6752 scale as u64 * den as u64,
6753 2 * num as u64 * tick as u64,
6754 "the SPS declares {scale}/2x{tick}, not {num}/{den}"
6755 );
6756 }
6757 s.target_fps = 30.0;
6758 assert!(
6759 enc.reconfigure_rate(&s),
6760 "{codec:?}: the driver refused 30 fps"
6761 );
6762 assert_eq!(
6763 (enc.init_params.frameRateNum, enc.init_params.frameRateDen),
6764 (30, 1)
6765 );
6766 s.target_fps = fps;
6767 assert!(
6768 enc.reconfigure_rate(&s),
6769 "{codec:?}: the driver refused {num}/{den}"
6770 );
6771 assert_eq!(
6772 (enc.init_params.frameRateNum, enc.init_params.frameRateDen),
6773 (num, den),
6774 "{codec:?}"
6775 );
6776 enc.encode_cpu_argb(&f, 640 * 4, 1, 25, false)
6777 .expect("encode after the change");
6778 }
6779 }
6780 }
6781
6782 #[test]
6786 #[ignore]
6787 fn gpu_frame_rate_rise_raises_the_level_at_a_key_frame() {
6788 let mut s = settings(1920, 1080, 60.0);
6789 let mut enc = host_session(&s).expect("NVENC init");
6790 let f = frame(1920, 1080, 30);
6791 for i in 0..3u64 {
6792 enc.encode_cpu_argb(&f, 1920 * 4, i, 25, i == 0)
6793 .expect("encode");
6794 }
6795 assert_eq!(enc.declared_level(), 42);
6796 s.target_fps = 120.0;
6797 assert!(enc.reconfigure_rate(&s), "the driver refused 120 fps");
6798 let pkt = enc
6799 .encode_cpu_argb(&f, 1920 * 4, 3, 25, false)
6800 .expect("encode");
6801 assert_eq!(
6802 pkt[1] & 0x0f,
6803 FRAME_KEY,
6804 "the raised level reaches the stream at a key frame"
6805 );
6806 let sps = crate::encoders::codec::annexb_nals(&pkt[VIDEO_HEADER_LEN..])
6807 .find(|n| n[0] & 0x1f == 7)
6808 .expect("an SPS");
6809 assert_eq!(sps[3], 51, "level_idc");
6810 }
6811
6812 #[test]
6817 #[ignore]
6818 fn gpu_rate_reconfigure_moves_the_vbv_initial_delay_with_the_buffer() {
6819 let (w, h) = (1920usize, 1080usize);
6820 let mut s = settings(w as i32, h as i32, 60.0);
6821 s.video_cbr_mode = true;
6822 s.video_bitrate_kbps = 8000;
6823 let mut enc = host_session(&s).expect("NVENC init");
6824 let rate = |enc: &NvencEncoder| {
6825 let rc = enc.encode_config.rcParams;
6826 (rc.averageBitRate, rc.vbvBufferSize, rc.vbvInitialDelay)
6827 };
6828 let vbv8 = cbr_vbv(&s, 8_000_000);
6829 assert_eq!(rate(&enc), (8_000_000, vbv8, vbv8));
6830 let frames: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(w, h, i)).collect();
6831 enc.encode_cpu_argb(&frames[0], w * 4, 0, 25, true)
6832 .expect("encode");
6833
6834 s.video_bitrate_kbps = 2000;
6835 assert!(enc.reconfigure_rate(&s), "the driver takes the lower rate");
6836 let vbv2 = cbr_vbv(&s, 2_000_000);
6837 assert_eq!(rate(&enc), (2_000_000, vbv2, vbv2));
6838 let n = 120u64;
6839 let mut bytes = 0usize;
6840 for i in 1..=n {
6841 let pkt = enc
6842 .encode_cpu_argb(&frames[i as usize % 16], w * 4, i, 25, false)
6843 .expect("encode");
6844 bytes += pkt.len().saturating_sub(VIDEO_HEADER_LEN);
6845 }
6846 let kbps = bytes as f64 * 8.0 * 60.0 / n as f64 / 1000.0;
6847 println!("CBR 2000 kbps after the rate change: {kbps:.0} kbps over {n} steady frames");
6848 assert!(
6849 (1500.0..=2500.0).contains(&kbps),
6850 "the session encodes at the new rate: {kbps:.0} kbps"
6851 );
6852
6853 enc.encode_config.rcParams.vbvInitialDelay = vbv2 * 4;
6854 let (status, detail) = reconfigure_raw(&mut enc);
6855 println!("a VBV initial delay of four buffers: {status:?} ({detail})");
6856 enc.encode_config.rcParams.vbvInitialDelay = vbv2;
6857 assert_eq!(reconfigure_raw(&mut enc).0, NVENCSTATUS::NV_ENC_SUCCESS);
6858 enc.encode_cpu_argb(&frames[7], w * 4, n + 1, 25, false)
6859 .expect("the session still encodes");
6860 }
6861
6862 #[test]
6867 #[ignore]
6868 fn gpu_frames_carry_four_slices_and_hevc_declares_high_tier() {
6869 use crate::encoders::codec::annexb_nals;
6870 for (w, h) in [(1280usize, 720usize), (640, 480)] {
6871 for codec in [Codec::H264, Codec::H265] {
6872 let mut s = settings(w as i32, h as i32, 60.0);
6873 s.codec = codec;
6874 let mut enc = host_session(&s).expect("NVENC init");
6875 for i in 0..4u64 {
6876 let pkt = enc
6877 .encode_cpu_argb(&frame(w, h, 20 + i as u8), w * 4, i, 25, i == 0)
6878 .expect("encode");
6879 let au = &pkt[VIDEO_HEADER_LEN..];
6880 let vcl = annexb_nals(au)
6881 .filter(|nal| match codec {
6882 Codec::H265 => (nal[0] >> 1) & 0x3f < 32,
6883 _ => matches!(nal[0] & 0x1f, 1 | 5),
6884 })
6885 .count();
6886 assert_eq!(
6887 vcl, SLICES_PER_FRAME as usize,
6888 "{codec:?} {w}x{h} frame {i} slices"
6889 );
6890 if codec == Codec::H264 && i == 0 {
6891 let sps = annexb_nals(au)
6896 .find(|nal| nal[0] & 0x1f == 7)
6897 .map(rbsp)
6898 .expect("an SPS on the key frame");
6899 assert_eq!(
6900 sps[3] as u32,
6901 nvenc_level(Codec::H264, w as u32, h as u32, 60, 0, true),
6902 "level_idc at {w}x{h}"
6903 );
6904 }
6905 if codec == Codec::H265 && i == 0 {
6906 let sps = annexb_nals(au)
6907 .find(|nal| (nal[0] >> 1) & 0x3f == 33)
6908 .map(rbsp)
6909 .expect("an SPS on the key frame");
6910 assert_eq!((sps[3] >> 5) & 1, 1, "general_tier_flag at {w}x{h}");
6916 assert_eq!(
6917 sps[14] as u32,
6918 nvenc_level(Codec::H265, w as u32, h as u32, 60, 0, true),
6919 "general_level_idc at {w}x{h}"
6920 );
6921 }
6922 }
6923 }
6924 }
6925 }
6926
6927 #[test]
6931 #[ignore]
6932 fn gpu_bench_slices() {
6933 let (w, h) = (1920usize, 1080usize);
6934 let n: usize = std::env::var("NVENC_BENCH_FRAMES")
6935 .ok()
6936 .and_then(|v| v.parse().ok())
6937 .unwrap_or(240);
6938 let frames: Vec<Vec<u8>> = (0..8u8).map(|k| frame(w, h, 10 + 30 * k)).collect();
6939 for codec in [Codec::H264, Codec::H265] {
6940 let mut s = settings(w as i32, h as i32, 60.0);
6941 s.codec = codec;
6942 let mut sizes = Vec::new();
6943 for slices in [1u32, SLICES_PER_FRAME] {
6944 let tuning = NvencTuning {
6945 slices,
6946 ..NvencTuning::default()
6947 };
6948 let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), tuning) {
6949 Ok(enc) => enc,
6950 Err(e) => {
6951 println!("{codec:?}: {e}");
6952 continue;
6953 }
6954 };
6955 enc.encode_cpu_packed(&frames[0], w * 4, false, 0, 25, true)
6956 .expect("warm-up");
6957 let mut bytes = 0usize;
6958 per_frame(&format!("{codec:?} {slices} slice(s)"), n, |i| {
6959 bytes += enc
6960 .encode_cpu_packed(&frames[i % 8], w * 4, false, 1 + i as u64, 25, false)
6961 .expect("encode")
6962 .len();
6963 });
6964 println!(" {} kbps", bytes * 8 * 60 / n / 1000);
6965 sizes.push(bytes);
6966 }
6967 if let [one, many] = sizes[..] {
6968 println!(
6969 "{codec:?}: {SLICES_PER_FRAME} slices cost {:+.2}% bitrate at a fixed quantizer",
6970 (many as f64 / one as f64 - 1.0) * 100.0
6971 );
6972 }
6973 }
6974 }
6975
6976 #[test]
6986 #[ignore]
6987 fn gpu_bench_cbr_policy() {
6988 let (w, h) = (1920usize, 1080usize);
6989 let env = |name: &str, default: usize| {
6990 std::env::var(name)
6991 .ok()
6992 .and_then(|v| v.parse().ok())
6993 .unwrap_or(default)
6994 };
6995 let n = env("NVENC_BENCH_FRAMES", 120);
6996 let kbps = env("NVENC_BENCH_KBPS", 8000);
6997 let fps = env("NVENC_BENCH_FPS", 60);
6998 let cuts: Vec<Vec<u8>> = (0..8u8).map(|k| frame(w, h, 10 + 30 * k)).collect();
6999 let steady: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(w, h, i)).collect();
7000 let other = frame(w, h, 200);
7001 let contents: Vec<(&str, Vec<&Vec<u8>>)> = vec![
7002 (
7003 "scene cuts",
7004 std::iter::once(&cuts[0])
7005 .chain((0..n).map(|i| &cuts[1 + i % 7]))
7006 .collect(),
7007 ),
7008 (
7009 "steady",
7010 std::iter::once(&steady[0])
7011 .chain((0..n).map(|i| &steady[1 + i % 15]))
7012 .collect(),
7013 ),
7014 (
7015 "one cut",
7016 std::iter::once(&other)
7017 .chain((0..n).map(|i| &steady[i % 16]))
7018 .collect(),
7019 ),
7020 ];
7021 println!("CBR {kbps} kbit/s at {fps} fps, {n} frames per row");
7022 for codec in [Codec::H264, Codec::H265] {
7023 for (content, seq) in &contents {
7024 for vbv_frames in [1.0f64, 1.5] {
7025 for full_delay in [true, false] {
7026 for slices in [SLICES_PER_FRAME, 1] {
7027 let mut s = settings(w as i32, h as i32, fps as f64);
7028 s.codec = codec;
7029 s.video_cbr_mode = true;
7030 s.video_bitrate_kbps = kbps as i32;
7031 s.video_vbv_multiplier = vbv_frames;
7032 let tuning = NvencTuning {
7033 slices,
7034 ..NvencTuning::default()
7035 };
7036 let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), tuning) {
7037 Ok(enc) => enc,
7038 Err(e) => {
7039 println!("{codec:?}: {e}");
7040 continue;
7041 }
7042 };
7043 if !full_delay {
7044 enc.encode_config.rcParams.vbvInitialDelay = 0;
7045 assert_eq!(
7046 reconfigure_raw(&mut enc).0,
7047 NVENCSTATUS::NV_ENC_SUCCESS
7048 );
7049 }
7050 let label = format!(
7051 "{codec:?} {content}: VBV {vbv_frames} frame(s), initial delay {}, {slices} slice(s)",
7052 if full_delay { "full" } else { "default" }
7053 );
7054 cbr_row(&label, &mut enc, codec, seq, w, h, fps);
7055 }
7056 }
7057 }
7058 }
7059 }
7060 }
7061
7062 #[test]
7069 #[ignore]
7070 fn gpu_bench_cbr_rate_control() {
7071 let (w, h) = (1920usize, 1080usize);
7072 let env = |name: &str, default: usize| {
7073 std::env::var(name)
7074 .ok()
7075 .and_then(|v| v.parse().ok())
7076 .unwrap_or(default)
7077 };
7078 let n = env("NVENC_BENCH_FRAMES", 120);
7079 let kbps = env("NVENC_BENCH_KBPS", 8000);
7080 let fps = env("NVENC_BENCH_FPS", 60);
7081 let cuts: Vec<Vec<u8>> = (0..8u8).map(|k| frame(w, h, 10 + 30 * k)).collect();
7082 let steady: Vec<Vec<u8>> = (0..16).map(|i| moving_frame(w, h, i)).collect();
7083 let other = frame(w, h, 200);
7084 let contents: Vec<(&str, Vec<&Vec<u8>>)> = vec![
7085 (
7086 "scene cuts",
7087 std::iter::once(&cuts[0])
7088 .chain((0..n).map(|i| &cuts[1 + i % 7]))
7089 .collect(),
7090 ),
7091 (
7092 "one cut",
7093 std::iter::once(&other)
7094 .chain((0..n).map(|i| &steady[i % 16]))
7095 .collect(),
7096 ),
7097 ];
7098 let variants = [
7099 (
7100 "production",
7101 NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
7102 true,
7103 0,
7104 ),
7105 (
7106 "single pass",
7107 NV_ENC_MULTI_PASS::NV_ENC_MULTI_PASS_DISABLED,
7108 true,
7109 0,
7110 ),
7111 (
7112 "two-pass full",
7113 NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_FULL_RESOLUTION,
7114 true,
7115 0,
7116 ),
7117 (
7118 "strictGOPTarget off",
7119 NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
7120 false,
7121 0,
7122 ),
7123 (
7124 "max QP 51",
7125 NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
7126 true,
7127 51,
7128 ),
7129 ];
7130 println!("H264 CBR {kbps} kbit/s at {fps} fps, {n} frames per row");
7131 for (content, seq) in &contents {
7132 for vbv_frames in [1.0f64, 1.5] {
7133 for (name, multipass, strict, max_qp) in variants {
7134 let mut s = settings(w as i32, h as i32, fps as f64);
7135 s.video_cbr_mode = true;
7136 s.video_bitrate_kbps = kbps as i32;
7137 s.video_vbv_multiplier = vbv_frames;
7138 s.video_max_qp = max_qp;
7139 let tuning = NvencTuning {
7140 multipass,
7141 ..NvencTuning::default()
7142 };
7143 let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), tuning) {
7144 Ok(enc) => enc,
7145 Err(e) => {
7146 println!("{name}: {e}");
7147 continue;
7148 }
7149 };
7150 if !strict {
7151 enc.encode_config.rcParams.set_strictGOPTarget(0);
7152 assert_eq!(reconfigure_raw(&mut enc).0, NVENCSTATUS::NV_ENC_SUCCESS);
7153 }
7154 cbr_row(
7155 &format!("{content}: VBV {vbv_frames} frame(s), {name}"),
7156 &mut enc,
7157 Codec::H264,
7158 seq,
7159 w,
7160 h,
7161 fps,
7162 );
7163 }
7164 }
7165 }
7166 }
7167
7168 #[test]
7178 #[ignore]
7179 fn gpu_hevc_high_tier_opens_above_the_main_tier_ceiling() {
7180 let mut s = settings(3840, 2160, 60.0);
7181 s.codec = Codec::H265;
7182 s.video_cbr_mode = true;
7183 s.video_bitrate_kbps = 100_000;
7185
7186 let high = host_session(&s);
7187 match &high {
7188 Ok(enc) => unsafe {
7189 let c = enc.encode_config.encodeCodecConfig.hevcConfig;
7190 assert_eq!(c.tier, 1);
7191 println!(
7192 "HEVC High tier at {} kbps: opened, level {}",
7193 s.video_bitrate_kbps, c.level,
7194 );
7195 },
7196 Err(e) => panic!("High tier must open above the Main-tier ceiling: {e}"),
7197 }
7198 drop(high);
7199
7200 let main = NvencEncoder::new_tuned(
7202 &s,
7203 ptr::null(),
7204 NvencTuning {
7205 hevc_high_tier: false,
7206 ..NvencTuning::default()
7207 },
7208 );
7209 match main {
7210 Ok(enc) => unsafe {
7211 println!(
7212 "HEVC Main tier at {} kbps: opened on level {}",
7213 s.video_bitrate_kbps, enc.encode_config.encodeCodecConfig.hevcConfig.level,
7214 )
7215 },
7216 Err(e) => println!(
7217 "HEVC Main tier at {} kbps: refused, the Main-tier ceiling: {e}",
7218 s.video_bitrate_kbps,
7219 ),
7220 }
7221 }
7222
7223 #[test]
7228 #[ignore]
7229 fn gpu_split_frame_follows_the_codec_and_picture() {
7230 use crate::webcam::decode::VideoDecoder;
7231 use NV_ENC_SPLIT_ENCODE_MODE as Split;
7232 for codec in [Codec::H265, Codec::Av1] {
7233 let mut s = settings(1920, 1080, 60.0);
7234 s.codec = codec;
7235 let mut enc = match host_session(&s) {
7236 Ok(enc) => enc,
7237 Err(e) if e.contains("engine") => {
7238 println!("{codec:?}: {e}");
7239 continue;
7240 }
7241 Err(e) => panic!("{codec:?}: {e}"),
7242 };
7243 if !enc.engines.is_some_and(|n| n > 1) || nvenc_cur_ver() < (12, 1) {
7244 println!(
7245 "{codec:?}: {}, API {:?}",
7246 enc.split_summary(),
7247 nvenc_cur_ver()
7248 );
7249 assert_eq!(
7250 enc.init_params.splitEncodeMode(),
7251 Split::NV_ENC_SPLIT_AUTO_MODE as u32
7252 );
7253 continue;
7254 }
7255 for (n, (w, h)) in [(1920usize, 1080usize), (3840, 2160), (1920, 1080)]
7256 .into_iter()
7257 .enumerate()
7258 {
7259 if n > 0 {
7260 s.width = w as i32;
7261 s.height = h as i32;
7262 assert!(enc.reconfigure_resolution(&s).expect("resize"));
7263 }
7264 let want = match codec {
7265 Codec::Av1 if enc.engines == Some(3) => Split::NV_ENC_SPLIT_THREE_FORCED_MODE,
7266 Codec::Av1 => Split::NV_ENC_SPLIT_AUTO_FORCED_MODE,
7267 _ if w * h >= 3840 * 2160 => Split::NV_ENC_SPLIT_AUTO_FORCED_MODE,
7268 _ => Split::NV_ENC_SPLIT_AUTO_MODE,
7269 };
7270 assert_eq!(
7271 enc.init_params.splitEncodeMode(),
7272 want as u32,
7273 "{codec:?} at {w}x{h}: {}",
7274 enc.split_summary()
7275 );
7276 let mut dec = VideoDecoder::new(codec).expect("decoder");
7277 for i in 0..6usize {
7278 let f = moving_frame(w, h, i);
7279 let pkt = enc
7280 .encode_cpu_packed(&f, w * 4, false, (n * 10 + i) as u64, 25, i == 0)
7281 .expect("encode");
7282 let psnr = luma_psnr(&mut dec, &pkt, &f, w, h);
7283 assert!(psnr > 30.0, "{codec:?} {w}x{h} frame {i}: {psnr:.1} dB");
7284 }
7285 println!("{codec:?} {w}x{h}: {}", enc.split_summary());
7286 }
7287 }
7288 }
7289
7290 #[test]
7296 #[ignore]
7297 fn gpu_cbr_targets_past_the_picture_level_open() {
7298 for (codec, kbps, level) in [
7299 (Codec::H264, 100_000, 50),
7300 (Codec::H265, 60_000, 150),
7301 (Codec::Av1, 30_000, 14),
7302 ] {
7303 let mut s = settings(1920, 1080, 60.0);
7304 s.codec = codec;
7305 s.video_cbr_mode = true;
7306 s.video_bitrate_kbps = kbps;
7307 let mut enc = match host_session(&s) {
7308 Ok(enc) => enc,
7309 Err(e) if e.contains("engine") => {
7310 println!("{codec:?}: {e}");
7311 continue;
7312 }
7313 Err(e) => panic!("{codec:?} at {kbps} kbps must open on level {level}: {e}"),
7314 };
7315 let level = if codec == Codec::Av1 && !NVENC_AV1_WEIGHTED.load(Ordering::Relaxed) {
7316 13
7317 } else {
7318 level
7319 };
7320 assert_eq!(
7321 enc.declared_level(),
7322 level,
7323 "{codec:?} level at {kbps} kbps"
7324 );
7325 s.video_bitrate_kbps = 200_000;
7326 assert!(
7327 enc.reconfigure_rate(&s),
7328 "{codec:?} live raise to 200 Mbit/s refused"
7329 );
7330 assert!(
7331 enc.declared_level() > level,
7332 "{codec:?} level did not rise with the target"
7333 );
7334 println!(
7335 "{codec:?}: {kbps} kbps opened on level {level}; 200 Mbit/s live took level {}",
7336 enc.declared_level()
7337 );
7338 }
7339 }
7340
7341 #[test]
7346 #[ignore]
7347 fn gpu_av1_declares_the_lowest_level_the_driver_admits() {
7348 let f = frame(1920, 1080, 30);
7349 for raised in [false, true] {
7350 NVENC_AV1_WEIGHTED.store(false, Ordering::Relaxed);
7351 let mut s = settings(1920, 1080, 60.0);
7352 s.codec = Codec::Av1;
7353 s.video_cbr_mode = true;
7354 s.video_bitrate_kbps = if raised { 8_000 } else { 30_000 };
7355 let mut enc = match host_session(&s) {
7356 Ok(enc) => enc,
7357 Err(e) if e.contains("engine") => {
7358 println!("Av1: {e}");
7359 return;
7360 }
7361 Err(e) => panic!("Av1 at {} kbps: {e}", s.video_bitrate_kbps),
7362 };
7363 enc.encode_cpu_argb(&f, 1920 * 4, 0, 25, true)
7364 .expect("encode");
7365 if raised {
7366 assert_eq!(enc.declared_level(), 13);
7367 s.video_bitrate_kbps = 30_000;
7368 assert!(
7369 enc.reconfigure_rate(&s),
7370 "the raise to 30 Mbit/s was refused"
7371 );
7372 }
7373 let weighted = NVENC_AV1_WEIGHTED.load(Ordering::Relaxed);
7374 assert_eq!(enc.declared_level(), if weighted { 14 } else { 13 });
7375 let pkt = enc
7376 .encode_cpu_argb(&f, 1920 * 4, 1, 25, false)
7377 .expect("encode at the level");
7378 if raised && weighted {
7379 assert!(
7380 crate::encoders::codec::av1_is_key(&pkt[VIDEO_HEADER_LEN..]),
7381 "a raised level reaches the decoder at a key frame"
7382 );
7383 }
7384 println!(
7385 "Av1 30 Mbit/s {}: level {} ({})",
7386 if raised { "raised from 8" } else { "opened" },
7387 enc.declared_level(),
7388 if weighted {
7389 "the driver holds two thirds of Annex A"
7390 } else {
7391 "Annex A"
7392 }
7393 );
7394 }
7395 }
7396
7397 #[test]
7403 #[ignore]
7404 fn gpu_reconfigures_cost_a_key_frame_only_where_asked() {
7405 let f = frame(1920, 1080, 30);
7406 let kind = |enc: &mut NvencEncoder, n: u64, crf: u32| {
7407 enc.encode_cpu_argb(&f, 1920 * 4, n, crf, n == 0)
7408 .expect("encode")[1]
7409 & 0x0f
7410 };
7411 let mut s = settings(1920, 1080, 60.0);
7412 s.video_cbr_mode = true;
7413 s.video_bitrate_kbps = 8000;
7414 let mut enc = host_session(&s).expect("NVENC init");
7415 kind(&mut enc, 0, 25);
7416 for (n, kbps) in [(1, 6000), (2, 8000), (3, 6000)] {
7417 s.video_bitrate_kbps = kbps;
7418 assert!(enc.reconfigure_rate(&s));
7419 assert_eq!(
7420 kind(&mut enc, n, 25),
7421 FRAME_DELTA,
7422 "{:?}: after the change to {kbps} kbit/s",
7423 nvenc_cur_ver()
7424 );
7425 }
7426 s.video_bitrate_kbps = 200_000;
7427 assert!(enc.reconfigure_rate(&s));
7428 assert_eq!(
7429 kind(&mut enc, 4, 25),
7430 FRAME_KEY,
7431 "{:?}: the raised level reaches the stream at a key frame",
7432 nvenc_cur_ver()
7433 );
7434 let mut enc = host_session(&settings(1920, 1080, 60.0)).expect("NVENC init");
7435 for n in 0..6u64 {
7436 let want = if n == 0 { FRAME_KEY } else { FRAME_DELTA };
7437 assert_eq!(
7438 kind(&mut enc, n, if n % 2 == 0 { 25 } else { 18 }),
7439 want,
7440 "{:?}: paint-over frame {n}",
7441 nvenc_cur_ver()
7442 );
7443 }
7444 }
7445
7446 #[test]
7449 #[ignore]
7450 fn gpu_vram_probe() {
7451 fn used_mb() -> Option<i64> {
7456 let out = std::process::Command::new("nvidia-smi")
7457 .args(["--query-gpu=memory.used", "--format=csv,noheader,nounits"])
7458 .output()
7459 .ok()?;
7460 String::from_utf8_lossy(&out.stdout)
7461 .lines()
7462 .map(|l| l.trim().parse::<i64>().ok())
7463 .sum()
7464 }
7465 let s = settings(1920, 1080, 60.0);
7466 let Some(before) = used_mb() else {
7467 println!("VRAM probe skipped: nvidia-smi answered nothing");
7468 return;
7469 };
7470 let mut enc = host_session(&s).expect("init");
7471 let f = frame(1920, 1080, 5);
7472 for i in 0..3u64 {
7473 enc.encode_cpu_argb(&f, 1920 * 4, i, 25, i == 0)
7474 .expect("encode");
7475 }
7476 println!(
7477 "VRAM delta for one 1080p session: {} MiB",
7478 used_mb().unwrap_or(before) - before
7479 );
7480 }
7481
7482 #[test]
7492 #[ignore]
7493 fn gpu_vui_describes_the_conversion() {
7494 for fullcolor in [false, true] {
7495 let mut s = settings(1280, 720, 60.0);
7496 s.video_fullcolor = fullcolor;
7497 let enc = host_session(&s).expect("NVENC init");
7498 let h264 = unsafe { &enc.encode_config.encodeCodecConfig.h264Config };
7500 let vui = &h264.h264VUIParameters;
7501 assert_eq!(
7502 vui.colourMatrix as u32,
7503 NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709 as u32,
7504 "fullcolor={fullcolor}"
7505 );
7506 assert_eq!(
7507 vui.colourPrimaries as u32,
7508 NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709 as u32
7509 );
7510 assert_eq!(
7511 vui.transferCharacteristics as u32,
7512 NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709 as u32
7513 );
7514 assert_eq!(vui.videoFullRangeFlag, 0, "fullcolor={fullcolor}");
7515 assert_eq!(h264.chromaFormatIDC, if fullcolor { 3 } else { 1 });
7516 }
7517 }
7518
7519 #[test]
7525 #[ignore]
7526 fn gpu_dmabuf_chroma_is_sited_at_the_block_center() {
7527 use crate::webcam::decode::{Codec, Decoder as _, VideoDecoder};
7528 let (w, h) = (256u32, 256u32);
7529 let s = settings(w as i32, h as i32, 60.0);
7530 let (gbm, mut renderer) = gpu_render();
7531 let egl_display = renderer.egl_context().display().get_display_handle().handle;
7532 let (_bo, dmabuf) = column_dmabuf(&gbm, &mut renderer, w, h);
7533 let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
7534 assert!(enc.csc.is_some(), "this GPU took no chroma convert");
7535 let pkt = enc.encode(&dmabuf, 0, 20, true).expect("dmabuf encode");
7536 let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
7537 assert!(
7538 dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
7539 "no picture"
7540 );
7541 let v = dec.frame().expect("decoded frame");
7542 let (mut su, mut sv) = (0.0f64, 0.0f64);
7543 let n = (v.chroma_height() * v.chroma_width()) as f64;
7544 for r in 0..v.chroma_height() {
7545 for c in 0..v.chroma_width() {
7546 let i = r * v.uv_stride + c;
7547 su += f64::from(v.u[i]);
7548 sv += f64::from(v.v[i]);
7549 }
7550 }
7551 let (u, cr) = (su / n, sv / n);
7552 println!(
7553 "[chroma-siting] dmabuf mapped as {}: ({u:.1}, {cr:.1})",
7554 mapped_kind(&enc)
7555 );
7556 let off = (u - 128.0).hypot(cr - 128.0);
7557 assert!(
7558 off <= 2.0,
7559 "the zero-copy path leaves chroma {off:.1} off neutral"
7560 );
7561 }
7562
7563 fn column_dmabuf(
7566 gbm: &gbm::Device<std::fs::File>,
7567 renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
7568 w: u32,
7569 h: u32,
7570 ) -> (gbm::BufferObject<()>, Dmabuf) {
7571 use gbm::{BufferObjectFlags, Format as GbmFormat};
7572 use smithay::backend::renderer::{Bind, Color32F, Frame, Renderer};
7573 use smithay::utils::{Physical, Rectangle, Size, Transform};
7574 let bo = gbm
7575 .create_buffer_object::<()>(w, h, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
7576 .expect("GBM buffer");
7577 let mut dmabuf = crate::create_dmabuf_from_bo(&bo);
7578 {
7579 let mut fb = renderer.bind(&mut dmabuf).expect("bind dmabuf");
7580 let size: Size<i32, Physical> = (w as i32, h as i32).into();
7581 let mut frame = renderer
7582 .render(&mut fb, size, Transform::Normal)
7583 .expect("render");
7584 let full: Rectangle<i32, Physical> = Rectangle::from_size(size);
7585 frame
7586 .clear(Color32F::new(0.0, 0.0, 1.0, 1.0), &[full])
7587 .expect("clear");
7588 for x in (1..w as i32).step_by(2) {
7589 let column: Rectangle<i32, Physical> =
7590 Rectangle::new((x, 0).into(), (1, h as i32).into());
7591 frame
7592 .draw_solid(
7593 column,
7594 &[Rectangle::from_size(column.size)],
7595 Color32F::new(1.0, 1.0, 0.0, 1.0),
7596 )
7597 .expect("draw column");
7598 }
7599 let sync = frame.finish().expect("finish");
7600 let _ = sync.wait();
7601 }
7602 (bo, dmabuf)
7603 }
7604
7605 #[test]
7616 #[ignore]
7617 fn gpu_chroma_is_sited_at_the_block_center() {
7618 use crate::encoders::chroma_siting::{BT709, chroma};
7619 let (w, h) = (256usize, 256usize);
7620 let (blue, yellow) = ([0.0, 0.0, 255.0], [255.0, 255.0, 0.0]);
7621 let st = settings(w as i32, h as i32, 60.0);
7622 let mut enc = host_session(&st).expect("NVENC init");
7623 assert!(enc.csc.is_some(), "this GPU took no chroma convert");
7624 let rows = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, false));
7625 let cols = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, true));
7626 println!("[chroma-siting] convert: rows {rows:?} columns {cols:?}");
7627 for (label, (u, v)) in [("rows", rows), ("columns", cols)] {
7628 let off = (u - 128.0).hypot(v - 128.0);
7629 assert!(off <= 2.0, "{label} come back {off:.1} off neutral chroma");
7630 }
7631 unsafe {
7632 let cu = enc.cuda.clone();
7633 (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
7634 if let Some(csc) = enc.csc.take() {
7635 csc.release(&cu, &enc.nvenc_funcs, enc.encoder_session);
7636 }
7637 (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
7638 }
7639 let hardware = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, true));
7640 let weighted = chroma([0, 1, 2].map(|i| 0.75 * blue[i] + 0.25 * yellow[i]), BT709);
7641 println!("[chroma-siting] hardware: columns {hardware:?} against 3:1 {weighted:?}");
7642 assert!(
7643 (hardware.0 - weighted.0).hypot(hardware.1 - weighted.1) <= 2.0,
7644 "NVENC's own conversion weights the columns 3:1: {hardware:?} against {weighted:?}"
7645 );
7646
7647 let full = RustCaptureSettings {
7648 video_fullcolor: true,
7649 ..st
7650 };
7651 let enc444 = host_session(&full).expect("NVENC init");
7652 if enc444.is_fullcolor() {
7653 assert!(
7654 enc444.csc.is_none(),
7655 "a 4:4:4 session has no chroma to site and takes no convert"
7656 );
7657 }
7658 }
7659
7660 #[test]
7669 #[ignore]
7670 fn gpu_chart_decodes_to_the_color_that_was_painted() {
7671 use crate::encoders::chroma_siting::{BT709, chart_bgra, chart_error};
7672 use crate::encoders::sps::{h264_chroma_format_idc, read_color};
7673 use crate::webcam::decode::{Decoder as _, VideoDecoder};
7674 let (w, h) = (256usize, 128usize);
7675 for (codec, fullcolor) in [
7676 (Codec::H264, false),
7677 (Codec::H264, true),
7678 (Codec::H265, true),
7679 ] {
7680 let st = RustCaptureSettings {
7681 codec,
7682 video_fullcolor: fullcolor,
7683 ..settings(w as i32, h as i32, 60.0)
7684 };
7685 let mut enc = host_session(&st).expect("NVENC init");
7686 if fullcolor && !enc.is_fullcolor() {
7687 println!("[chart] this GPU carries no 4:4:4 {codec:?}");
7688 continue;
7689 }
7690 assert_eq!(
7691 enc.csc.is_some(),
7692 !fullcolor,
7693 "the convert follows the chroma format"
7694 );
7695 let check = |enc: &mut NvencEncoder, bgra: &[u8], w: usize, frame: u64, what: &str| {
7696 let pkt = enc
7697 .encode_cpu_packed(bgra, w * 4, false, frame, 20, true)
7698 .expect("encode");
7699 let stream = &pkt[VIDEO_HEADER_LEN..];
7700 if codec == Codec::H264 && fullcolor {
7701 assert_eq!(
7702 h264_chroma_format_idc(stream),
7703 Some(3),
7704 "the SPS declares 4:4:4"
7705 );
7706 let sps = crate::encoders::codec::annexb_nals(stream)
7707 .find(|n| n[0] & 0x1f == 7)
7708 .expect("an SPS");
7709 assert_eq!(
7710 read_color(sps).map(|s| (s.matrix, s.full_range)),
7711 Some((1, false)),
7712 "the SPS declares BT.709 limited"
7713 );
7714 println!("[chart] NVENC H264 4:4:4{what}: the SPS declares 4:4:4 BT.709");
7715 return;
7716 }
7717 let mut dec = VideoDecoder::new(codec).expect("decoder");
7718 assert!(dec.decode(stream).expect("decode"), "no picture");
7719 let worst = chart_error(&dec.frame().expect("decoded frame"), BT709);
7720 println!(
7721 "[chart] NVENC {codec:?} 4:4:4 {fullcolor}{what}: worst |dRGB| {worst:.1}"
7722 );
7723 assert!(
7724 worst <= 8.0,
7725 "the session paints {worst:.1} off the chart{what}"
7726 );
7727 };
7728 check(&mut enc, &chart_bgra(w, h), w, 0, "");
7729
7730 let (w2, h2) = (w + 64, h + 32);
7733 let grown = RustCaptureSettings {
7734 width: w2 as i32,
7735 height: h2 as i32,
7736 ..st
7737 };
7738 assert!(
7739 enc.reconfigure_resolution(&grown).expect("resize"),
7740 "the resize was taken"
7741 );
7742 assert_eq!(
7743 enc.csc.is_some(),
7744 !fullcolor,
7745 "the convert followed the resize"
7746 );
7747 check(&mut enc, &chart_bgra(w2, h2), w2, 1, " after a resize");
7748 }
7749 }
7750
7751 #[test]
7758 #[ignore]
7759 fn gpu_hardware_conversion_matches_the_declared_matrix() {
7760 use crate::encoders::chroma_siting::{BT601, BT709, chart_bgra, chart_error, chroma};
7761 use crate::webcam::decode::{Decoder as _, VideoDecoder};
7762 let (w, h) = (256usize, 256usize);
7763 let (blue, yellow) = ([0.0, 0.0, 255.0], [255.0, 255.0, 0.0]);
7764 let st = settings(w as i32, h as i32, 60.0);
7765 let tuning = NvencTuning {
7766 hardware_csc: true,
7767 ..Default::default()
7768 };
7769 let mut enc = NvencEncoder::new_tuned(&st, ptr::null(), tuning).expect("NVENC init");
7770 assert!(
7771 enc.csc.is_none(),
7772 "the session was asked for NVENC's own conversion"
7773 );
7774
7775 let pkt = enc
7776 .encode_cpu_packed(&chart_bgra(w, h), w * 4, false, 0, 20, true)
7777 .expect("encode");
7778 let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
7779 assert!(
7780 dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
7781 "no picture"
7782 );
7783 let worst = chart_error(&dec.frame().expect("decoded frame"), BT709);
7784 println!("[csc] NVENC's own conversion: chart worst |dRGB| {worst:.1}");
7785 assert!(
7786 worst <= 8.0,
7787 "the hardware conversion paints {worst:.1} off the declared matrix"
7788 );
7789
7790 let hardware = encode_and_measure(&mut enc, &color_pair(w, h, blue, yellow, true));
7791 let mix = [0, 1, 2].map(|i| 0.75 * blue[i] + 0.25 * yellow[i]);
7792 let (weighted_601, weighted_709) = (chroma(mix, BT601), chroma(mix, BT709));
7793 let off = |c: (f64, f64)| (hardware.0 - c.0).hypot(hardware.1 - c.1);
7794 println!(
7795 "[csc] NVENC's own conversion: columns {hardware:?} against the 3:1 mix under \
7796 BT.709 {weighted_709:?} and BT.601 {weighted_601:?}"
7797 );
7798 assert!(
7799 off(weighted_709) <= 2.0 && off(weighted_709) < off(weighted_601),
7800 "the hardware conversion did not follow the declared matrix: {hardware:?}"
7801 );
7802 }
7803
7804 #[test]
7810 #[ignore]
7811 fn gpu_external_pointer_is_encoded_where_it_lies() {
7812 use crate::encoders::chroma_siting::{BT709, ycbcr};
7813 use crate::webcam::decode::VideoDecoder;
7814 let (w, h) = (256usize, 128usize);
7815 let st = settings(w as i32, h as i32, 60.0);
7816 let mut enc = host_session(&st).expect("NVENC init");
7817 let paint = [32u8, 192, 64];
7818 let poison = [240u8, 16, 200];
7819 let (external, external_pitch) = unsafe {
7820 let cu = enc.cuda.clone();
7821 (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
7822 let (mut ptr_, mut pitch) = (0 as CUdeviceptr, 0usize);
7823 assert_eq!(
7824 (cu.cuMemAllocPitch_v2)(&mut ptr_, &mut pitch, w * 4, h, 16),
7825 CUresult::CUDA_SUCCESS
7826 );
7827 paint_surface(&cu, ptr_, pitch, w, h, paint);
7828 paint_surface(&cu, enc.input_device_ptr, enc.input_pitch, w, h, poison);
7829 (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
7830 (ptr_, pitch)
7831 };
7832 let pkt = enc
7833 .encode_cuda_pitch(external, external_pitch, false, 0, 20, true)
7834 .expect("external encode");
7835 let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
7836 let (mean, _) = decoded_means(&mut dec, &pkt, (0, 0, w as i32, h as i32));
7837 let want = ycbcr(paint.map(f64::from), BT709);
7838 println!("[external] decoded {mean:?} against the painted {want:?}");
7839 for i in 0..3 {
7840 assert!(
7841 (mean[i] - want[i]).abs() <= 8.0,
7842 "plane {i} came back {:.1}, not the painted {:.1}: the frame was staged, not read in place",
7843 mean[i],
7844 want[i]
7845 );
7846 }
7847 assert!(
7848 enc.encode_cuda_pitch(external, w * 4 - 4, false, 1, 20, false)
7849 .is_err(),
7850 "a pitch too short for the session's rows must be refused"
7851 );
7852 enc.release_external_input();
7853 unsafe {
7854 let cu = enc.cuda.clone();
7855 (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
7856 (cu.cuMemFree_v2)(external);
7857 (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
7858 }
7859 }
7860
7861 #[test]
7866 #[ignore]
7867 fn gpu_an_uploaded_frame_is_hashed_beside_its_encode() {
7868 let (w, h, rows) = (1920usize, 1080usize, 32u32);
7869 let mut enc = host_session(&settings(w as i32, h as i32, 60.0)).expect("NVENC init");
7870 let mut picture = frame(w, h, 3);
7871 let encode = |enc: &mut NvencEncoder, picture: &[u8], n: u64, hash: bool| {
7872 if hash {
7873 assert!(enc.hash_next_upload(rows), "the driver takes the band hash");
7874 }
7875 let out = enc.encode_cpu_packed(picture, w * 4, false, n, 23, n == 0);
7876 assert!(out.is_ok_and(|o| !o.is_empty()), "frame {n} encodes");
7877 enc.take_upload_hashes()
7878 };
7879 let first = encode(&mut enc, &picture, 0, true).expect("hashes come with the frame");
7880 assert_eq!(first.len(), h.div_ceil(rows as usize));
7881 assert_eq!(
7882 encode(&mut enc, &picture, 1, true),
7883 Some(first.clone()),
7884 "one picture, one hash"
7885 );
7886 assert_eq!(encode(&mut enc, &picture, 2, false), None, "none unasked");
7887 for y in 200..220 {
7888 for x in 300..302 {
7889 picture[(y * w + x) * 4..][..3].copy_from_slice(&[0, 0, 0]);
7890 }
7891 }
7892 let caret = encode(&mut enc, &picture, 3, true).unwrap();
7893 let changed: Vec<usize> = (0..first.len()).filter(|&i| first[i] != caret[i]).collect();
7894 assert_eq!(changed, vec![200 / rows as usize], "a 2x20 caret");
7895
7896 let mut timed = |hash: bool, from: u64| {
7897 let t = std::time::Instant::now();
7898 for n in from..from + 120 {
7899 picture[(n as usize % h) * w * 4] ^= 0xff;
7900 encode(&mut enc, &picture, n, hash);
7901 }
7902 t.elapsed().as_secs_f64() * 1e3 / 120.0
7903 };
7904 let (plain, hashed) = (timed(false, 4), timed(true, 124));
7905 let (plain2, hashed2) = (timed(false, 244), timed(true, 364));
7906 eprintln!(
7907 "1080p host encode: {:.3} / {:.3} ms plain, {:.3} / {:.3} ms with the device hash",
7908 plain, plain2, hashed, hashed2
7909 );
7910 }
7911
7912 #[test]
7916 #[ignore]
7917 fn gpu_band_hash_reads_the_bands_that_changed() {
7918 let (w, h, rows) = (1920usize, 1080usize, 32usize);
7919 let pitch = w * 4 + 64;
7920 let mut enc = host_session(&settings(w as i32, h as i32, 60.0)).expect("NVENC init");
7921 let (cu, ctx) = (enc.cuda.clone(), enc.cuda_context);
7922 let picture = frame(w, h, 3);
7923 let mut host = vec![0u8; pitch * h];
7924 for y in 0..h {
7925 host[y * pitch..][..w * 4].copy_from_slice(&picture[y * w * 4..][..w * 4]);
7926 }
7927 let mut dev: CUdeviceptr = 0;
7928 unsafe {
7929 (cu.cuCtxPushCurrent_v2)(ctx);
7930 assert_eq!(
7931 (cu.cuMemAlloc_v2)(&mut dev, pitch * h),
7932 CUresult::CUDA_SUCCESS
7933 );
7934 (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
7935 }
7936 let upload = |host: &[u8]| unsafe {
7937 (cu.cuCtxPushCurrent_v2)(ctx);
7938 assert_eq!(
7939 (cu.cuMemcpyHtoD_v2)(dev, host.as_ptr() as *const c_void, host.len()),
7940 CUresult::CUDA_SUCCESS
7941 );
7942 (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
7943 };
7944 let changed = |a: &[u64], b: &[u64]| -> Vec<usize> {
7945 (0..a.len()).filter(|&i| a[i] != b[i]).collect()
7946 };
7947 upload(&host);
7948 let hash =
7949 |enc: &mut NvencEncoder| enc.band_hashes(dev, pitch, w as u32, h as u32, rows as u32);
7950 let jit = std::time::Instant::now();
7951 let first = hash(&mut enc).expect("the driver takes the band hash");
7952 let jit = jit.elapsed().as_secs_f64() * 1e3;
7953 assert_eq!(first.len(), h.div_ceil(rows));
7954 assert_eq!(hash(&mut enc).unwrap(), first, "one frame, one hash");
7955
7956 for y in 200..220 {
7957 for x in 300..302 {
7958 host[y * pitch + x * 4..][..3].copy_from_slice(&[0, 0, 0]);
7959 }
7960 }
7961 upload(&host);
7962 let caret = hash(&mut enc).unwrap();
7963 assert_eq!(changed(&first, &caret), vec![200 / rows], "a 2x20 caret");
7964
7965 for y in 0..h {
7966 for x in 0..w {
7967 host[y * pitch + x * 4 + 3] ^= 0x5a;
7968 }
7969 host[y * pitch + w * 4..][..64].fill(0xa5);
7970 }
7971 upload(&host);
7972 assert_eq!(
7973 hash(&mut enc).unwrap(),
7974 caret,
7975 "the alpha byte and the padding are not the picture"
7976 );
7977
7978 let (a, b) = (650 * pitch + 100 * 4, 650 * pitch + 101 * 4);
7979 assert_ne!(host[a..a + 3], host[b..b + 3]);
7980 for i in 0..4 {
7981 host.swap(a + i, b + i);
7982 }
7983 upload(&host);
7984 assert_eq!(
7985 changed(&caret, &hash(&mut enc).unwrap()),
7986 vec![650 / rows],
7987 "two pixels swapped"
7988 );
7989
7990 let mut time = |w: u32, h: u32| -> Vec<f64> {
7991 let mut ms: Vec<f64> = (0..300)
7992 .map(|_| {
7993 let t = std::time::Instant::now();
7994 enc.band_hashes(dev, pitch, w, h, rows as u32).unwrap();
7995 t.elapsed().as_secs_f64() * 1e3
7996 })
7997 .collect();
7998 ms.sort_by(f64::total_cmp);
7999 ms
8000 };
8001 let (hashing, fixed) = (time(w as u32, h as u32), time(64, rows as u32));
8002 let mut encoding: Vec<f64> = (0..120u64)
8003 .map(|i| {
8004 let t = std::time::Instant::now();
8005 enc.encode_cuda_pitch(dev, pitch, false, i, 25, i == 0)
8006 .expect("encode");
8007 t.elapsed().as_secs_f64() * 1e3
8008 })
8009 .collect();
8010 encoding.sort_by(f64::total_cmp);
8011 println!(
8012 "[band hash] 1080p: {:.3} ms median, {:.3} ms p99 a frame ({:.3} ms of it for a 64x{rows} \
8013 one), the first {jit:.1} ms with the module's load; the encode {:.3} ms median",
8014 hashing[150], hashing[297], fixed[150], encoding[60]
8015 );
8016 enc.release_external_input();
8017 unsafe {
8018 (cu.cuCtxPushCurrent_v2)(ctx);
8019 (cu.cuMemFree_v2)(dev);
8020 (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8021 }
8022 }
8023
8024 unsafe fn paint_surface(
8026 cuda: &CudaFunctions,
8027 dst: CUdeviceptr,
8028 dst_pitch: usize,
8029 w: usize,
8030 h: usize,
8031 rgb: [u8; 3],
8032 ) {
8033 let mut host = vec![255u8; w * h * 4];
8034 for px in host.as_chunks_mut::<4>().0 {
8035 px[..3].copy_from_slice(&[rgb[2], rgb[1], rgb[0]]);
8036 }
8037 let copy = CUDA_MEMCPY2D {
8038 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
8039 srcHost: host.as_ptr() as *const c_void,
8040 srcPitch: w * 4,
8041 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
8042 dstDevice: dst,
8043 dstPitch: dst_pitch,
8044 WidthInBytes: w * 4,
8045 Height: h,
8046 ..Default::default()
8047 };
8048 assert_eq!(
8049 (cuda.cuMemcpy2D_v2)(©),
8050 CUresult::CUDA_SUCCESS,
8051 "paint the surface"
8052 );
8053 }
8054
8055 fn color_pair(w: usize, h: usize, a: [f64; 3], b: [f64; 3], by_column: bool) -> Vec<u8> {
8058 let mut f = vec![255u8; w * h * 4];
8059 for y in 0..h {
8060 for x in 0..w {
8061 let p = if (if by_column { x } else { y }) % 2 == 0 {
8062 a
8063 } else {
8064 b
8065 };
8066 f[(y * w + x) * 4..][..3].copy_from_slice(&[p[2] as u8, p[1] as u8, p[0] as u8]);
8067 }
8068 }
8069 f
8070 }
8071
8072 fn encode_and_measure(enc: &mut NvencEncoder, bgra: &[u8]) -> (f64, f64) {
8074 use crate::webcam::decode::{Decoder as _, VideoDecoder};
8075 let w = enc.width() as usize;
8076 let pkt = enc
8077 .encode_cpu_packed(bgra, w * 4, false, 0, 20, true)
8078 .expect("packed encode");
8079 let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
8080 assert!(
8081 dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
8082 "no picture from this access unit"
8083 );
8084 let v = dec.frame().expect("decoded frame");
8085 let (mut su, mut sv) = (0.0f64, 0.0f64);
8086 let n = (v.chroma_height() * v.chroma_width()) as f64;
8087 for r in 0..v.chroma_height() {
8088 for c in 0..v.chroma_width() {
8089 let i = r * v.uv_stride + c;
8090 su += f64::from(v.u[i]);
8091 sv += f64::from(v.v[i]);
8092 }
8093 }
8094 (su / n, sv / n)
8095 }
8096
8097 #[test]
8100 #[ignore]
8101 fn gpu_bench_chroma_convert() {
8102 use crate::encoders::software::convert_to_yuv_mt;
8103 for (w, h) in [(1920usize, 1080usize), (3840, 2160)] {
8104 let st = settings(w as i32, h as i32, 60.0);
8105 let bgra = crate::encoders::chroma_siting::bgra(w, h);
8106 let mut kernel = host_session(&st).expect("NVENC init");
8107 assert!(kernel.csc.is_some());
8108 let with = bench_frames(&mut kernel, &bgra);
8109 let mut hardware = host_session(&st).expect("NVENC init");
8110 unsafe {
8111 let cu = hardware.cuda.clone();
8112 (cu.cuCtxPushCurrent_v2)(hardware.cuda_context);
8113 if let Some(csc) = hardware.csc.take() {
8114 csc.release(&cu, &hardware.nvenc_funcs, hardware.encoder_session);
8115 }
8116 (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8117 }
8118 let without = bench_frames(&mut hardware, &bgra);
8119 let (cw, ch) = (w / 2, h / 2);
8120 let (mut y, mut u, mut v) = (vec![0u8; w * h], vec![0u8; cw * ch], vec![0u8; cw * ch]);
8121 let mut host = f64::MAX;
8122 for _ in 0..20 {
8123 let t = std::time::Instant::now();
8124 convert_to_yuv_mt(
8125 &bgra,
8126 (w * 4) as u32,
8127 w,
8128 h,
8129 false,
8130 false,
8131 false,
8132 false,
8133 &mut y,
8134 &mut u,
8135 &mut v,
8136 (w, cw),
8137 8,
8138 )
8139 .expect("host convert");
8140 host = host.min(t.elapsed().as_secs_f64() * 1000.0);
8141 }
8142 println!(
8143 "[bench] {w}x{h}: encode {with:.2} ms with the convert, {without:.2} ms on NVENC's own; the host convert costs {host:.2} ms on 8 threads"
8144 );
8145 }
8146 }
8147
8148 fn bench_frames(enc: &mut NvencEncoder, bgra: &[u8]) -> f64 {
8150 let w = enc.width() as usize;
8151 for i in 0..10 {
8152 enc.encode_cpu_packed(bgra, w * 4, false, i, 25, i == 0)
8153 .expect("warm-up");
8154 }
8155 let t = std::time::Instant::now();
8156 for i in 0..60u64 {
8157 enc.encode_cpu_packed(bgra, w * 4, false, 10 + i, 25, false)
8158 .expect("encode");
8159 }
8160 t.elapsed().as_secs_f64() * 1000.0 / 60.0
8161 }
8162
8163 #[test]
8164 #[ignore]
8165 fn gpu_init_above_default_headroom() {
8166 let s = settings(2160, 4096, 30.0);
8167 let mut enc = host_session(&s).expect("NVENC init portrait 4K");
8168 assert_eq!(enc.init_params.maxEncodeWidth, 4096);
8169 assert_eq!(enc.init_params.maxEncodeHeight, 4096);
8170 let f = frame(2160, 4096, 20);
8171 let pkt = enc
8172 .encode_cpu_argb(&f, 2160 * 4, 0, 25, true)
8173 .expect("encode portrait 4K");
8174 assert_eq!(wire_dims(&pkt), (2160, 4096));
8175 }
8176
8177 fn thread_cpu() -> std::time::Duration {
8180 let mut ts = libc::timespec {
8181 tv_sec: 0,
8182 tv_nsec: 0,
8183 };
8184 unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) };
8185 std::time::Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
8186 }
8187
8188 fn per_frame(label: &str, n: usize, mut f: impl FnMut(usize)) -> (f64, f64) {
8190 let t0 = std::time::Instant::now();
8191 let c0 = thread_cpu();
8192 for i in 0..n {
8193 f(i);
8194 }
8195 let wall = t0.elapsed().as_secs_f64() * 1e6 / n as f64;
8196 let cpu = (thread_cpu() - c0).as_secs_f64() * 1e6 / n as f64;
8197 println!("{label}: {wall:.0} us wall/frame, {cpu:.0} us cpu/frame ({n} frames)");
8198 (wall, cpu)
8199 }
8200
8201 fn gpu_render() -> (
8205 gbm::Device<std::fs::File>,
8206 smithay::backend::renderer::gles::GlesRenderer,
8207 ) {
8208 let node = std::env::var("PIXELFLUX_TEST_RENDER_NODE")
8209 .ok()
8210 .or_else(|| crate::auto_select_render_node(Some("nvidia")))
8211 .expect("no NVIDIA render node");
8212 crate::gpu_render_init(std::path::Path::new(&node)).expect("GPU render init")
8213 }
8214
8215 const BG: [f32; 3] = [0.1, 0.2, 0.8];
8217 const FG: [f32; 3] = [0.9, 0.3, 0.1];
8218
8219 fn painted_ycbcr(rgb: [f32; 3]) -> [f64; 3] {
8221 use crate::encoders::chroma_siting::{BT709, ycbcr};
8222 ycbcr(rgb.map(|c| f64::from(c) * 255.0), BT709)
8223 }
8224
8225 fn block_rect(w: u32, h: u32, seed: u32) -> (i32, i32, i32, i32) {
8228 let x = ((seed * 37) % (w / 2)) as i32 & !1;
8229 let y = ((seed * 53) % (h / 2)) as i32 & !1;
8230 (x, y, (w / 4) as i32 & !1, (h / 4) as i32 & !1)
8231 }
8232
8233 fn painted_dmabuf(
8237 gbm: &gbm::Device<std::fs::File>,
8238 renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
8239 w: u32,
8240 h: u32,
8241 seed: u32,
8242 ) -> (gbm::BufferObject<()>, Dmabuf) {
8243 use gbm::{BufferObjectFlags, Format as GbmFormat};
8244 use smithay::backend::renderer::{Bind, Color32F, Frame, Renderer};
8245 use smithay::utils::{Physical, Rectangle, Size, Transform};
8246 let bo = gbm
8247 .create_buffer_object::<()>(w, h, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
8248 .expect("GBM buffer");
8249 let mut dmabuf = crate::create_dmabuf_from_bo(&bo);
8250 {
8251 let mut fb = renderer.bind(&mut dmabuf).expect("bind dmabuf");
8252 let size: Size<i32, Physical> = (w as i32, h as i32).into();
8253 let mut frame = renderer
8254 .render(&mut fb, size, Transform::Normal)
8255 .expect("render");
8256 let full: Rectangle<i32, Physical> = Rectangle::from_size(size);
8257 frame
8258 .clear(Color32F::new(BG[0], BG[1], BG[2], 1.0), &[full])
8259 .expect("clear");
8260 let (x, y, bw, bh) = block_rect(w, h, seed);
8261 let block: Rectangle<i32, Physical> = Rectangle::new((x, y).into(), (bw, bh).into());
8262 frame
8263 .draw_solid(
8264 block,
8265 &[Rectangle::from_size(block.size)],
8266 Color32F::new(FG[0], FG[1], FG[2], 1.0),
8267 )
8268 .expect("draw block");
8269 let sync = frame.finish().expect("finish");
8270 let _ = sync.wait();
8271 }
8272 (bo, dmabuf)
8273 }
8274
8275 fn decoded_means(
8278 dec: &mut crate::webcam::decode::VideoDecoder,
8279 pkt: &[u8],
8280 rect: (i32, i32, i32, i32),
8281 ) -> ([f64; 3], [f64; 3]) {
8282 use crate::webcam::decode::Decoder;
8283 assert!(
8284 dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
8285 "no picture from this access unit"
8286 );
8287 let v = dec.frame().expect("decoded frame");
8288 let (rx, ry, rw, rh) = rect;
8289 let inside = |x: usize, y: usize| {
8290 x as i32 >= rx && (x as i32) < rx + rw && y as i32 >= ry && (y as i32) < ry + rh
8291 };
8292 let mut acc = [[0f64; 3]; 2];
8293 let mut cnt = [0f64; 2];
8294 for y in 0..v.height {
8295 for x in 0..v.width {
8296 let k = if inside(x, y) { 0 } else { 1 };
8297 acc[k][0] += v.y[y * v.y_stride + x] as f64;
8298 acc[k][1] += v.u[(y / 2) * v.uv_stride + x / 2] as f64;
8299 acc[k][2] += v.v[(y / 2) * v.uv_stride + x / 2] as f64;
8300 cnt[k] += 1.0;
8301 }
8302 }
8303 let mean = |k: usize| [acc[k][0] / cnt[k], acc[k][1] / cnt[k], acc[k][2] / cnt[k]];
8304 (mean(0), mean(1))
8305 }
8306
8307 fn assert_painted(label: &str, block: [f64; 3], bg: [f64; 3], tol: f64) {
8310 let (eb, eg) = (painted_ycbcr(FG), painted_ycbcr(BG));
8311 for i in 0..3 {
8312 assert!(
8313 (block[i] - eb[i]).abs() <= tol,
8314 "{label}: block plane {i} = {:.1}, expected {:.1}",
8315 block[i],
8316 eb[i]
8317 );
8318 assert!(
8319 (bg[i] - eg[i]).abs() <= tol,
8320 "{label}: background plane {i} = {:.1}, expected {:.1}",
8321 bg[i],
8322 eg[i]
8323 );
8324 }
8325 }
8326
8327 fn all_direct(enc: &NvencEncoder) -> bool {
8329 !enc.dmabuf_cache.is_empty()
8330 && enc
8331 .dmabuf_cache
8332 .values()
8333 .all(|c| matches!(c.input, DmaBufInput::Direct { .. }))
8334 }
8335
8336 fn mapped_kind(enc: &NvencEncoder) -> &'static str {
8338 match enc
8339 .dmabuf_cache
8340 .values()
8341 .next()
8342 .map(|c| c.egl_frame.frame_type)
8343 {
8344 Some(CU_EGL_FRAME_TYPE_PITCH) => "pitch-linear",
8345 Some(CU_EGL_FRAME_TYPE_ARRAY) => "a CUDA array",
8346 _ => "an unknown frame kind",
8347 }
8348 }
8349
8350 #[test]
8357 #[ignore]
8358 fn gpu_dmabuf_direct_and_copy_paths_decode_to_the_paint() {
8359 use crate::webcam::decode::{Codec, VideoDecoder};
8360 let (w, h) = (1920u32, 1080u32);
8361 let s = settings(w as i32, h as i32, 60.0);
8362 let (gbm, mut renderer) = gpu_render();
8363 let egl_display = renderer.egl_context().display().get_display_handle().handle;
8364 let bufs: Vec<_> = (1..=2u32)
8365 .map(|seed| (seed, painted_dmabuf(&gbm, &mut renderer, w, h, seed)))
8366 .collect();
8367 let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
8368
8369 let run = |enc: &mut NvencEncoder, label: &str| -> Vec<Vec<u8>> {
8370 let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
8371 let mut out = Vec::new();
8372 for i in 0..6u64 {
8373 let (seed, (_, dmabuf)) = &bufs[(i % 2) as usize];
8374 let pkt = enc.encode(dmabuf, i, 25, i == 0).expect("dmabuf encode");
8375 assert_eq!(wire_dims(&pkt), (w as u16, h as u16));
8376 let (block, bg) = decoded_means(&mut dec, &pkt, block_rect(w, h, *seed));
8377 assert_painted(&format!("{label} frame {i}"), block, bg, 6.0);
8378 out.push(pkt[VIDEO_HEADER_LEN..].to_vec());
8379 }
8380 out
8381 };
8382
8383 if all_direct(&enc) {
8386 unsafe {
8387 let cu = enc.cuda.clone();
8388 (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
8389 paint_surface(
8390 &cu,
8391 enc.input_device_ptr,
8392 enc.input_pitch,
8393 w as usize,
8394 h as usize,
8395 [240, 16, 200],
8396 );
8397 (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8398 }
8399 }
8400 let direct = run(&mut enc, "direct");
8401 println!(
8402 "driver mapped the dmabuf {}: {}",
8403 mapped_kind(&enc),
8404 if all_direct(&enc) {
8405 "registered in place"
8406 } else {
8407 "direct registration unavailable, copy arm used"
8408 }
8409 );
8410
8411 let mut copy_only = NvencEncoder::new(&s, egl_display).expect("NVENC init");
8415 copy_only.direct_dmabuf = false;
8416 let copied = run(&mut copy_only, "copy");
8417 assert!(!all_direct(©_only));
8418 let identical = direct.iter().zip(&copied).all(|(a, b)| a == b);
8419 println!(
8420 "direct vs copy streams: {} ({} vs {} bytes)",
8421 if identical {
8422 "byte-identical"
8423 } else {
8424 "differ"
8425 },
8426 direct.iter().map(Vec::len).sum::<usize>(),
8427 copied.iter().map(Vec::len).sum::<usize>()
8428 );
8429 if let Ok(dir) = std::env::var("NVENC_TEST_DUMP_DIR") {
8430 std::fs::write(format!("{dir}/dmabuf-direct.h264"), direct.concat()).unwrap();
8431 std::fs::write(format!("{dir}/dmabuf-copy.h264"), copied.concat()).unwrap();
8432 }
8433 }
8434
8435 #[test]
8440 #[ignore]
8441 fn gpu_dmabuf_ten_bit_arms_agree() {
8442 let (w, h) = (1920u32, 1080u32);
8443 let (gbm, mut renderer) = gpu_render();
8444 let egl_display = renderer.egl_context().display().get_display_handle().handle;
8445 let bufs: Vec<_> = (1..=2u32)
8446 .map(|seed| painted_dmabuf(&gbm, &mut renderer, w, h, seed))
8447 .collect();
8448 for fullcolor in [false, true] {
8449 let mut s = settings(w as i32, h as i32, 60.0);
8450 s.codec = Codec::H265;
8451 s.video_fullcolor = fullcolor;
8452 s.video_bit_depth = 10;
8453 let mut streams = Vec::new();
8454 for direct in [true, false] {
8455 let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
8456 if enc.bit_depth() != 10 {
8457 println!("this device codes no 10-bit HEVC");
8458 return;
8459 }
8460 enc.direct_dmabuf = direct;
8461 let stream: Vec<u8> = (0..6u64)
8462 .flat_map(|i| {
8463 let (_, dmabuf) = &bufs[(i % 2) as usize];
8464 let pkt = enc.encode(dmabuf, i, 25, i == 0).expect("dmabuf encode");
8465 pkt[VIDEO_HEADER_LEN..].to_vec()
8466 })
8467 .collect();
8468 println!(
8469 "4:4:4 {fullcolor} direct {direct}: mapped {}, {} bytes",
8470 mapped_kind(&enc),
8471 stream.len()
8472 );
8473 if let Ok(dir) = std::env::var("NVENC_TEST_DUMP_DIR") {
8474 std::fs::write(
8475 format!("{dir}/dmabuf-10bit-{fullcolor}-{direct}.h265"),
8476 &stream,
8477 )
8478 .unwrap();
8479 }
8480 streams.push(stream);
8481 }
8482 assert_eq!(streams[0], streams[1], "4:4:4 {fullcolor}: the arms differ");
8483 }
8484 }
8485
8486 #[test]
8491 #[ignore]
8492 fn gpu_packed_bgra_and_rgba_inputs_agree() {
8493 use crate::webcam::decode::{Codec, VideoDecoder};
8494 let (w, h) = (1280u32, 720u32);
8495 let s = settings(w as i32, h as i32, 60.0);
8496 let rect = block_rect(w, h, 3);
8497 let paint = |rgba: bool| -> Vec<u8> {
8498 let to_u8 = |c: f32| (c * 255.0).round() as u8;
8499 let mut f = vec![0u8; (w * h * 4) as usize];
8500 for y in 0..h as i32 {
8501 for x in 0..w as i32 {
8502 let inside =
8503 x >= rect.0 && x < rect.0 + rect.2 && y >= rect.1 && y < rect.1 + rect.3;
8504 let c = if inside { FG } else { BG };
8505 let px = &mut f[((y as u32 * w + x as u32) * 4) as usize..][..4];
8506 let (r, g, b) = (to_u8(c[0]), to_u8(c[1]), to_u8(c[2]));
8507 if rgba {
8508 px.copy_from_slice(&[r, g, b, 255]);
8509 } else {
8510 px.copy_from_slice(&[b, g, r, 255]);
8511 }
8512 }
8513 }
8514 f
8515 };
8516 let bgra = paint(false);
8517 let rgba = paint(true);
8518 let mut enc = host_session(&s).expect("NVENC init");
8519 let mut dec = VideoDecoder::new(Codec::H264).expect("H.264 decoder");
8520 let stride = (w * 4) as usize;
8521 let pass = |enc: &mut NvencEncoder, dec: &mut VideoDecoder, tag: &str| {
8525 for (i, (buf, is_rgba)) in
8526 [(&bgra, false), (&rgba, true), (&bgra, false), (&rgba, true)]
8527 .into_iter()
8528 .enumerate()
8529 {
8530 let pkt = enc
8531 .encode_cpu_packed(buf, stride, is_rgba, i as u64, 25, i == 0)
8532 .expect("packed encode");
8533 if enc.csc.is_none() {
8534 assert_eq!(
8535 enc.input_format,
8536 if is_rgba {
8537 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
8538 } else {
8539 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
8540 }
8541 );
8542 }
8543 let (block, bg) = decoded_means(dec, &pkt, rect);
8544 assert_painted(&format!("{tag} frame {i} rgba={is_rgba}"), block, bg, 6.0);
8545 }
8546 };
8547 pass(&mut enc, &mut dec, "convert");
8548 unsafe {
8549 let cu = enc.cuda.clone();
8550 (cu.cuCtxPushCurrent_v2)(enc.cuda_context);
8551 if let Some(csc) = enc.csc.take() {
8552 csc.release(&cu, &enc.nvenc_funcs, enc.encoder_session);
8553 }
8554 (cu.cuCtxPopCurrent_v2)(ptr::null_mut());
8555 }
8556 pass(&mut enc, &mut dec, "hardware");
8557 }
8558
8559 #[test]
8562 #[ignore]
8563 fn gpu_bench_tuning() {
8564 let (w, h) = (1920u32, 1080u32);
8565 let n: usize = std::env::var("NVENC_BENCH_FRAMES")
8566 .ok()
8567 .and_then(|v| v.parse().ok())
8568 .unwrap_or(240);
8569 let mut s = settings(w as i32, h as i32, 60.0);
8570 s.video_cbr_mode = true;
8571 s.video_bitrate_kbps = 8000;
8572 let frames: Vec<Vec<u8>> = (0..8u8)
8573 .map(|k| frame(w as usize, h as usize, 10 + 30 * k))
8574 .collect();
8575 let stride = (w * 4) as usize;
8576 let presets = [
8577 ("P1", NV_ENC_PRESET_P1_GUID),
8578 ("P2", NV_ENC_PRESET_P2_GUID),
8579 ("P3", NV_ENC_PRESET_P3_GUID),
8580 ("P4", NV_ENC_PRESET_P4_GUID),
8581 ("P5", NV_ENC_PRESET_P5_GUID),
8582 ("P6", NV_ENC_PRESET_P6_GUID),
8583 ("P7", NV_ENC_PRESET_P7_GUID),
8584 ];
8585 let passes = [
8586 ("single pass", NV_ENC_MULTI_PASS::NV_ENC_MULTI_PASS_DISABLED),
8587 (
8588 "two-pass quarter",
8589 NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION,
8590 ),
8591 (
8592 "two-pass full",
8593 NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_FULL_RESOLUTION,
8594 ),
8595 ];
8596 let mut cases: Vec<(String, NvencTuning)> = Vec::new();
8597 for (name, preset) in presets {
8598 cases.push((
8599 format!("{name} two-pass quarter"),
8600 NvencTuning {
8601 preset,
8602 ..NvencTuning::default()
8603 },
8604 ));
8605 }
8606 for (name, multipass) in passes {
8607 cases.push((
8608 format!("P4 {name}"),
8609 NvencTuning {
8610 multipass,
8611 ..NvencTuning::default()
8612 },
8613 ));
8614 }
8615 cases.push((
8616 "P4 two-pass quarter spatial AQ".into(),
8617 NvencTuning {
8618 spatial_aq: true,
8619 ..NvencTuning::default()
8620 },
8621 ));
8622 cases.push((
8623 "P4 two-pass quarter temporal AQ".into(),
8624 NvencTuning {
8625 temporal_aq: true,
8626 ..NvencTuning::default()
8627 },
8628 ));
8629 for codec in [Codec::H264, Codec::H265] {
8630 s.codec = codec;
8631 for (label, tuning) in &cases {
8632 let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), *tuning) {
8633 Ok(enc) => enc,
8634 Err(e) => {
8635 println!("{codec:?} {label}: {e}");
8636 continue;
8637 }
8638 };
8639 enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true)
8640 .expect("warm-up");
8641 let mut bytes = 0usize;
8642 per_frame(&format!("{codec:?} {label}"), n, |i| {
8643 bytes += enc
8644 .encode_cpu_packed(&frames[i % 8], stride, false, 1 + i as u64, 25, false)
8645 .expect("encode")
8646 .len();
8647 });
8648 println!(" {} kbps", bytes * 8 * 60 / n / 1000);
8649 }
8650 }
8651 }
8652
8653 #[test]
8661 #[ignore]
8662 fn gpu_bench_tuning_on_content() {
8663 use crate::webcam::decode::VideoDecoder;
8664 let n: usize = std::env::var("NVENC_BENCH_FRAMES")
8665 .ok()
8666 .and_then(|v| v.parse().ok())
8667 .unwrap_or(90);
8668 let glyph = |c: u32, gx: usize, gy: usize| {
8669 let s = c.wrapping_mul(2654435761) >> 7;
8670 (s & 1 == 1 && gx == 1)
8671 || (s & 2 == 2 && gx == 5)
8672 || (s & 4 == 4 && gy == 1)
8673 || (s & 8 == 8 && gy == 6)
8674 || (s & 16 == 16 && gy == 11)
8675 || (s & 32 == 32 && gx * 2 == gy)
8676 };
8677 let only = |var: &str, item: String| {
8678 std::env::var(var).map_or(true, |v| v.split(',').any(|x| x == item))
8679 };
8680 use NV_ENC_SPLIT_ENCODE_MODE as Split;
8681 for (w, h, kbps) in [
8682 (1920usize, 1080usize, 8000),
8683 (2560, 1440, 14000),
8684 (3840, 2160, 30000),
8685 ] {
8686 if !only("NVENC_BENCH_HEIGHTS", h.to_string()) {
8687 continue;
8688 }
8689 let mut page = vec![255u8; w * 2 * h * 4];
8690 for y in 0..2 * h {
8691 let (cy, gy) = (y / 18, y % 18);
8692 if cy % 9 == 8 || gy >= 13 {
8693 continue;
8694 }
8695 for x in 0..w {
8696 let (cx, gx) = (x / 10, x % 10);
8697 let c = (cx as u32).wrapping_mul(31).wrapping_add(cy as u32 * 977);
8698 if gx < 7 && (cx + cy * 3) % 13 != 0 && glyph(c, gx, gy) {
8699 page[(y * w + x) * 4..(y * w + x) * 4 + 3].copy_from_slice(&[40, 30, 20]);
8700 }
8701 }
8702 }
8703 let (tw, th) = (w * 2, h * 2);
8704 let mut tex = vec![0u8; tw * th * 4];
8705 for y in 0..th {
8706 for x in 0..tw {
8707 let (fx, fy) = (x as f32, y as f32);
8708 let v = 128.0
8709 + 50.0 * (fx / 97.0).sin() * (fy / 61.0).cos()
8710 + 30.0 * (fx / 13.0 + fy / 29.0).sin()
8711 + ((x as u32).wrapping_mul(2654435761) ^ (y as u32).wrapping_mul(40503))
8712 as f32
8713 / u32::MAX as f32
8714 * 24.0;
8715 let i = (y * tw + x) * 4;
8716 tex[i] = v as u8;
8717 tex[i + 1] = (v * 0.8) as u8;
8718 tex[i + 2] = (255.0 - v) as u8;
8719 }
8720 }
8721 let mut bufs = [vec![0u8; w * h * 4], vec![0u8; w * h * 4]];
8722 let draw = |scene: usize, t: usize, out: &mut Vec<u8>| {
8723 for y in 0..h {
8724 let row = &mut out[y * w * 4..(y + 1) * w * 4];
8725 if scene == 0 {
8726 let src = (y + t * 4) % (2 * h);
8727 row.copy_from_slice(&page[src * w * 4..(src + 1) * w * 4]);
8728 } else {
8729 let (ox, oy) = ((t * 6) % w, (y + t * 3) % h);
8730 row.copy_from_slice(&tex[(oy * tw + ox) * 4..(oy * tw + ox + w) * 4]);
8731 }
8732 }
8733 };
8734 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
8735 if !only("NVENC_BENCH_CODECS", format!("{codec:?}")) {
8736 continue;
8737 }
8738 let mut rows: Vec<(String, NvencTuning)> = vec![
8739 ("production".into(), NvencTuning::default()),
8740 (
8741 "P1".into(),
8742 NvencTuning {
8743 preset: NV_ENC_PRESET_P1_GUID,
8744 ..NvencTuning::default()
8745 },
8746 ),
8747 (
8748 "P2".into(),
8749 NvencTuning {
8750 preset: NV_ENC_PRESET_P2_GUID,
8751 ..NvencTuning::default()
8752 },
8753 ),
8754 (
8755 "P3 L0=1".into(),
8756 NvencTuning {
8757 ref_l0: Some(NV_ENC_NUM_REF_FRAMES::NV_ENC_NUM_REF_FRAMES_1),
8758 ..NvencTuning::default()
8759 },
8760 ),
8761 ];
8762 if codec != Codec::H264 {
8763 for (name, split) in [
8764 ("split off", Split::NV_ENC_SPLIT_DISABLE_MODE),
8765 ("split driver's", Split::NV_ENC_SPLIT_AUTO_MODE),
8766 ("split forced", Split::NV_ENC_SPLIT_AUTO_FORCED_MODE),
8767 ("split two", Split::NV_ENC_SPLIT_TWO_FORCED_MODE),
8768 ("split three", Split::NV_ENC_SPLIT_THREE_FORCED_MODE),
8769 ] {
8770 rows.push((
8771 format!("P3 {name}"),
8772 NvencTuning {
8773 split: Some(split),
8774 ..NvencTuning::default()
8775 },
8776 ));
8777 }
8778 }
8779 for (scene, scene_name) in [(0usize, "text"), (1, "pan")] {
8780 for (label, tuning) in &rows {
8781 let mut s = settings(w as i32, h as i32, 60.0);
8782 s.codec = codec;
8783 s.video_cbr_mode = true;
8784 s.video_bitrate_kbps = kbps;
8785 let mut enc = match NvencEncoder::new_tuned(&s, ptr::null(), *tuning) {
8786 Ok(enc) => enc,
8787 Err(e) => {
8788 println!("{codec:?} {w}x{h} {label}: {e}");
8789 continue;
8790 }
8791 };
8792 let engines = unsafe {
8793 query_cap(
8794 &enc.nvenc_funcs,
8795 enc.encoder_session,
8796 codec_guid(codec).unwrap(),
8797 NV_ENC_CAPS::NV_ENC_CAPS_NUM_ENCODER_ENGINES,
8798 )
8799 };
8800 let mut dec = VideoDecoder::new(codec).unwrap();
8801 let (mut times, mut bytes, mut psnr) =
8802 (Vec::with_capacity(n), 0usize, 0f64);
8803 for t in 0..n + 5 {
8804 let k = t % 2;
8805 draw(scene, t, &mut bufs[k]);
8806 let t0 = std::time::Instant::now();
8807 let out = enc
8808 .encode_cpu_packed(&bufs[k], w * 4, false, t as u64, 25, t == 0)
8809 .expect("encode");
8810 let ms = t0.elapsed().as_secs_f64() * 1e3;
8811 let p = luma_psnr(&mut dec, &out, &bufs[k], w, h);
8812 if t >= 5 {
8813 times.push(ms);
8814 bytes += out.len();
8815 psnr += p;
8816 }
8817 }
8818 times.sort_by(|a, b| a.total_cmp(b));
8819 println!(
8820 "TUNING {codec:?} {w}x{h} {scene_name} {label} (engines {engines:?}): encode {:.2} ms p50 {:.2} p99, {:.2} Mbit/s against {:.1}, PSNR {:.2} dB",
8821 times[n / 2],
8822 times[(n * 99) / 100],
8823 bytes as f64 * 8.0 * 60.0 / n as f64 / 1e6,
8824 kbps as f64 / 1000.0,
8825 psnr / n as f64
8826 );
8827 }
8828 }
8829 }
8830 }
8831 }
8832
8833 #[test]
8837 #[ignore]
8838 fn gpu_bench_dmabuf_paths() {
8839 let (w, h) = (1920u32, 1080u32);
8840 let n: usize = std::env::var("NVENC_BENCH_FRAMES")
8841 .ok()
8842 .and_then(|v| v.parse().ok())
8843 .unwrap_or(300);
8844 let s = settings(w as i32, h as i32, 60.0);
8845 let (gbm, mut renderer) = gpu_render();
8846 let egl_display = renderer.egl_context().display().get_display_handle().handle;
8847 let bufs: Vec<_> = (1..=2u32)
8848 .map(|seed| painted_dmabuf(&gbm, &mut renderer, w, h, seed).1)
8849 .collect();
8850 let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
8851 for pass in 0..2 {
8852 enc.direct_dmabuf = pass == 0;
8853 enc.reconfigure_resolution(&s)
8854 .expect("reconfigure drains the import cache");
8855 enc.encode(&bufs[0], 0, 25, true).expect("warm-up");
8856 enc.encode(&bufs[1], 1, 25, false).expect("warm-up");
8857 let label = if all_direct(&enc) {
8858 format!("dmabuf registered in place ({})", mapped_kind(&enc))
8859 } else if pass == 0 {
8860 format!(
8861 "dmabuf copy arm (direct registration unavailable, mapped {})",
8862 mapped_kind(&enc)
8863 )
8864 } else {
8865 format!("dmabuf per-frame copy (mapped {})", mapped_kind(&enc))
8866 };
8867 per_frame(&label, n, |i| {
8868 enc.encode(&bufs[i % 2], 2 + i as u64, 25, false)
8869 .expect("encode");
8870 });
8871 }
8872 }
8873
8874 #[test]
8878 #[ignore]
8879 fn gpu_bench_readback_upload() {
8880 let (w, h) = (1920u32, 1080u32);
8881 let n: usize = std::env::var("NVENC_BENCH_FRAMES")
8882 .ok()
8883 .and_then(|v| v.parse().ok())
8884 .unwrap_or(300);
8885 let s = settings(w as i32, h as i32, 60.0);
8886 let frames: Vec<Vec<u8>> = (0..4u8)
8887 .map(|k| frame(w as usize, h as usize, 10 + 40 * k))
8888 .collect();
8889 let stride = (w * 4) as usize;
8890 let mut enc = host_session(&s).expect("NVENC init");
8891
8892 enc.reconfigure_resolution(&s).expect("reconfigure");
8893 enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true)
8894 .expect("warm-up");
8895 per_frame(
8896 "encode_cpu_packed BGRA (pinned, async upload, hardware CSC)",
8897 n,
8898 |i| {
8899 enc.encode_cpu_packed(&frames[i % 4], stride, false, 1 + i as u64, 25, false)
8900 .expect("packed");
8901 },
8902 );
8903
8904 enc.reconfigure_resolution(&s).expect("reconfigure");
8905 enc.encode_cpu_packed(&frames[0], stride, true, 0, 25, true)
8906 .expect("warm-up");
8907 per_frame(
8908 "encode_cpu_packed RGBA (pinned, async upload, hardware CSC)",
8909 n,
8910 |i| {
8911 enc.encode_cpu_packed(&frames[i % 4], stride, true, 1 + i as u64, 25, false)
8912 .expect("packed");
8913 },
8914 );
8915
8916 enc.reconfigure_resolution(&s).expect("reconfigure");
8917 enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true)
8918 .expect("warm-up");
8919 per_frame(
8920 "packed BGRA with a synchronous cuMemcpy2D upload",
8921 n,
8922 |i| unsafe {
8923 let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
8924 let src = &frames[i % 4];
8925 enc.pin_host_source(src.as_ptr() as usize, src.len());
8926 let copy = CUDA_MEMCPY2D {
8927 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
8928 srcHost: src.as_ptr() as *const c_void,
8929 srcPitch: stride,
8930 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
8931 dstDevice: enc.input_device_ptr,
8932 dstPitch: enc.input_pitch,
8933 WidthInBytes: stride,
8934 Height: h as usize,
8935 ..Default::default()
8936 };
8937 assert_eq!((enc.cuda.cuMemcpy2D_v2)(©), CUresult::CUDA_SUCCESS);
8938 enc.submit_frame(
8939 enc.mapped_input_buffer,
8940 enc.input_format,
8941 1 + i as u64,
8942 false,
8943 )
8944 .expect("submit");
8945 (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
8946 },
8947 );
8948 }
8949
8950 #[derive(Clone, Copy, Debug)]
8953 enum LockWait {
8954 Block,
8955 Spin,
8956 Sleep(std::time::Duration),
8957 }
8958
8959 #[test]
8965 #[ignore]
8966 fn gpu_bench_lock_strategies() {
8967 let s = settings(1920, 1080, 60.0);
8968 let mut enc = host_session(&s).expect("NVENC init");
8969 let frames = [frame(1920, 1080, 10), frame(1920, 1080, 90)];
8970 let n = 240usize;
8971 let strategies = [
8972 LockWait::Spin,
8973 LockWait::Block,
8974 LockWait::Sleep(std::time::Duration::from_micros(50)),
8975 LockWait::Sleep(std::time::Duration::from_micros(200)),
8976 LockWait::Sleep(std::time::Duration::from_micros(1000)),
8977 ];
8978 for pass in 0..2 {
8979 for &strategy in &strategies {
8980 let mut waits: Vec<f64> = Vec::with_capacity(n);
8981 let mut busy_total = 0u64;
8982 let c0 = thread_cpu();
8983 for i in 0..n {
8984 let (wait, busy) =
8985 unsafe { lock_round_trip(&mut enc, &frames[i % 2], i as u64, strategy) };
8986 waits.push(wait);
8987 busy_total += busy;
8988 }
8989 let cpu_us = (thread_cpu() - c0).as_secs_f64() * 1e6 / n as f64;
8990 waits.sort_by(|a, b| a.partial_cmp(b).unwrap());
8991 let mean = waits.iter().sum::<f64>() / n as f64;
8992 let pct = |q: f64| waits[((n as f64 - 1.0) * q) as usize];
8993 let empty = EMPTY_SUCCESSES.swap(0, std::sync::atomic::Ordering::Relaxed);
8994 if pass == 1 {
8995 println!(
8996 "{strategy:?}: submit->locked mean {mean:.0} us p50 {:.0} p95 {:.0} p99 {:.0} max {:.0}; {cpu_us:.0} us cpu/frame; {:.1} not-ready answers/frame; {:.2} empty successes/frame",
8997 pct(0.5),
8998 pct(0.95),
8999 pct(0.99),
9000 waits[n - 1],
9001 busy_total as f64 / n as f64,
9002 empty as f64 / n as f64
9003 );
9004 }
9005 }
9006 }
9007 }
9008
9009 static EMPTY_SUCCESSES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
9011
9012 unsafe fn lock_round_trip(
9015 enc: &mut NvencEncoder,
9016 pixels: &[u8],
9017 frame_number: u64,
9018 strategy: LockWait,
9019 ) -> (f64, u64) {
9020 let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
9021 let copy = CUDA_MEMCPY2D {
9022 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
9023 srcHost: pixels.as_ptr() as *const c_void,
9024 srcPitch: (enc.width * 4) as usize,
9025 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
9026 dstDevice: enc.input_device_ptr,
9027 dstPitch: enc.input_pitch,
9028 WidthInBytes: (enc.width * 4) as usize,
9029 Height: enc.height as usize,
9030 ..Default::default()
9031 };
9032 assert_eq!((enc.cuda.cuMemcpy2D_v2)(©), CUresult::CUDA_SUCCESS);
9033 let output_bitstream = enc.bitstream_buffers[enc.current_buffer_idx];
9034 enc.current_buffer_idx = (enc.current_buffer_idx + 1) % enc.bitstream_buffers.len();
9035 let mut pic_params = NV_ENC_PIC_PARAMS {
9036 version: sv(NvStruct::PicParams),
9037 inputWidth: enc.width,
9038 inputHeight: enc.height,
9039 inputBuffer: enc.mapped_input_buffer,
9040 outputBitstream: output_bitstream,
9041 bufferFmt: enc.input_format,
9042 pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
9043 encodePicFlags: if frame_number == 0 {
9044 NV_ENC_PIC_FLAGS::NV_ENC_PIC_FLAG_FORCEIDR as u32
9045 } else {
9046 0
9047 },
9048 ..Default::default()
9049 };
9050 let t0 = std::time::Instant::now();
9051 let st =
9052 (enc.nvenc_funcs.nvEncEncodePicture.unwrap())(enc.encoder_session, &mut pic_params);
9053 assert_eq!(st, NVENCSTATUS::NV_ENC_SUCCESS);
9054 let mut negotiated = Negotiated::new(NV_ENC_LOCK_BITSTREAM {
9055 version: sv(NvStruct::LockBitstream),
9056 outputBitstream: output_bitstream,
9057 ..Default::default()
9058 });
9059 let lock_params = &mut negotiated.value;
9060 lock_params.set_doNotWait(
9061 matches!(strategy, LockWait::Block)
9062 .then_some(0)
9063 .unwrap_or(1),
9064 );
9065 let lock_fn = enc.nvenc_funcs.nvEncLockBitstream.unwrap();
9066 let mut busy = 0u64;
9067 let mut empty_successes = 0u64;
9068 loop {
9069 match lock_fn(enc.encoder_session, lock_params) {
9070 NVENCSTATUS::NV_ENC_SUCCESS if lock_params.bitstreamSizeInBytes > 0 => break,
9071 NVENCSTATUS::NV_ENC_SUCCESS => {
9072 empty_successes += 1;
9073 (enc.nvenc_funcs.nvEncUnlockBitstream.unwrap())(
9074 enc.encoder_session,
9075 output_bitstream,
9076 );
9077 if let LockWait::Sleep(d) = strategy {
9078 std::thread::sleep(d);
9079 }
9080 if matches!(strategy, LockWait::Block) {
9081 panic!("blocking lock returned an empty bitstream");
9082 }
9083 }
9084 NVENCSTATUS::NV_ENC_ERR_LOCK_BUSY => {
9085 busy += 1;
9086 if let LockWait::Sleep(d) = strategy {
9087 std::thread::sleep(d);
9088 }
9089 }
9090 other => panic!("lock failed: {other:?}"),
9091 }
9092 }
9093 let wait = t0.elapsed().as_secs_f64() * 1e6;
9094 if empty_successes > 0 {
9095 EMPTY_SUCCESSES.fetch_add(empty_successes, std::sync::atomic::Ordering::Relaxed);
9096 }
9097 (enc.nvenc_funcs.nvEncUnlockBitstream.unwrap())(enc.encoder_session, output_bitstream);
9098 (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
9099 (wait, busy)
9100 }
9101}
9102
9103#[cfg(test)]
9104mod version_tests {
9105 use super::*;
9106
9107 const ALL: [(NvStruct, u32); 13] = [
9110 (NvStruct::FunctionList, NV_ENCODE_API_FUNCTION_LIST_VER),
9111 (
9112 NvStruct::OpenSessionExParams,
9113 NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER,
9114 ),
9115 (NvStruct::Config, NV_ENC_CONFIG_VER),
9116 (NvStruct::RcParams, NV_ENC_RC_PARAMS_VER),
9117 (NvStruct::PresetConfig, NV_ENC_PRESET_CONFIG_VER),
9118 (NvStruct::InitializeParams, NV_ENC_INITIALIZE_PARAMS_VER),
9119 (NvStruct::ReconfigureParams, NV_ENC_RECONFIGURE_PARAMS_VER),
9120 (NvStruct::RegisterResource, NV_ENC_REGISTER_RESOURCE_VER),
9121 (NvStruct::MapInputResource, NV_ENC_MAP_INPUT_RESOURCE_VER),
9122 (
9123 NvStruct::CreateBitstreamBuffer,
9124 NV_ENC_CREATE_BITSTREAM_BUFFER_VER,
9125 ),
9126 (NvStruct::PicParams, NV_ENC_PIC_PARAMS_VER),
9127 (NvStruct::LockBitstream, NV_ENC_LOCK_BITSTREAM_VER),
9128 (NvStruct::CapsParam, NV_ENC_CAPS_PARAM_VER),
9129 ];
9130
9131 #[test]
9136 fn table_is_identity_for_pinned_version() {
9137 let maj = NVENCAPI_VERSION & 0xFF;
9138 let min = (NVENCAPI_VERSION >> 24) & 0xFF;
9139 for (s, base) in ALL {
9140 assert_eq!(nvenc_struct_ver(s, maj, min), base, "{:?}", s);
9141 }
9142 assert_eq!(maj | (min << 24), NVENCAPI_VERSION);
9143 }
9144
9145 #[test]
9148 fn reconfigure_flags_sit_where_each_version_reads_them() {
9149 for (maj, min, at) in [
9150 (10, 0, 1816),
9151 (11, 0, 1816),
9152 (11, 1, 1816),
9153 (12, 0, 1816),
9154 (12, 1, 1816),
9155 (12, 2, 1808),
9156 (13, 0, 1808),
9157 ] {
9158 let params =
9159 reconfigure_params(NV_ENC_INITIALIZE_PARAMS::default(), (maj, min), true, true);
9160 let bytes = unsafe {
9161 std::slice::from_raw_parts(
9162 (¶ms as *const Negotiated<_>).cast::<u8>(),
9163 std::mem::size_of_val(¶ms),
9164 )
9165 };
9166 let word = |at: usize| u32::from_ne_bytes(bytes[at..at + 4].try_into().unwrap());
9167 assert_eq!(
9168 (word(1808), word(1816)),
9169 if at == 1816 { (0, 3) } else { (3, 0) },
9170 "{maj}.{min}"
9171 );
9172 }
9173 }
9174
9175 #[test]
9179 fn negotiated_structs_hold_the_longest_layout() {
9180 assert_eq!(
9181 std::mem::size_of::<Negotiated<NV_ENC_INITIALIZE_PARAMS>>(),
9182 1808
9183 );
9184 assert_eq!(
9185 std::mem::size_of::<Negotiated<NV_ENC_RECONFIGURE_PARAMS>>(),
9186 1824
9187 );
9188 assert_eq!(
9189 std::mem::size_of::<Negotiated<NV_ENC_LOCK_BITSTREAM>>(),
9190 1552
9191 );
9192 }
9193
9194 #[test]
9202 fn table_matches_historical_headers() {
9203 #[rustfmt::skip]
9204 let expected: [(u32, u32, [u32; 12]); 7] = [
9205 (10, 0, [0x7002000A, 0x7001000A, 0xF007000A, 0x7001000A, 0xF004000A, 0xF005000A, 0xF001000A,
9206 0x7003000A, 0x7004000A, 0x7001000A, 0xF004000A, 0x7001000A]),
9207 (11, 0, [0x7002000B, 0x7001000B, 0xF007000B, 0x7001000B, 0xF004000B, 0xF005000B, 0xF001000B,
9208 0x7003000B, 0x7004000B, 0x7001000B, 0xF004000B, 0x7001000B]),
9209 (11, 1, [0x7102000B, 0x7101000B, 0xF107000B, 0x7101000B, 0xF104000B, 0xF105000B, 0xF101000B,
9210 0x7103000B, 0x7104000B, 0x7101000B, 0xF104000B, 0x7101000B]),
9211 (12, 0, [0x7002000C, 0x7001000C, 0xF008000C, 0x7001000C, 0xF004000C, 0xF005000C, 0xF001000C,
9212 0x7004000C, 0x7004000C, 0x7001000C, 0xF006000C, 0x7002000C]),
9213 (12, 1, [0x7102000C, 0x7101000C, 0xF108000C, 0x7101000C, 0xF104000C, 0xF106000C, 0xF101000C,
9214 0x7104000C, 0x7104000C, 0x7101000C, 0xF106000C, 0xF101000C]),
9215 (12, 2, [0x7202000C, 0x7201000C, 0xF209000C, 0x7201000C, 0xF205000C, 0xF207000C, 0xF202000C,
9216 0x7205000C, 0x7204000C, 0x7201000C, 0xF207000C, 0xF202000C]),
9217 (13, 0, [0x7002000D, 0x7001000D, 0xF009000D, 0x7001000D, 0xF005000D, 0xF007000D, 0xF002000D,
9218 0x7005000D, 0x7004000D, 0x7001000D, 0xF007000D, 0xF002000D]),
9219 ];
9220 for (maj, min, words) in expected {
9221 for ((s, _), want) in ALL.iter().zip(words) {
9222 assert_eq!(
9223 nvenc_struct_ver(*s, maj, min),
9224 want,
9225 "{:?} at {}.{}",
9226 s,
9227 maj,
9228 min
9229 );
9230 }
9231 }
9232 }
9233}
9234
9235#[cfg(test)]
9236mod decision_tests {
9237 use super::*;
9238
9239 #[test]
9243 fn caps_chroma_downgrade() {
9244 let d = decide_caps(true, 1920, 1080, Some(0), None, None);
9245 assert!(!d.fullcolor && d.downgraded_color && d.too_large.is_none());
9246
9247 let d = decide_caps(true, 1920, 1080, Some(1), None, None);
9248 assert!(d.fullcolor && !d.downgraded_color);
9249
9250 let d = decide_caps(true, 1920, 1080, None, None, None);
9251 assert!(d.fullcolor && !d.downgraded_color);
9252
9253 let d = decide_caps(false, 1920, 1080, Some(0), None, None);
9255 assert!(!d.fullcolor && !d.downgraded_color);
9256 }
9257
9258 #[test]
9262 fn caps_dimension_gate() {
9263 assert_eq!(
9264 decide_caps(false, 5120, 2160, None, Some(4096), Some(4096)).too_large,
9265 Some((4096, 4096))
9266 );
9267 assert_eq!(
9268 decide_caps(false, 3840, 4320, None, Some(4096), Some(4096)).too_large,
9269 Some((4096, 4096))
9270 );
9271 assert!(
9272 decide_caps(false, 3840, 2160, None, Some(4096), Some(4096))
9273 .too_large
9274 .is_none()
9275 );
9276 assert!(
9277 decide_caps(false, 7680, 4320, None, None, None)
9278 .too_large
9279 .is_none()
9280 );
9281 assert!(
9283 decide_caps(false, 3840, 2160, None, Some(0), Some(0))
9284 .too_large
9285 .is_none()
9286 );
9287 }
9288
9289 #[test]
9292 fn headroom_is_floored_and_capped() {
9293 assert_eq!(nvenc_headroom(1920, 4096, Some(8192)), 4096);
9294 assert_eq!(nvenc_headroom(3840, 4096, Some(4096)), 4096);
9295 assert_eq!(nvenc_headroom(6000, 4096, Some(8192)), 6000);
9296 assert_eq!(nvenc_headroom(1920, 4096, None), 4096);
9297 assert_eq!(nvenc_headroom(1920, 4096, Some(2048)), 2048);
9298 }
9299
9300 #[test]
9305 fn nvenc_level_follows_geometry_except_av1() {
9306 assert_eq!(nvenc_level(Codec::H264, 1920, 1080, 60, 0, true), 42);
9309 assert_eq!(nvenc_level(Codec::H265, 1920, 1080, 60, 0, true), 123);
9310 assert_eq!(nvenc_level(Codec::H264, 3840, 2160, 60, 0, true), 52);
9312 assert_eq!(nvenc_level(Codec::H265, 3840, 2160, 60, 0, true), 153);
9313 assert_eq!(h265_level(1280, 720, 144, 0, true), 123);
9315 assert_eq!(nvenc_level(Codec::H265, 1280, 720, 144, 0, true), 150);
9316 for (w, h) in [(1280u32, 720u32), (1920, 1080), (3840, 2160)] {
9318 let macroblocks = (w as u64 / 16) * (h as u64 / 16);
9319 assert!(
9320 h264_max_macroblocks(nvenc_level(Codec::H264, w, h, 60, 0, true)) >= macroblocks,
9321 "H.264 level at {w}x{h} cannot hold the picture"
9322 );
9323 assert!(
9324 h265_max_picture(nvenc_level(Codec::H265, w, h, 60, 0, true))
9325 >= (w as u64) * (h as u64),
9326 "HEVC level at {w}x{h} cannot hold the picture"
9327 );
9328 }
9329 let pixels = (HEADROOM_WIDTH * HEADROOM_HEIGHT) as u64;
9332 for (w, h) in [(1280, 720), (1920, 1080), (3840, 2160)] {
9333 assert_eq!(
9334 nvenc_level(Codec::Av1, w, h, 60, 0, true),
9335 nvenc_level(Codec::Av1, HEADROOM_WIDTH, HEADROOM_HEIGHT, 60, 0, true),
9336 "AV1 level moved with the capture at {w}x{h}"
9337 );
9338 assert!(
9339 av1_max_picture(nvenc_level(Codec::Av1, w, h, 60, 0, true)) >= pixels,
9340 "AV1 level at {w}x{h} cannot hold the headroom"
9341 );
9342 }
9343 for codec in [Codec::H264, Codec::H265, Codec::Av1] {
9345 assert_eq!(
9346 nvenc_level(codec, 1920, 1080, 30, 0, true),
9347 nvenc_level(codec, 1920, 1080, 60, 0, true),
9348 "{codec:?} level moved between 30 and 60 fps"
9349 );
9350 }
9351 assert_eq!(
9354 nvenc_level(Codec::H264, 1920, 1080, 60, 100_000_000, true),
9355 50
9356 );
9357 assert_eq!(
9358 nvenc_level(Codec::H265, 1920, 1080, 60, 60_000_000, true),
9359 150
9360 );
9361 assert_eq!(
9362 nvenc_level(Codec::H265, 1920, 1080, 60, 60_000_000, false),
9363 156
9364 );
9365 for (bps, annex_a, weighted) in [
9368 (26_666_666, 13, 13),
9369 (26_666_667, 13, 14),
9370 (40_000_000, 13, 14),
9371 (40_000_001, 14, 17),
9372 (45_000_000, 14, 17),
9373 (60_000_001, 17, 17),
9374 (66_666_667, 17, 18),
9375 (100_000_001, 18, 18),
9376 (106_666_666, 18, 18),
9377 ] {
9378 for (weigh, level) in [(false, annex_a), (true, weighted)] {
9379 assert_eq!(
9380 nvenc_level(
9381 Codec::Av1,
9382 1920,
9383 1080,
9384 60,
9385 level_rate(Codec::Av1, bps, weigh),
9386 true
9387 ),
9388 level,
9389 "AV1 at {bps} bit/s, weighed {weigh}"
9390 );
9391 }
9392 }
9393 assert_eq!(level_rate(Codec::H265, 60_000_000, true), 60_000_000);
9394 }
9395
9396 #[test]
9400 fn split_mode_follows_the_codec_and_picture() {
9401 use NV_ENC_SPLIT_ENCODE_MODE::{
9402 NV_ENC_SPLIT_AUTO_FORCED_MODE as Forced, NV_ENC_SPLIT_AUTO_MODE as Driver,
9403 NV_ENC_SPLIT_THREE_FORCED_MODE as Three,
9404 };
9405 for (codec, w, h, engines, api, want) in [
9406 (Codec::Av1, 1280, 720, Some(2), (13, 0), Forced),
9407 (Codec::Av1, 3840, 2160, Some(3), (12, 1), Three),
9408 (Codec::Av1, 1920, 1080, Some(3), (13, 0), Three),
9409 (Codec::Av1, 1920, 1080, Some(4), (13, 0), Forced),
9410 (Codec::Av1, 1920, 1080, Some(3), (12, 0), Driver),
9411 (Codec::H265, 3840, 2160, Some(3), (13, 0), Forced),
9412 (Codec::H265, 1920, 1080, Some(3), (13, 0), Driver),
9413 (Codec::Av1, 1920, 1080, Some(1), (13, 0), Driver),
9414 (Codec::Av1, 1920, 1080, None, (13, 0), Driver),
9415 (Codec::H265, 2560, 1440, Some(2), (13, 0), Driver),
9416 (Codec::H265, 3840, 2160, Some(2), (13, 0), Forced),
9417 (Codec::H265, 2160, 3840, Some(2), (13, 0), Forced),
9418 (Codec::H265, 3840, 2160, Some(2), (12, 0), Driver),
9419 (Codec::H264, 3840, 2160, Some(2), (13, 0), Driver),
9420 ] {
9421 assert_eq!(
9422 split_mode(codec, w, h, engines, api),
9423 want,
9424 "{codec:?} {w}x{h} on {engines:?} engines, API {api:?}"
9425 );
9426 }
9427 }
9428
9429 #[test]
9432 fn cbr_targets_hold_to_the_top_level() {
9433 for (codec, kbps, bps) in [
9434 (Codec::Av1, 8_000, 8_000_000),
9435 (Codec::Av1, 106_666, 106_666_000),
9436 (Codec::Av1, 200_000, 106_666_666),
9437 (Codec::H265, 800_000, 800_000_000),
9438 (Codec::H265, 1_000_000, 800_000_000),
9439 (Codec::H264, 1_000_000, 1_000_000_000),
9440 (Codec::H264, 5_000_000, 1_000_000_000),
9441 ] {
9442 let s = RustCaptureSettings {
9443 codec,
9444 video_bitrate_kbps: kbps,
9445 ..Default::default()
9446 };
9447 assert_eq!(cbr_bps(&s), bps, "{codec:?} at {kbps} kbps");
9448 }
9449 assert_eq!(
9450 nvenc_level(
9451 Codec::Av1,
9452 1920,
9453 1080,
9454 240,
9455 nvenc_rate_ceiling(Codec::Av1) as u64,
9456 true
9457 ),
9458 18
9459 );
9460 }
9461
9462 fn av1_max_picture(level: u32) -> u64 {
9464 match level {
9465 8 | 9 => 2_359_296,
9466 12..=15 => 8_912_896,
9467 _ => 35_651_584,
9468 }
9469 }
9470
9471 fn h265_max_picture(level: u32) -> u64 {
9473 match level {
9474 123 => 2_228_224,
9475 150 | 153 | 156 => 8_912_896,
9476 _ => 35_651_584,
9477 }
9478 }
9479
9480 fn h264_max_macroblocks(level: u32) -> u64 {
9482 match level {
9483 41 => 8192,
9484 42 => 8704,
9485 50 => 22080,
9486 51 | 52 => 36864,
9487 _ => 139264,
9488 }
9489 }
9490
9491 #[test]
9495 fn dmabuf_identity_distinguishes_recycled_fd() {
9496 let base = DmaBufIdentity {
9497 dev: 1,
9498 ino: 10,
9499 size: 100,
9500 modifier: 0,
9501 width: 1920,
9502 height: 1080,
9503 };
9504 assert_eq!(base, base);
9505 let mut new_ino = base;
9506 new_ino.ino = 11;
9507 assert_ne!(base, new_ino);
9508 let mut new_size = base;
9509 new_size.size = 200;
9510 assert_ne!(base, new_size);
9511 let mut new_mod = base;
9512 new_mod.modifier = 1;
9513 assert_ne!(base, new_mod);
9514 let mut new_geom = base;
9515 new_geom.width = 1280;
9516 assert_ne!(base, new_geom);
9517 }
9518
9519 #[test]
9523 fn dmabuf_identity_probe_is_stable_and_parameterized() {
9524 use std::os::fd::AsRawFd;
9525 let f = std::fs::File::open("/dev/null").expect("open /dev/null");
9526 let fd = f.as_raw_fd();
9527 let a = DmaBufIdentity::probe(fd, 0x1234, 1920, 1080);
9528 assert_eq!(a, DmaBufIdentity::probe(fd, 0x1234, 1920, 1080));
9529 assert_ne!(a, DmaBufIdentity::probe(fd, 0x9999, 1920, 1080));
9530 assert_ne!(a, DmaBufIdentity::probe(fd, 0x1234, 1280, 720));
9531 }
9532
9533 fn egl_frame(frame_type: u32, w: u32, h: u32, pitch: u32, plane: usize) -> CUeglFrame {
9537 let mut f: CUeglFrame = unsafe { std::mem::zeroed() };
9538 f.frame_type = frame_type;
9539 f.plane_count = 1;
9540 f.width = w;
9541 f.height = h;
9542 f.pitch = pitch;
9543 f.num_channels = 4;
9544 f.cu_format = CU_AD_FORMAT_U8;
9545 f.frame.p_pitch = [plane as *mut c_void, ptr::null_mut(), ptr::null_mut()];
9546 f
9547 }
9548
9549 #[test]
9554 fn pitch_linear_frame_direct_registration_rules() {
9555 let pitch_ok = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7680, 0x1000);
9556 assert_eq!(
9557 direct_plane(&pitch_ok, 1920, 1080),
9558 Some(DirectPlane::Pitch(7680))
9559 );
9560 let padded = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 8192, 0x1000);
9561 assert_eq!(
9562 direct_plane(&padded, 1920, 1080),
9563 Some(DirectPlane::Pitch(8192))
9564 );
9565 let larger = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 2048, 1200, 8192, 0x1000);
9566 assert_eq!(
9567 direct_plane(&larger, 1920, 1080),
9568 Some(DirectPlane::Pitch(8192))
9569 );
9570
9571 let null_plane = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7680, 0);
9572 assert_eq!(direct_plane(&null_plane, 1920, 1080), None);
9573 let short_pitch = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7676, 0x1000);
9574 assert_eq!(direct_plane(&short_pitch, 1920, 1080), None);
9575 let unaligned = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7682, 0x1000);
9576 assert_eq!(direct_plane(&unaligned, 1920, 1080), None);
9577 let smaller = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1280, 720, 7680, 0x1000);
9578 assert_eq!(direct_plane(&smaller, 1920, 1080), None);
9579 let mut no_planes = pitch_ok;
9580 no_planes.plane_count = 0;
9581 assert_eq!(direct_plane(&no_planes, 1920, 1080), None);
9582 assert_eq!(direct_plane(&pitch_ok, 0, 1080), None);
9583 let mut unknown_kind = pitch_ok;
9584 unknown_kind.frame_type = 7;
9585 assert_eq!(direct_plane(&unknown_kind, 1920, 1080), None);
9586 }
9587
9588 #[test]
9592 fn cuda_array_frame_direct_registration_rules() {
9593 let array = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1920, 1080, 0, 0x2000);
9594 assert_eq!(
9595 direct_plane(&array, 1920, 1080),
9596 Some(DirectPlane::Array(7680))
9597 );
9598 let wider = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 2048, 1080, 0, 0x2000);
9599 assert_eq!(
9600 direct_plane(&wider, 1920, 1080),
9601 Some(DirectPlane::Array(8192))
9602 );
9603
9604 let null_array = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1920, 1080, 0, 0);
9605 assert_eq!(direct_plane(&null_array, 1920, 1080), None);
9606 let mut one_channel = array;
9607 one_channel.num_channels = 1;
9608 assert_eq!(direct_plane(&one_channel, 1920, 1080), None);
9609 let mut wide_elements = array;
9610 wide_elements.cu_format = 3;
9611 assert_eq!(direct_plane(&wide_elements, 1920, 1080), None);
9612 let smaller = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1280, 720, 0, 0x2000);
9613 assert_eq!(direct_plane(&smaller, 1920, 1080), None);
9614 }
9615
9616 #[test]
9620 fn fourcc_selects_nvenc_byte_order() {
9621 for code in [Fourcc::Argb8888, Fourcc::Xrgb8888] {
9622 assert_eq!(
9623 fourcc_nvenc_format(code),
9624 Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB)
9625 );
9626 }
9627 for code in [Fourcc::Abgr8888, Fourcc::Xbgr8888] {
9628 assert_eq!(
9629 fourcc_nvenc_format(code),
9630 Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR)
9631 );
9632 }
9633 for code in [
9634 Fourcc::Rgb565,
9635 Fourcc::Nv12,
9636 Fourcc::Argb2101010,
9637 Fourcc::Bgra8888,
9638 Fourcc::Rgba8888,
9639 ] {
9640 assert_eq!(fourcc_nvenc_format(code), None, "{code:?}");
9641 }
9642 }
9643
9644 #[test]
9646 fn egl_import_names_alpha_twins() {
9647 assert_eq!(egl_import_fourcc(Fourcc::Xrgb8888), Fourcc::Argb8888);
9648 assert_eq!(egl_import_fourcc(Fourcc::Xbgr8888), Fourcc::Abgr8888);
9649 for code in [Fourcc::Argb8888, Fourcc::Abgr8888, Fourcc::Nv12] {
9650 assert_eq!(egl_import_fourcc(code), code);
9651 }
9652 }
9653}