1#![allow(unsafe_op_in_unsafe_fn)]
24
25#![allow(non_camel_case_types)]
26#![allow(non_snake_case)]
27
28use std::collections::HashMap;
29use std::ffi::{c_char, c_void, CStr, CString};
30use std::os::unix::io::AsRawFd;
31use std::ptr;
32use std::sync::Arc;
33
34use libloading::{Library, Symbol};
35use smithay::backend::allocator::{dmabuf::Dmabuf, Buffer, Fourcc};
36
37use crate::RustCaptureSettings;
38use nvcodec_sys::cuda::*;
39use nvcodec_sys::*;
40
41type EGLDisplay = *const c_void;
44type EGLImageKHR = *mut c_void;
45type EGLint = i32;
46type EGLenum = u32;
47type EGLBoolean = u32;
48
49const EGL_NO_IMAGE_KHR: EGLImageKHR = ptr::null_mut();
50const EGL_LINUX_DMA_BUF_EXT: u32 = 0x3270;
51const EGL_DMA_BUF_PLANE0_FD_EXT: EGLint = 0x3272;
52const EGL_DMA_BUF_PLANE0_OFFSET_EXT: EGLint = 0x3273;
53const EGL_DMA_BUF_PLANE0_PITCH_EXT: EGLint = 0x3274;
54const EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT: EGLint = 0x3443;
55const EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT: EGLint = 0x3444;
56const EGL_WIDTH: EGLint = 0x3057;
57const EGL_HEIGHT: EGLint = 0x3056;
58const EGL_LINUX_DRM_FOURCC_EXT: EGLint = 0x3271;
59const EGL_NONE: EGLint = 0x3038;
60
61type CUgraphicsResource = *mut c_void;
64
65const CU_EGL_FRAME_TYPE_ARRAY: u32 = 0;
68const CU_EGL_FRAME_TYPE_PITCH: u32 = 1;
69const CU_AD_FORMAT_U8: u32 = 1;
71
72#[repr(C)]
75#[derive(Clone, Copy)]
76struct CUeglFrame {
77 frame: CUeglFrameUnion,
78 width: u32,
79 height: u32,
80 depth: u32,
81 pitch: u32,
82 plane_count: u32,
83 num_channels: u32,
84 frame_type: u32,
85 egl_color_format: u32,
86 cu_format: u32,
87}
88
89#[repr(C)]
92#[derive(Clone, Copy)]
93union CUeglFrameUnion {
94 p_array: [CUarray; 3],
95 p_pitch: [*mut c_void; 3],
96}
97
98type EglCreateImageKhrFn = unsafe extern "C" fn(
99 dpy: EGLDisplay,
100 ctx: *mut c_void,
101 target: EGLenum,
102 buffer: *mut c_void,
103 attrib_list: *const EGLint,
104) -> EGLImageKHR;
105type EglDestroyImageKhrFn = unsafe extern "C" fn(dpy: EGLDisplay, image: EGLImageKHR) -> EGLBoolean;
106
107struct EglFunctions {
110 _lib: Library,
111 eglGetProcAddress: unsafe extern "C" fn(procname: *const c_char) -> *mut c_void,
112 eglCreateImageKHR: EglCreateImageKhrFn,
113 eglDestroyImageKHR: EglDestroyImageKhrFn,
114}
115
116struct CudaFunctions {
119 _lib: Library,
120 cuInit: unsafe extern "C" fn(flags: u32) -> CUresult,
121 cuDeviceGet: unsafe extern "C" fn(device: *mut CUdevice, ordinal: i32) -> CUresult,
122 cuDeviceGetByPCIBusId: unsafe extern "C" fn(dev: *mut CUdevice, pciBusId: *const c_char) -> CUresult,
123 cuDevicePrimaryCtxRetain: unsafe extern "C" fn(
124 pctx: *mut CUcontext,
125 dev: CUdevice,
126 ) -> CUresult,
127 cuCtxPushCurrent_v2: unsafe extern "C" fn(ctx: CUcontext) -> CUresult,
128 cuCtxPopCurrent_v2: unsafe extern "C" fn(pctx: *mut CUcontext) -> CUresult,
129 cuDevicePrimaryCtxRelease_v2: unsafe extern "C" fn(dev: CUdevice) -> CUresult,
130 cuMemAlloc_v2: unsafe extern "C" fn(dptr: *mut CUdeviceptr, bytesize: usize) -> CUresult,
131 cuMemAllocPitch_v2: unsafe extern "C" fn(
132 dptr: *mut CUdeviceptr,
133 pPitch: *mut usize,
134 WidthInBytes: usize,
135 Height: usize,
136 ElementSizeBytes: u32,
137 ) -> CUresult,
138 cuMemFree_v2: unsafe extern "C" fn(dptr: CUdeviceptr) -> CUresult,
139 cuMemcpyHtoD_v2: unsafe extern "C" fn(
140 dstDevice: CUdeviceptr,
141 srcHost: *const c_void,
142 ByteCount: usize,
143 ) -> CUresult,
144 cuMemcpyDtoH_v2: unsafe extern "C" fn(
145 dstHost: *mut c_void,
146 srcDevice: CUdeviceptr,
147 ByteCount: usize,
148 ) -> CUresult,
149 cuMemcpy2D_v2: unsafe extern "C" fn(pCopy: *const CUDA_MEMCPY2D) -> CUresult,
150 cuMemcpy2DAsync_v2: unsafe extern "C" fn(pCopy: *const CUDA_MEMCPY2D, hStream: CUstream) -> CUresult,
151 cuStreamSynchronize: unsafe extern "C" fn(hStream: CUstream) -> CUresult,
152 cuMemHostRegister_v2: unsafe extern "C" fn(p: *mut c_void, bytesize: usize, flags: u32) -> CUresult,
153 cuMemHostUnregister: unsafe extern "C" fn(p: *mut c_void) -> CUresult,
154 cuGraphicsEGLRegisterImage: unsafe extern "C" fn(
155 pCudaResource: *mut CUgraphicsResource,
156 image: EGLImageKHR,
157 flags: u32,
158 ) -> CUresult,
159 cuGraphicsUnregisterResource: unsafe extern "C" fn(resource: CUgraphicsResource) -> CUresult,
160 cuGraphicsResourceGetMappedEglFrame: unsafe extern "C" fn(
161 pEglFrame: *mut CUeglFrame,
162 resource: CUgraphicsResource,
163 index: u32,
164 mipLevel: u32,
165 ) -> CUresult,
166 cuDeviceGetCount: unsafe extern "C" fn(count: *mut i32) -> CUresult,
167 cuDeviceGetName: unsafe extern "C" fn(name: *mut c_char, len: i32, dev: CUdevice) -> CUresult,
168 cuDeviceGetUuid: unsafe extern "C" fn(uuid: *mut CUuuid, dev: CUdevice) -> CUresult,
169 cuGetErrorName: unsafe extern "C" fn(error: CUresult, pStr: *mut *const c_char) -> CUresult,
170}
171
172struct NvencLibrary {
182 _lib: Library,
183 create_instance: unsafe extern "C" fn(
184 functionList: *mut NV_ENCODE_API_FUNCTION_LIST,
185 ) -> NVENCSTATUS,
186 get_max_version: Option<unsafe extern "C" fn(*mut u32) -> NVENCSTATUS>,
187}
188
189static NVENC_NEG_VER: std::sync::OnceLock<(u32, u32)> = std::sync::OnceLock::new();
192
193#[derive(Clone, Copy, Debug)]
206enum NvStruct {
207 FunctionList,
208 OpenSessionExParams,
209 Config,
210 RcParams,
211 PresetConfig,
212 InitializeParams,
213 ReconfigureParams,
214 RegisterResource,
215 MapInputResource,
216 CreateBitstreamBuffer,
217 PicParams,
218 LockBitstream,
219 CapsParam,
220}
221
222impl NvStruct {
223 fn rev(self, api: u32) -> (u32, bool) {
235 match self {
236 NvStruct::FunctionList => (2, false),
237 NvStruct::OpenSessionExParams => (1, false),
238 NvStruct::Config => match api {
239 0xC2.. => (9, true),
240 0xC0..=0xC1 => (8, true),
241 _ => (7, true),
242 },
243 NvStruct::RcParams => (1, false),
244 NvStruct::PresetConfig => (if api >= 0xC2 { 5 } else { 4 }, true),
245 NvStruct::InitializeParams => match api {
246 0xC2.. => (7, true),
247 0xC1 => (6, true),
248 _ => (5, true),
249 },
250 NvStruct::ReconfigureParams => (if api >= 0xC2 { 2 } else { 1 }, true),
251 NvStruct::RegisterResource => match api {
252 0xC2.. => (5, false),
253 0xC0..=0xC1 => (4, false),
254 _ => (3, false),
255 },
256 NvStruct::MapInputResource => (4, false),
257 NvStruct::CreateBitstreamBuffer => (1, false),
258 NvStruct::PicParams => match api {
259 0xC2.. => (7, true),
260 0xC0..=0xC1 => (6, true),
261 _ => (4, true),
262 },
263 NvStruct::LockBitstream => match api {
264 0xC2.. => (2, true),
265 0xC1 => (1, true),
266 0xC0 => (2, false),
267 _ => (1, false),
268 },
269 NvStruct::CapsParam => (1, false),
270 }
271 }
272}
273
274fn nvenc_struct_ver(s: NvStruct, maj: u32, min: u32) -> u32 {
287 let (rev, high_bit) = s.rev((maj << 4) | (min & 0xF));
288 (maj & 0xFF) | ((min & 0xF) << 24) | (rev << 16) | (0x7 << 28) | ((high_bit as u32) << 31)
289}
290
291#[inline]
295fn nvenc_cur_ver() -> (u32, u32) {
296 NVENC_NEG_VER
297 .get()
298 .copied()
299 .unwrap_or((NVENCAPI_VERSION & 0xFF, (NVENCAPI_VERSION >> 24) & 0xFF))
300}
301
302#[inline]
305fn sv(s: NvStruct) -> u32 {
306 let (m, n) = nvenc_cur_ver();
307 nvenc_struct_ver(s, m, n)
308}
309
310#[inline]
314fn neg_api() -> u32 {
315 let (m, n) = nvenc_cur_ver();
316 m | (n << 24)
317}
318
319fn nvenc_negotiate(lib: &NvencLibrary) {
340 NVENC_NEG_VER.get_or_init(|| {
341 let pinned = (NVENCAPI_VERSION & 0xFF, (NVENCAPI_VERSION >> 24) & 0xFF);
342 let mut drv_max: u32 = 0;
343 if let Some(get_max) = lib.get_max_version {
344 let mut m: u32 = 0;
345 if unsafe { get_max(&mut m) } == NVENCSTATUS::NV_ENC_SUCCESS {
346 drv_max = m;
347 }
348 }
349 if let Ok(cap) = std::env::var("PIXELFLUX_NVENC_MAX_API") {
350 let mut it = cap.split('.');
351 if let (Some(a), Some(b)) = (it.next(), it.next())
352 && let (Ok(cm), Ok(cn)) = (a.parse::<u32>(), b.parse::<u32>()) {
353 let capv = (cm << 4) | (cn & 0xF);
354 if capv != 0 && (drv_max == 0 || capv < drv_max) {
355 drv_max = capv;
356 }
357 }
358 }
359 let candidates = [pinned, (12, 1), (12, 0), (11, 1), (11, 0), (10, 0)];
360 for (maj, min) in candidates {
361 let vv = (maj << 4) | min;
362 if drv_max != 0 && vv > drv_max {
363 continue;
364 }
365 let mut probe = NV_ENCODE_API_FUNCTION_LIST {
366 version: nvenc_struct_ver(NvStruct::FunctionList, maj, min),
367 ..Default::default()
368 };
369 let st = unsafe { (lib.create_instance)(&mut probe) };
370 if st == NVENCSTATUS::NV_ENC_SUCCESS
371 && probe.nvEncOpenEncodeSessionEx.is_some()
372 && probe.nvEncInitializeEncoder.is_some()
373 && probe.nvEncGetEncodePresetConfigEx.is_some()
374 && probe.nvEncEncodePicture.is_some()
375 && probe.nvEncLockBitstream.is_some()
376 {
377 eprintln!("[pixelflux] NVENC API version negotiated: {}.{}", maj, min);
378 return (maj, min);
379 }
380 }
381 pinned
382 });
383}
384
385struct CachedDmaBuf {
390 identity: DmaBufIdentity,
391 egl_image: EGLImageKHR,
392 cuda_resource: CUgraphicsResource,
393 egl_frame: CUeglFrame,
394 input: DmaBufInput,
395}
396
397#[derive(Clone, Copy)]
405enum DmaBufInput {
406 Direct {
407 registered: NV_ENC_REGISTERED_PTR,
408 mapped: NV_ENC_INPUT_PTR,
409 format: NV_ENC_BUFFER_FORMAT,
410 },
411 Copy,
412}
413
414fn fourcc_nvenc_format(code: Fourcc) -> Option<NV_ENC_BUFFER_FORMAT> {
418 match code {
419 Fourcc::Argb8888 | Fourcc::Xrgb8888 => Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB),
420 Fourcc::Abgr8888 | Fourcc::Xbgr8888 => Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR),
421 _ => None,
422 }
423}
424
425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
428enum DirectPlane {
429 Pitch(u32),
431 Array(u32),
435}
436
437fn direct_plane(frame: &CUeglFrame, width: u32, height: u32) -> Option<DirectPlane> {
445 if frame.plane_count < 1 || width == 0 || height == 0 {
446 return None;
447 }
448 if frame.width < width || frame.height < height {
449 return None;
450 }
451 match frame.frame_type {
452 CU_EGL_FRAME_TYPE_PITCH => {
453 let plane = unsafe { frame.frame.p_pitch[0] };
454 let pitch_ok = frame.pitch >= width.saturating_mul(4) && frame.pitch % 4 == 0;
455 (!plane.is_null() && pitch_ok).then_some(DirectPlane::Pitch(frame.pitch))
456 }
457 CU_EGL_FRAME_TYPE_ARRAY => {
458 let array = unsafe { frame.frame.p_array[0] };
459 let packed_8bit = frame.cu_format == CU_AD_FORMAT_U8 && frame.num_channels == 4;
460 (!array.is_null() && packed_8bit).then_some(DirectPlane::Array(frame.width * 4))
461 }
462 _ => None,
463 }
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
476struct DmaBufIdentity {
477 dev: u64,
478 ino: u64,
479 size: i64,
480 modifier: u64,
481 width: u32,
482 height: u32,
483}
484
485impl DmaBufIdentity {
486 fn probe(fd: i32, modifier: u64, width: u32, height: u32) -> Self {
490 let mut st: libc::stat = unsafe { std::mem::zeroed() };
491 let (dev, ino, size) = if unsafe { libc::fstat(fd, &mut st) } == 0 {
492 (st.st_dev as u64, st.st_ino as u64, st.st_size as i64)
493 } else {
494 (0, 0, 0)
495 };
496 Self { dev, ino, size, modifier, width, height }
497 }
498}
499
500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512struct CapsDecision {
513 fullcolor: bool,
514 downgraded_color: bool,
515 too_large: Option<(i32, i32)>,
516}
517
518fn decide_caps(
520 req_fullcolor: bool,
521 req_w: i32,
522 req_h: i32,
523 cap_yuv444: Option<i32>,
524 cap_width_max: Option<i32>,
525 cap_height_max: Option<i32>,
526) -> CapsDecision {
527 let downgraded_color = req_fullcolor && cap_yuv444 == Some(0);
528 let fullcolor = req_fullcolor && !downgraded_color;
529 let exceeds = |req: i32, cap: Option<i32>| cap.is_some_and(|m| m > 0 && req > m);
530 let too_large = if exceeds(req_w, cap_width_max) || exceeds(req_h, cap_height_max) {
531 Some((cap_width_max.unwrap_or(0), cap_height_max.unwrap_or(0)))
532 } else {
533 None
534 };
535 CapsDecision { fullcolor, downgraded_color, too_large }
536}
537
538fn nvenc_headroom(size: u32, floor: u32, cap: Option<i32>) -> u32 {
542 let want = size.max(floor);
543 match cap {
544 Some(m) if m > 0 => want.min(m as u32),
545 _ => want,
546 }
547}
548
549unsafe fn query_cap(
553 funcs: &NV_ENCODE_API_FUNCTION_LIST,
554 session: *mut c_void,
555 cap: NV_ENC_CAPS,
556) -> Option<i32> {
557 let get = funcs.nvEncGetEncodeCaps?;
558 let mut param = NV_ENC_CAPS_PARAM {
559 version: sv(NvStruct::CapsParam),
560 capsToQuery: cap,
561 reserved: [0u32; 62],
562 };
563 let mut val: i32 = 0;
564 if get(session, NV_ENC_CODEC_H264_GUID, &mut param, &mut val) == NVENCSTATUS::NV_ENC_SUCCESS {
565 Some(val)
566 } else {
567 None
568 }
569}
570
571const NV_ENC_H264_PROFILE_HIGH_GUID: GUID = GUID {
573 Data1: 0x205b553d,
574 Data2: 0x5f01,
575 Data3: 0x4d9e,
576 Data4: [0x91, 0x84, 0xda, 0x32, 0x77, 0x5b, 0x55, 0x9b],
577};
578
579const NV_ENC_H264_PROFILE_HIGH_444_GUID: GUID = GUID {
581 Data1: 0x7ac663cb,
582 Data2: 0xa598,
583 Data3: 0x4960,
584 Data4: [0xb8, 0x44, 0x33, 0x9b, 0x26, 0x1a, 0x7d, 0x5c],
585};
586
587pub struct NvencEncoder {
613 encoder_session: *mut c_void,
614 cuda_context: CUcontext,
615 cuda_device: CUdevice,
616 egl_display: EGLDisplay,
617 width: u32,
618 height: u32,
619 current_qp: u32,
620 encode_config: NV_ENC_CONFIG,
621 init_params: NV_ENC_INITIALIZE_PARAMS,
622 input_device_ptr: CUdeviceptr,
623 input_pitch: usize,
624 input_format: NV_ENC_BUFFER_FORMAT,
625 registered_input_resource: NV_ENC_REGISTERED_PTR,
626 mapped_input_buffer: NV_ENC_INPUT_PTR,
627 nv12_device_ptr: Option<CUdeviceptr>,
628 nv12_pitch: usize,
629 nv12_registered_resource: Option<NV_ENC_REGISTERED_PTR>,
630 nv12_mapped_buffer: Option<NV_ENC_INPUT_PTR>,
631 bitstream_buffers: Vec<NV_ENC_OUTPUT_PTR>,
632 current_buffer_idx: usize,
633 dmabuf_cache: HashMap<i32, CachedDmaBuf>,
634 pinned_hosts: HashMap<usize, usize>,
635 cuda: Arc<CudaFunctions>,
636 egl: Arc<EglFunctions>,
637 _nvenc_lib: Arc<NvencLibrary>,
638 nvenc_funcs: NV_ENCODE_API_FUNCTION_LIST,
639 omit_stripe_headers: bool,
640 node_index: i32,
641 pin_uploads: bool,
644 direct_dmabuf: bool,
647}
648
649unsafe impl Send for NvencEncoder {}
650
651impl Drop for NvencEncoder {
668 fn drop(&mut self) {
669 unsafe {
670 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
671
672 if !self.mapped_input_buffer.is_null() {
673 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
674 self.encoder_session,
675 self.mapped_input_buffer,
676 );
677 }
678 if !self.registered_input_resource.is_null() {
679 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
680 self.encoder_session,
681 self.registered_input_resource,
682 );
683 }
684 if self.input_device_ptr != 0 {
685 (self.cuda.cuMemFree_v2)(self.input_device_ptr);
686 }
687
688 if let Some(mapped) = self.nv12_mapped_buffer {
689 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
690 self.encoder_session,
691 mapped,
692 );
693 }
694 if let Some(registered) = self.nv12_registered_resource {
695 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
696 self.encoder_session,
697 registered,
698 );
699 }
700 if let Some(ptr) = self.nv12_device_ptr {
701 (self.cuda.cuMemFree_v2)(ptr);
702 }
703
704 for &bs in &self.bitstream_buffers {
705 (self.nvenc_funcs.nvEncDestroyBitstreamBuffer.unwrap())(
706 self.encoder_session,
707 bs,
708 );
709 }
710
711 let imports: Vec<CachedDmaBuf> = self.dmabuf_cache.drain().map(|(_, c)| c).collect();
712 for cache in imports {
713 self.release_dmabuf_import(cache);
714 }
715
716 for (base, len) in &self.pinned_hosts {
717 if *len > 0 {
718 (self.cuda.cuMemHostUnregister)(*base as *mut c_void);
719 }
720 }
721
722 if !self.encoder_session.is_null() {
723 (self.nvenc_funcs.nvEncDestroyEncoder.unwrap())(self.encoder_session);
724 }
725
726 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
727 (self.cuda.cuDevicePrimaryCtxRelease_v2)(self.cuda_device);
728 }
729 }
730}
731
732use super::min_h264_level;
733
734fn nvenc_h264_level(width: u32, height: u32, fps: u32) -> u32 {
743 min_h264_level(width, height, fps).max(52)
744}
745
746impl NvencEncoder {
747 fn load_egl() -> Result<EglFunctions, String> {
754 unsafe {
755 let lib_name = "libEGL.so.1";
756 let lib = Library::new(lib_name)
757 .or_else(|_| Library::new("libEGL.so"))
758 .map_err(|e| format!("Could not load EGL library: {}", e))?;
759
760 let get_proc_addr_sym: Symbol<unsafe extern "C" fn(*const c_char) -> *mut c_void> = lib
761 .get(b"eglGetProcAddress\0")
762 .map_err(|e| format!("Missing symbol eglGetProcAddress: {}", e))?;
763
764 let eglGetProcAddress = *get_proc_addr_sym;
765
766 let load_extension = |name: &str| -> Result<*mut c_void, String> {
767 let c_name = CString::new(name).unwrap();
768 let addr = eglGetProcAddress(c_name.as_ptr());
769 if addr.is_null() {
770 Err(format!("EGL Extension not found: {}", name))
771 } else {
772 Ok(addr)
773 }
774 };
775
776 let create_addr = load_extension("eglCreateImageKHR")?;
777 let destroy_addr = load_extension("eglDestroyImageKHR")?;
778
779 Ok(EglFunctions {
780 _lib: lib,
781 eglGetProcAddress,
782 eglCreateImageKHR: std::mem::transmute::<*mut c_void, EglCreateImageKhrFn>(create_addr),
783 eglDestroyImageKHR: std::mem::transmute::<*mut c_void, EglDestroyImageKhrFn>(destroy_addr),
784 })
785 }
786 }
787
788 fn load_cuda() -> Result<CudaFunctions, String> {
794 unsafe {
795 let lib_name = if cfg!(windows) {
796 "nvcuda.dll"
797 } else {
798 "libcuda.so.1"
799 };
800 let lib = Library::new(lib_name)
801 .map_err(|e| format!("Could not load CUDA library ({}): {}", lib_name, e))?;
802
803 macro_rules! load {
804 ($lib:expr, $name:expr) => {
805 *$lib.get($name).map_err(|e| {
806 format!(
807 "Missing symbol {}: {}",
808 std::str::from_utf8($name).unwrap(),
809 e
810 )
811 })?
812 };
813 }
814
815 Ok(CudaFunctions {
816 cuInit: load!(lib, b"cuInit\0"),
817 cuDeviceGet: load!(lib, b"cuDeviceGet\0"),
818 cuDeviceGetByPCIBusId: load!(lib, b"cuDeviceGetByPCIBusId\0"),
819 cuDevicePrimaryCtxRetain: load!(lib, b"cuDevicePrimaryCtxRetain\0"),
820 cuCtxPushCurrent_v2: load!(lib, b"cuCtxPushCurrent_v2\0"),
821 cuCtxPopCurrent_v2: load!(lib, b"cuCtxPopCurrent_v2\0"),
822 cuDevicePrimaryCtxRelease_v2: load!(lib, b"cuDevicePrimaryCtxRelease_v2\0"),
823 cuMemAlloc_v2: load!(lib, b"cuMemAlloc_v2\0"),
824 cuMemAllocPitch_v2: load!(lib, b"cuMemAllocPitch_v2\0"),
825 cuMemFree_v2: load!(lib, b"cuMemFree_v2\0"),
826 cuMemcpyHtoD_v2: load!(lib, b"cuMemcpyHtoD_v2\0"),
827 cuMemcpyDtoH_v2: load!(lib, b"cuMemcpyDtoH_v2\0"),
828 cuMemcpy2D_v2: load!(lib, b"cuMemcpy2D_v2\0"),
829 cuMemcpy2DAsync_v2: load!(lib, b"cuMemcpy2DAsync_v2\0"),
830 cuStreamSynchronize: load!(lib, b"cuStreamSynchronize\0"),
831 cuMemHostRegister_v2: load!(lib, b"cuMemHostRegister_v2\0"),
832 cuMemHostUnregister: load!(lib, b"cuMemHostUnregister\0"),
833 cuGraphicsEGLRegisterImage: load!(lib, b"cuGraphicsEGLRegisterImage\0"),
834 cuGraphicsUnregisterResource: load!(lib, b"cuGraphicsUnregisterResource\0"),
835 cuGraphicsResourceGetMappedEglFrame: load!(
836 lib,
837 b"cuGraphicsResourceGetMappedEglFrame\0"
838 ),
839 cuDeviceGetCount: load!(lib, b"cuDeviceGetCount\0"),
840 cuDeviceGetName: load!(lib, b"cuDeviceGetName\0"),
841 cuDeviceGetUuid: load!(lib, b"cuDeviceGetUuid\0"),
842 cuGetErrorName: load!(lib, b"cuGetErrorName\0"),
843 _lib: lib,
844 })
845 }
846 }
847
848 fn load_nvenc() -> Result<NvencLibrary, String> {
854 unsafe {
855 let lib_name = NVENC_DLL_NAME;
856 let lib = Library::new(lib_name)
857 .map_err(|e| format!("Could not load NVENC library ({}): {}", lib_name, e))?;
858
859 let create_instance = *lib
860 .get(NV_ENCODE_API_CREATE_INSTANCE_FN_NAME)
861 .map_err(|e| e.to_string())?;
862 let get_max_version = lib
863 .get::<NvEncodeApiGetMaxSupportedVersionFn>(
864 NV_ENCODE_API_GET_MAX_SUPPORTED_VERSION_FN_NAME,
865 )
866 .map(|s| *s)
867 .ok();
868 Ok(NvencLibrary {
869 create_instance,
870 get_max_version,
871 _lib: lib,
872 })
873 }
874 }
875
876 unsafe fn get_error_string(cuda: &CudaFunctions, err: CUresult) -> String {
880 let mut p_str: *const c_char = ptr::null();
881 if (cuda.cuGetErrorName)(err, &mut p_str) == CUresult::CUDA_SUCCESS && !p_str.is_null() {
882 CStr::from_ptr(p_str).to_string_lossy().into_owned()
883 } else {
884 format!("Unknown CUDA Error ({})", err.0)
885 }
886 }
887
888 unsafe fn probe_devices(cuda: &CudaFunctions) {
891 let mut count = 0;
892 if (cuda.cuDeviceGetCount)(&mut count) != CUresult::CUDA_SUCCESS {
893 return;
894 }
895 println!("[NVENC] Found {} CUDA devices:", count);
896 for i in 0..count {
897 let mut dev = 0;
898 (cuda.cuDeviceGet)(&mut dev, i);
899 let mut name_buf = [0 as c_char; 256];
900 (cuda.cuDeviceGetName)(name_buf.as_mut_ptr(), 256, dev);
901 let name = CStr::from_ptr(name_buf.as_ptr()).to_string_lossy();
902 println!("[NVENC] Device {}: {}", i, name);
903 }
904 }
905
906 fn get_pci_bus_id(render_index: i32) -> Option<String> {
909 let path = format!("/sys/class/drm/renderD{}/device", 128 + render_index);
910 if let Ok(target) = std::fs::read_link(&path)
911 && let Some(name) = target.file_name()
912 && let Some(name_str) = name.to_str() {
913 return Some(name_str.to_string());
914 }
915 None
916 }
917
918 pub fn new(
966 settings: &RustCaptureSettings,
967 egl_display: *const c_void,
968 ) -> Result<Self, String> {
969 println!("[NVENC] Initializing...");
970
971 let egl = Arc::new(Self::load_egl()?);
972 let cuda = Arc::new(Self::load_cuda()?);
973 let nvenc_lib = Arc::new(Self::load_nvenc()?);
974 nvenc_negotiate(&nvenc_lib);
975
976 crate::nvgpufilter::install();
977
978 static LEAK_ONCE: std::sync::Once = std::sync::Once::new();
979 LEAK_ONCE.call_once(|| {
980 std::mem::forget(egl.clone());
981 std::mem::forget(cuda.clone());
982 std::mem::forget(nvenc_lib.clone());
983 });
984
985 unsafe {
986 let res = (cuda.cuInit)(0);
987 if res != CUresult::CUDA_SUCCESS {
988 return Err(format!(
989 "Init CUDA failed: {}",
990 Self::get_error_string(&cuda, res)
991 ));
992 }
993
994 Self::probe_devices(&cuda);
995
996 let mut cu_device: CUdevice = 0;
997 let mut device_found = false;
998
999 if let Some(pci_bus_id) = Self::get_pci_bus_id(settings.encode_node_index.max(0)) {
1000 let c_pci_bus_id = CString::new(pci_bus_id.clone()).unwrap();
1001 if (cuda.cuDeviceGetByPCIBusId)(&mut cu_device, c_pci_bus_id.as_ptr()) == CUresult::CUDA_SUCCESS {
1002 println!("[NVENC] Bound to CUDA device via PCI Bus ID: {}", pci_bus_id);
1003 device_found = true;
1004 }
1005 }
1006
1007 if !device_found {
1008 let res = (cuda.cuDeviceGet)(&mut cu_device, 0);
1009 if res != CUresult::CUDA_SUCCESS {
1010 return Err("Failed to get default CUDA device".into());
1011 }
1012 }
1013
1014 let mut cu_context: CUcontext = ptr::null_mut();
1020 let res = (cuda.cuDevicePrimaryCtxRetain)(&mut cu_context, cu_device);
1021 if res != CUresult::CUDA_SUCCESS {
1022 return Err("Failed to retain the device's primary CUDA context".into());
1023 }
1024 if (cuda.cuCtxPushCurrent_v2)(cu_context) != CUresult::CUDA_SUCCESS {
1025 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1026 return Err("Failed to make the primary CUDA context current".into());
1027 }
1028
1029 let width = settings.width as u32;
1030 let height = settings.height as u32;
1031 let mut input_device_ptr: CUdeviceptr = 0;
1032 let mut input_pitch: usize = 0;
1033
1034 let res = (cuda.cuMemAllocPitch_v2)(
1035 &mut input_device_ptr,
1036 &mut input_pitch,
1037 (width * 4) as usize,
1038 height as usize,
1039 16,
1040 );
1041 if res != CUresult::CUDA_SUCCESS {
1042 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1043 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1044 return Err("Failed to allocate ARGB input buffer on GPU".into());
1045 }
1046
1047 let mut function_list = NV_ENCODE_API_FUNCTION_LIST {
1048 version: sv(NvStruct::FunctionList),
1049 ..Default::default()
1050 };
1051 if (nvenc_lib.create_instance)(&mut function_list) != NVENCSTATUS::NV_ENC_SUCCESS {
1052 (cuda.cuMemFree_v2)(input_device_ptr);
1053 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1054 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1055 return Err("NvEncodeAPICreateInstance failed".into());
1056 }
1057
1058 let mut session_params = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
1059 version: sv(NvStruct::OpenSessionExParams),
1060 deviceType: NV_ENC_DEVICE_TYPE::NV_ENC_DEVICE_TYPE_CUDA,
1061 device: cu_context as *mut c_void,
1062 apiVersion: neg_api(),
1063 ..Default::default()
1064 };
1065
1066 let mut encoder_session: *mut c_void = ptr::null_mut();
1067 let open_fn = function_list.nvEncOpenEncodeSessionEx.unwrap();
1068 if open_fn(&mut session_params, &mut encoder_session) != NVENCSTATUS::NV_ENC_SUCCESS {
1069 (cuda.cuMemFree_v2)(input_device_ptr);
1070 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1071 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1072 return Err("Failed to open NVENC session".into());
1073 }
1074
1075 let caps_444 = query_cap(
1079 &function_list,
1080 encoder_session,
1081 NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_YUV444_ENCODE,
1082 );
1083 let caps_wmax =
1084 query_cap(&function_list, encoder_session, NV_ENC_CAPS::NV_ENC_CAPS_WIDTH_MAX);
1085 let caps_hmax =
1086 query_cap(&function_list, encoder_session, NV_ENC_CAPS::NV_ENC_CAPS_HEIGHT_MAX);
1087 let caps = decide_caps(
1088 settings.video_fullcolor,
1089 width as i32,
1090 height as i32,
1091 caps_444,
1092 caps_wmax,
1093 caps_hmax,
1094 );
1095 if let Some((mw, mh)) = caps.too_large {
1096 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
1097 (cuda.cuMemFree_v2)(input_device_ptr);
1098 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1099 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1100 return Err(format!(
1101 "NVENC maximum encode size {mw}x{mh} exceeded by {width}x{height}; using software"
1102 ));
1103 }
1104 if caps.downgraded_color {
1105 eprintln!("[NVENC] GPU does not support 4:4:4 (YUV444) encoding; encoding 4:2:0.");
1106 }
1107
1108 let is_444 = caps.fullcolor;
1109 let profile_guid = if is_444 {
1110 NV_ENC_H264_PROFILE_HIGH_444_GUID
1111 } else {
1112 NV_ENC_H264_PROFILE_HIGH_GUID
1113 };
1114
1115 let mut config = NV_ENC_CONFIG {
1116 version: sv(NvStruct::Config),
1117 ..Default::default()
1118 };
1119 let mut preset_config = NV_ENC_PRESET_CONFIG {
1120 version: sv(NvStruct::PresetConfig),
1121 presetCfg: config,
1122 ..Default::default()
1123 };
1124
1125 let get_preset_ex = function_list.nvEncGetEncodePresetConfigEx.unwrap();
1126 let preset_status = get_preset_ex(
1127 encoder_session,
1128 NV_ENC_CODEC_H264_GUID,
1129 NV_ENC_PRESET_P4_GUID,
1130 NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
1131 &mut preset_config,
1132 );
1133 if preset_status != NVENCSTATUS::NV_ENC_SUCCESS {
1134 let detail = function_list.nvEncGetLastErrorString.and_then(|f| {
1135 let p = f(encoder_session);
1136 if p.is_null() {
1137 None
1138 } else {
1139 Some(CStr::from_ptr(p).to_string_lossy().into_owned())
1140 }
1141 });
1142 eprintln!(
1143 "[NVENC] nvEncGetEncodePresetConfigEx failed ({preset_status:?}): {}",
1144 detail.as_deref().unwrap_or("no error string")
1145 );
1146 }
1147
1148 config = preset_config.presetCfg;
1149 config.version = sv(NvStruct::Config);
1150 config.profileGUID = profile_guid;
1151 if settings.video_cbr_mode {
1152 let bps = (settings.video_bitrate_kbps.max(0) as u32).saturating_mul(1000);
1153 config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
1154 config.rcParams.multiPass = NV_ENC_MULTI_PASS::NV_ENC_TWO_PASS_QUARTER_RESOLUTION;
1155 config.rcParams.averageBitRate = bps;
1156 config.rcParams.maxBitRate = bps;
1157 config.rcParams.vbvBufferSize = crate::encoders::vbv_bits(
1158 bps,
1159 settings.target_fps,
1160 settings.keyframe_interval_s,
1161 settings.video_vbv_multiplier,
1162 );
1163 if settings.video_min_qp > 0 {
1164 let q = settings.video_min_qp.min(51) as u32;
1165 config.rcParams.set_enableMinQP(1);
1166 config.rcParams.minQP.qpInterP = q;
1167 config.rcParams.minQP.qpInterB = q;
1168 config.rcParams.minQP.qpIntra = q;
1169 }
1170 if settings.video_max_qp > 0 {
1171 let q = settings.video_max_qp.min(51) as u32;
1172 config.rcParams.set_enableMaxQP(1);
1173 config.rcParams.maxQP.qpInterP = q;
1174 config.rcParams.maxQP.qpInterB = q;
1175 config.rcParams.maxQP.qpIntra = q;
1176 }
1177 } else {
1178 config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CONSTQP;
1179 config.rcParams.constQP.qpInterP = settings.video_crf as u32;
1180 config.rcParams.constQP.qpInterB = settings.video_crf as u32;
1181 config.rcParams.constQP.qpIntra = settings.video_crf as u32;
1182 }
1183 config.frameIntervalP = 1;
1184 config.gopLength = 0xFFFFFFFF;
1185 config.rcParams.set_zeroReorderDelay(1);
1186 config.encodeCodecConfig.h264Config.h264VUIParameters.bitstreamRestrictionFlag = 1;
1187 config.encodeCodecConfig.h264Config.level =
1188 nvenc_h264_level(width, height, settings.target_fps as u32);
1189 config.encodeCodecConfig.h264Config.idrPeriod = 0xFFFFFFFF;
1190 config.encodeCodecConfig.h264Config.h264VUIParameters.videoSignalTypePresentFlag = 1;
1191 config.encodeCodecConfig.h264Config.h264VUIParameters.videoFormat =
1192 NV_ENC_VUI_VIDEO_FORMAT::NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED;
1193 config.encodeCodecConfig.h264Config.h264VUIParameters.colourDescriptionPresentFlag = 1;
1194 config.encodeCodecConfig.h264Config.h264VUIParameters.colourPrimaries =
1199 NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709;
1200 config.encodeCodecConfig.h264Config.h264VUIParameters.transferCharacteristics =
1201 NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709;
1202 config.encodeCodecConfig.h264Config.h264VUIParameters.colourMatrix =
1203 NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M;
1204 config.encodeCodecConfig.h264Config.chromaFormatIDC = if is_444 { 3 } else { 1 };
1205 config.encodeCodecConfig.h264Config.h264VUIParameters.videoFullRangeFlag = 0;
1210 config.encodeCodecConfig.h264Config.set_repeatSPSPPS(1);
1211 config.encodeCodecConfig.h264Config.entropyCodingMode =
1212 NV_ENC_H264_ENTROPY_CODING_MODE::NV_ENC_H264_ENTROPY_CODING_MODE_CABAC;
1213 config.encodeCodecConfig.h264Config.set_outputAUD(0);
1214 config.rcParams.set_strictGOPTarget(1);
1215 config.rcParams.set_enableLookahead(0);
1216 config.rcParams.lookaheadDepth = 0;
1217
1218 let mut init_params = NV_ENC_INITIALIZE_PARAMS {
1219 version: sv(NvStruct::InitializeParams),
1220 encodeGUID: NV_ENC_CODEC_H264_GUID,
1221 presetGUID: NV_ENC_PRESET_P4_GUID,
1222 tuningInfo: NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
1223 encodeWidth: width,
1224 encodeHeight: height,
1225 darWidth: width,
1226 darHeight: height,
1227 frameRateNum: settings.target_fps.max(1.0) as u32,
1228 frameRateDen: 1,
1229 enablePTD: 1,
1230 encodeConfig: &mut config,
1231 maxEncodeWidth: nvenc_headroom(width, 4096, caps_wmax),
1232 maxEncodeHeight: nvenc_headroom(height, 2304, caps_hmax),
1233 ..Default::default()
1234 };
1235
1236 let init_fn = function_list.nvEncInitializeEncoder.unwrap();
1237 if init_fn(encoder_session, &mut init_params) != NVENCSTATUS::NV_ENC_SUCCESS {
1238 init_params.maxEncodeWidth = width;
1239 init_params.maxEncodeHeight = height;
1240 if init_fn(encoder_session, &mut init_params) != NVENCSTATUS::NV_ENC_SUCCESS {
1241 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
1242 (cuda.cuMemFree_v2)(input_device_ptr);
1243 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1244 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1245 return Err("Failed to initialize encoder".into());
1246 }
1247 eprintln!("[NVENC] Init with resize headroom failed; running without it.");
1248 }
1249
1250 init_params.encodeConfig = ptr::null_mut();
1251
1252 let mut reg_res = NV_ENC_REGISTER_RESOURCE {
1253 version: sv(NvStruct::RegisterResource),
1254 resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
1255 width,
1256 height,
1257 resourceToRegister: input_device_ptr as *mut c_void,
1258 pitch: input_pitch as u32,
1259 bufferFormat: NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB,
1260 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
1261 ..Default::default()
1262 };
1263
1264 let register_fn = function_list.nvEncRegisterResource.unwrap();
1265 if register_fn(encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
1266 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
1267 (cuda.cuMemFree_v2)(input_device_ptr);
1268 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1269 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1270 return Err("Failed to register input buffer".into());
1271 }
1272
1273 let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
1274 version: sv(NvStruct::MapInputResource),
1275 registeredResource: reg_res.registeredResource,
1276 ..Default::default()
1277 };
1278 let map_fn = function_list.nvEncMapInputResource.unwrap();
1279 if map_fn(encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
1280 (function_list.nvEncUnregisterResource.unwrap())(
1281 encoder_session,
1282 reg_res.registeredResource,
1283 );
1284 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
1285 (cuda.cuMemFree_v2)(input_device_ptr);
1286 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1287 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1288 return Err("Failed to map input buffer".into());
1289 }
1290
1291 let mut bitstream_buffers = Vec::new();
1292 let create_bs_fn = function_list.nvEncCreateBitstreamBuffer.unwrap();
1293 for _ in 0..4 {
1294 let mut bitstream_params = NV_ENC_CREATE_BITSTREAM_BUFFER {
1295 version: sv(NvStruct::CreateBitstreamBuffer),
1296 ..Default::default()
1297 };
1298 if create_bs_fn(encoder_session, &mut bitstream_params)
1299 != NVENCSTATUS::NV_ENC_SUCCESS
1300 {
1301 for &bs in &bitstream_buffers {
1302 (function_list.nvEncDestroyBitstreamBuffer.unwrap())(encoder_session, bs);
1303 }
1304 (function_list.nvEncUnmapInputResource.unwrap())(
1305 encoder_session,
1306 map_params.mappedResource,
1307 );
1308 (function_list.nvEncUnregisterResource.unwrap())(
1309 encoder_session,
1310 reg_res.registeredResource,
1311 );
1312 (function_list.nvEncDestroyEncoder.unwrap())(encoder_session);
1313 (cuda.cuMemFree_v2)(input_device_ptr);
1314 (cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1315 (cuda.cuDevicePrimaryCtxRelease_v2)(cu_device);
1316 return Err("Failed to create bitstream buffer".into());
1317 }
1318 bitstream_buffers.push(bitstream_params.bitstreamBuffer);
1319 }
1320
1321 println!("[NVENC] Initialized successfully (4:4:4 mode: {}).", is_444);
1322
1323 Ok(Self {
1324 encoder_session,
1325 cuda_context: cu_context,
1326 cuda_device: cu_device,
1327 egl_display: egl_display as EGLDisplay,
1328 width,
1329 height,
1330 current_qp: settings.video_crf as u32,
1331 encode_config: config,
1332 init_params,
1333 input_device_ptr,
1334 input_pitch,
1335 input_format: NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB,
1336 registered_input_resource: reg_res.registeredResource,
1337 mapped_input_buffer: map_params.mappedResource,
1338 nv12_device_ptr: None,
1339 nv12_pitch: 0,
1340 nv12_registered_resource: None,
1341 nv12_mapped_buffer: None,
1342 bitstream_buffers,
1343 current_buffer_idx: 0,
1344 dmabuf_cache: HashMap::new(),
1345 pinned_hosts: HashMap::new(),
1346 cuda,
1347 egl,
1348 _nvenc_lib: nvenc_lib,
1349 nvenc_funcs: function_list,
1350 omit_stripe_headers: settings.omit_stripe_headers,
1351 node_index: settings.encode_node_index.max(0),
1352 pin_uploads: std::env::var("PIXELFLUX_NVENC_PIN").as_deref() != Ok("0"),
1353 direct_dmabuf: std::env::var("PIXELFLUX_NVENC_DIRECT").as_deref() != Ok("0"),
1354 })
1355 }
1356 }
1357
1358 pub fn reconfigure_resolution(&mut self, settings: &RustCaptureSettings) -> Result<(), String> {
1383 let new_w = settings.width as u32;
1384 let new_h = settings.height as u32;
1385 let is_444 =
1386 unsafe { self.encode_config.encodeCodecConfig.h264Config.chromaFormatIDC == 3 };
1387 let is_cbr = self.encode_config.rcParams.rateControlMode
1388 == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
1389 if settings.encode_node_index.max(0) != self.node_index {
1390 return Err("encode device changed".into());
1391 }
1392 if settings.video_fullcolor != is_444 {
1393 return Err("chroma format changed".into());
1394 }
1395 if settings.video_cbr_mode != is_cbr {
1396 return Err("rate-control mode changed".into());
1397 }
1398 if new_w == 0
1399 || new_h == 0
1400 || new_w > self.init_params.maxEncodeWidth
1401 || new_h > self.init_params.maxEncodeHeight
1402 {
1403 return Err(format!(
1404 "{}x{} outside reconfigure headroom {}x{}",
1405 new_w, new_h, self.init_params.maxEncodeWidth, self.init_params.maxEncodeHeight
1406 ));
1407 }
1408
1409 unsafe {
1410 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
1411 if !self.mapped_input_buffer.is_null() {
1412 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
1413 self.encoder_session,
1414 self.mapped_input_buffer,
1415 );
1416 self.mapped_input_buffer = ptr::null_mut();
1417 }
1418 if !self.registered_input_resource.is_null() {
1419 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1420 self.encoder_session,
1421 self.registered_input_resource,
1422 );
1423 self.registered_input_resource = ptr::null_mut();
1424 }
1425 if self.input_device_ptr != 0 {
1426 (self.cuda.cuMemFree_v2)(self.input_device_ptr);
1427 self.input_device_ptr = 0;
1428 }
1429 if let Some(mapped) = self.nv12_mapped_buffer.take() {
1430 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(self.encoder_session, mapped);
1431 }
1432 if let Some(registered) = self.nv12_registered_resource.take() {
1433 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1434 self.encoder_session,
1435 registered,
1436 );
1437 }
1438 if let Some(ptr) = self.nv12_device_ptr.take() {
1439 (self.cuda.cuMemFree_v2)(ptr);
1440 }
1441 self.nv12_pitch = 0;
1442 let imports: Vec<CachedDmaBuf> = self.dmabuf_cache.drain().map(|(_, c)| c).collect();
1443 for cache in imports {
1444 self.release_dmabuf_import(cache);
1445 }
1446 for (base, len) in self.pinned_hosts.drain() {
1447 if len > 0 {
1448 (self.cuda.cuMemHostUnregister)(base as *mut c_void);
1449 }
1450 }
1451
1452 self.encode_config.encodeCodecConfig.h264Config.level =
1453 nvenc_h264_level(new_w, new_h, settings.target_fps as u32);
1454 if is_cbr {
1455 let bps = (settings.video_bitrate_kbps.max(0) as u32).saturating_mul(1000);
1456 self.encode_config.rcParams.averageBitRate = bps;
1457 self.encode_config.rcParams.maxBitRate = bps;
1458 self.encode_config.rcParams.vbvBufferSize = crate::encoders::vbv_bits(
1459 bps,
1460 settings.target_fps,
1461 settings.keyframe_interval_s,
1462 settings.video_vbv_multiplier,
1463 );
1464 } else {
1465 let qp = settings.video_crf as u32;
1466 self.encode_config.rcParams.constQP.qpInterP = qp;
1467 self.encode_config.rcParams.constQP.qpInterB = qp;
1468 self.encode_config.rcParams.constQP.qpIntra = qp;
1469 self.current_qp = qp;
1470 }
1471 self.init_params.encodeWidth = new_w;
1472 self.init_params.encodeHeight = new_h;
1473 self.init_params.darWidth = new_w;
1474 self.init_params.darHeight = new_h;
1475 self.init_params.frameRateNum = (settings.target_fps.max(1.0)) as u32;
1476 self.init_params.frameRateDen = 1;
1477 self.init_params.encodeConfig = &mut self.encode_config;
1478 let mut reconfig_params = NV_ENC_RECONFIGURE_PARAMS {
1479 version: sv(NvStruct::ReconfigureParams),
1480 reInitEncodeParams: self.init_params,
1481 ..Default::default()
1482 };
1483 reconfig_params.set_resetEncoder(1);
1484 reconfig_params.set_forceIDR(1);
1485 let reconfig_fn = self.nvenc_funcs.nvEncReconfigureEncoder.unwrap();
1486 if reconfig_fn(self.encoder_session, &mut reconfig_params)
1487 != NVENCSTATUS::NV_ENC_SUCCESS
1488 {
1489 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1490 return Err("NvEncReconfigureEncoder rejected the resolution change".into());
1491 }
1492 self.width = new_w;
1493 self.height = new_h;
1494
1495 let mut input_device_ptr: CUdeviceptr = 0;
1496 let mut input_pitch: usize = 0;
1497 let res = (self.cuda.cuMemAllocPitch_v2)(
1498 &mut input_device_ptr,
1499 &mut input_pitch,
1500 (new_w * 4) as usize,
1501 new_h as usize,
1502 16,
1503 );
1504 if res != CUresult::CUDA_SUCCESS {
1505 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1506 return Err("Failed to allocate ARGB input buffer on GPU".into());
1507 }
1508 let mut reg_res = NV_ENC_REGISTER_RESOURCE {
1509 version: sv(NvStruct::RegisterResource),
1510 resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
1511 width: new_w,
1512 height: new_h,
1513 resourceToRegister: input_device_ptr as *mut c_void,
1514 pitch: input_pitch as u32,
1515 bufferFormat: self.input_format,
1516 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
1517 ..Default::default()
1518 };
1519 let register_fn = self.nvenc_funcs.nvEncRegisterResource.unwrap();
1520 if register_fn(self.encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
1521 (self.cuda.cuMemFree_v2)(input_device_ptr);
1522 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1523 return Err("Failed to register input buffer".into());
1524 }
1525 let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
1526 version: sv(NvStruct::MapInputResource),
1527 registeredResource: reg_res.registeredResource,
1528 ..Default::default()
1529 };
1530 let map_fn = self.nvenc_funcs.nvEncMapInputResource.unwrap();
1531 if map_fn(self.encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
1532 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1533 self.encoder_session,
1534 reg_res.registeredResource,
1535 );
1536 (self.cuda.cuMemFree_v2)(input_device_ptr);
1537 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1538 return Err("Failed to map input buffer".into());
1539 }
1540 self.input_device_ptr = input_device_ptr;
1541 self.input_pitch = input_pitch;
1542 self.registered_input_resource = reg_res.registeredResource;
1543 self.mapped_input_buffer = map_params.mappedResource;
1544 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1545 }
1546 self.omit_stripe_headers = settings.omit_stripe_headers;
1547 Ok(())
1548 }
1549
1550 unsafe fn pin_host_source(&mut self, base: usize, len: usize) {
1555 if let std::collections::hash_map::Entry::Vacant(e) = self.pinned_hosts.entry(base) {
1556 let st = (self.cuda.cuMemHostRegister_v2)(base as *mut c_void, len, 0);
1557 e.insert(if st == CUresult::CUDA_SUCCESS { len } else { 0 });
1558 }
1559 }
1560
1561 pub fn release_pinned_hosts(&mut self) {
1568 if self.pinned_hosts.is_empty() {
1569 return;
1570 }
1571 unsafe {
1572 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
1573 for (base, len) in self.pinned_hosts.drain() {
1574 if len > 0 {
1575 (self.cuda.cuMemHostUnregister)(base as *mut c_void);
1576 }
1577 }
1578 let _ = (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1579 }
1580 }
1581
1582 unsafe fn release_dmabuf_import(&self, cache: CachedDmaBuf) {
1587 if let DmaBufInput::Direct { registered, mapped, .. } = cache.input {
1588 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(self.encoder_session, mapped);
1589 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(self.encoder_session, registered);
1590 }
1591 (self.cuda.cuGraphicsUnregisterResource)(cache.cuda_resource);
1592 (self.egl.eglDestroyImageKHR)(self.egl_display, cache.egl_image);
1593 }
1594
1595 unsafe fn set_input_format(&mut self, format: NV_ENC_BUFFER_FORMAT) -> Result<(), String> {
1602 if self.input_format == format && !self.registered_input_resource.is_null() {
1603 return Ok(());
1604 }
1605 if !self.mapped_input_buffer.is_null() {
1606 (self.nvenc_funcs.nvEncUnmapInputResource.unwrap())(
1607 self.encoder_session,
1608 self.mapped_input_buffer,
1609 );
1610 self.mapped_input_buffer = ptr::null_mut();
1611 }
1612 if !self.registered_input_resource.is_null() {
1613 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1614 self.encoder_session,
1615 self.registered_input_resource,
1616 );
1617 self.registered_input_resource = ptr::null_mut();
1618 }
1619 let mut reg_res = NV_ENC_REGISTER_RESOURCE {
1620 version: sv(NvStruct::RegisterResource),
1621 resourceType: NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
1622 width: self.width,
1623 height: self.height,
1624 resourceToRegister: self.input_device_ptr as *mut c_void,
1625 pitch: self.input_pitch as u32,
1626 bufferFormat: format,
1627 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
1628 ..Default::default()
1629 };
1630 if (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res)
1631 != NVENCSTATUS::NV_ENC_SUCCESS
1632 {
1633 return Err(format!("Failed to register input buffer as {format:?}"));
1634 }
1635 let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
1636 version: sv(NvStruct::MapInputResource),
1637 registeredResource: reg_res.registeredResource,
1638 ..Default::default()
1639 };
1640 if (self.nvenc_funcs.nvEncMapInputResource.unwrap())(self.encoder_session, &mut map_params)
1641 != NVENCSTATUS::NV_ENC_SUCCESS
1642 {
1643 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
1644 self.encoder_session,
1645 reg_res.registeredResource,
1646 );
1647 return Err(format!("Failed to map input buffer as {format:?}"));
1648 }
1649 self.registered_input_resource = reg_res.registeredResource;
1650 self.mapped_input_buffer = map_params.mappedResource;
1651 self.input_format = format;
1652 Ok(())
1653 }
1654
1655 unsafe fn reconfigure_if_needed(&mut self, target_qp: u32) -> bool {
1664 if self.encode_config.rcParams.rateControlMode
1665 == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
1666 {
1667 return false;
1668 }
1669 if self.current_qp != target_qp {
1670 self.encode_config.rcParams.constQP.qpInterP = target_qp;
1671 self.encode_config.rcParams.constQP.qpInterB = target_qp;
1672 self.encode_config.rcParams.constQP.qpIntra = target_qp;
1673 self.init_params.encodeConfig = &mut self.encode_config;
1674
1675 let mut reconfig_params = NV_ENC_RECONFIGURE_PARAMS {
1676 version: sv(NvStruct::ReconfigureParams),
1677 reInitEncodeParams: self.init_params,
1678 ..Default::default()
1679 };
1680
1681 let reconfig_fn = self.nvenc_funcs.nvEncReconfigureEncoder.unwrap();
1682 if reconfig_fn(self.encoder_session, &mut reconfig_params)
1683 == NVENCSTATUS::NV_ENC_SUCCESS
1684 {
1685 self.current_qp = target_qp;
1686 return true;
1687 } else {
1688 eprintln!("[NVENC] Reconfigure failed.");
1689 }
1690 }
1691 false
1692 }
1693
1694 pub fn reconfigure_rate(&mut self, settings: &RustCaptureSettings) {
1701 unsafe {
1702 let mut changed = false;
1703 if self.encode_config.rcParams.rateControlMode
1704 == NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR
1705 {
1706 let bps = (settings.video_bitrate_kbps.max(0) as u32).saturating_mul(1000);
1707 let vbv = crate::encoders::vbv_bits(
1708 bps,
1709 settings.target_fps,
1710 settings.keyframe_interval_s,
1711 settings.video_vbv_multiplier,
1712 );
1713 if self.encode_config.rcParams.averageBitRate != bps
1714 || self.encode_config.rcParams.maxBitRate != bps
1715 || self.encode_config.rcParams.vbvBufferSize != vbv
1716 {
1717 self.encode_config.rcParams.averageBitRate = bps;
1718 self.encode_config.rcParams.maxBitRate = bps;
1719 self.encode_config.rcParams.vbvBufferSize = vbv;
1720 changed = true;
1721 }
1722 }
1723 let fps = (settings.target_fps.max(1.0)) as u32;
1724 if self.init_params.frameRateNum != fps {
1725 self.init_params.frameRateNum = fps;
1726 self.init_params.frameRateDen = 1;
1727 self.encode_config.encodeCodecConfig.h264Config.level = nvenc_h264_level(
1728 self.init_params.encodeWidth,
1729 self.init_params.encodeHeight,
1730 fps,
1731 );
1732 changed = true;
1733 }
1734 if !changed {
1735 return;
1736 }
1737 self.init_params.encodeConfig = &mut self.encode_config;
1738 let mut reconfig_params = NV_ENC_RECONFIGURE_PARAMS {
1739 version: sv(NvStruct::ReconfigureParams),
1740 reInitEncodeParams: self.init_params,
1741 ..Default::default()
1742 };
1743 let reconfig_fn = self.nvenc_funcs.nvEncReconfigureEncoder.unwrap();
1744 if reconfig_fn(self.encoder_session, &mut reconfig_params)
1745 != NVENCSTATUS::NV_ENC_SUCCESS
1746 {
1747 eprintln!("[NVENC] Rate reconfigure failed.");
1748 }
1749 }
1750 }
1751
1752 unsafe fn submit_frame(
1766 &mut self,
1767 mapped_buffer: NV_ENC_INPUT_PTR,
1768 buffer_format: NV_ENC_BUFFER_FORMAT,
1769 frame_number: u64,
1770 force_idr: bool,
1771 ) -> Result<Vec<u8>, String> {
1772 let output_bitstream = self.bitstream_buffers[self.current_buffer_idx];
1773 self.current_buffer_idx = (self.current_buffer_idx + 1) % self.bitstream_buffers.len();
1774
1775 let mut pic_params = NV_ENC_PIC_PARAMS {
1776 version: sv(NvStruct::PicParams),
1777 inputWidth: self.width,
1778 inputHeight: self.height,
1779 inputBuffer: mapped_buffer,
1780 outputBitstream: output_bitstream,
1781 bufferFmt: buffer_format,
1782 pictureStruct: NV_ENC_PIC_STRUCT::NV_ENC_PIC_STRUCT_FRAME,
1783 encodePicFlags: if force_idr {
1784 NV_ENC_PIC_FLAGS::NV_ENC_PIC_FLAG_FORCEIDR as u32
1785 } else {
1786 0
1787 },
1788 ..Default::default()
1789 };
1790
1791 let encode_fn = self.nvenc_funcs.nvEncEncodePicture.unwrap();
1792 let res = encode_fn(self.encoder_session, &mut pic_params);
1793 if res != NVENCSTATUS::NV_ENC_SUCCESS {
1794 return Err(format!("Encode Picture failed: {:?}", res));
1795 }
1796
1797 let mut lock_params = NV_ENC_LOCK_BITSTREAM {
1798 version: sv(NvStruct::LockBitstream),
1799 outputBitstream: output_bitstream,
1800 ..Default::default()
1801 };
1802 lock_params.set_doNotWait(0);
1803
1804 let lock_fn = self.nvenc_funcs.nvEncLockBitstream.unwrap();
1805 if lock_fn(self.encoder_session, &mut lock_params) != NVENCSTATUS::NV_ENC_SUCCESS {
1806 return Err("Lock Bitstream failed".into());
1807 }
1808
1809 let data_ptr = lock_params.bitstreamBufferPtr as *const u8;
1810 let data_size = lock_params.bitstreamSizeInBytes as usize;
1811 let header_sz = if self.omit_stripe_headers { 0 } else { 10 };
1812 let mut output = Vec::with_capacity(header_sz + data_size);
1813
1814 if !self.omit_stripe_headers {
1815 let type_hdr = match lock_params.pictureType {
1816 NV_ENC_PIC_TYPE::NV_ENC_PIC_TYPE_IDR => 0x01u8,
1817 NV_ENC_PIC_TYPE::NV_ENC_PIC_TYPE_I => 0x02u8,
1818 _ => 0x00u8,
1819 };
1820 output.push(0x04);
1821 output.push(type_hdr);
1822 output.extend_from_slice(&(frame_number as u16).to_be_bytes());
1823 output.extend_from_slice(&0u16.to_be_bytes());
1824 output.extend_from_slice(&(self.width as u16).to_be_bytes());
1825 output.extend_from_slice(&(self.height as u16).to_be_bytes());
1826 }
1827
1828 if data_size > 0 && !data_ptr.is_null() {
1829 let slice = std::slice::from_raw_parts(data_ptr, data_size);
1830 output.extend_from_slice(slice);
1831 }
1832
1833 (self.nvenc_funcs.nvEncUnlockBitstream.unwrap())(self.encoder_session, output_bitstream);
1834 Ok(output)
1835 }
1836
1837 pub fn encode(
1863 &mut self,
1864 dmabuf: &Dmabuf,
1865 frame_number: u64,
1866 target_qp: u32,
1867 force_idr: bool,
1868 ) -> Result<Vec<u8>, String> {
1869 unsafe {
1870 self.reconfigure_if_needed(target_qp);
1871 let fd = dmabuf.handles().next().ok_or("No handles")?.as_raw_fd();
1872 let fmt = dmabuf.format();
1873 let modifier: u64 = fmt.modifier.into();
1874 let identity = DmaBufIdentity::probe(fd, modifier, self.width, self.height);
1875 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
1876
1877 if self.dmabuf_cache.get(&fd).is_some_and(|c| c.identity != identity)
1881 && let Some(stale) = self.dmabuf_cache.remove(&fd)
1882 {
1883 self.release_dmabuf_import(stale);
1884 }
1885
1886 if !self.dmabuf_cache.contains_key(&fd) {
1887 let stride = dmabuf.strides().next().unwrap_or(0) as i32;
1888 let offset = dmabuf.offsets().next().unwrap_or(0) as i32;
1889
1890 let attribs = [
1891 EGL_WIDTH,
1892 self.width as i32,
1893 EGL_HEIGHT,
1894 self.height as i32,
1895 EGL_LINUX_DRM_FOURCC_EXT,
1896 fmt.code as i32,
1897 EGL_DMA_BUF_PLANE0_FD_EXT,
1898 fd,
1899 EGL_DMA_BUF_PLANE0_OFFSET_EXT,
1900 offset,
1901 EGL_DMA_BUF_PLANE0_PITCH_EXT,
1902 stride,
1903 EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
1904 (modifier & 0xFFFFFFFF) as i32,
1905 EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
1906 (modifier >> 32) as i32,
1907 EGL_NONE,
1908 ];
1909
1910 let egl_image = (self.egl.eglCreateImageKHR)(
1911 self.egl_display,
1912 ptr::null_mut(),
1913 EGL_LINUX_DMA_BUF_EXT,
1914 ptr::null_mut(),
1915 attribs.as_ptr(),
1916 );
1917 if egl_image == EGL_NO_IMAGE_KHR {
1918 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1919 return Err("Failed to create EGLImage".into());
1920 }
1921
1922 let mut cuda_resource: CUgraphicsResource = ptr::null_mut();
1923 if (self.cuda.cuGraphicsEGLRegisterImage)(&mut cuda_resource, egl_image, 1)
1924 != CUresult::CUDA_SUCCESS
1925 {
1926 (self.egl.eglDestroyImageKHR)(self.egl_display, egl_image);
1927 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1928 return Err("Failed to register EGLImage".into());
1929 }
1930
1931 let mut egl_frame: CUeglFrame = std::mem::zeroed();
1932 if (self.cuda.cuGraphicsResourceGetMappedEglFrame)(
1933 &mut egl_frame,
1934 cuda_resource,
1935 0,
1936 0,
1937 ) != CUresult::CUDA_SUCCESS
1938 {
1939 (self.cuda.cuGraphicsUnregisterResource)(cuda_resource);
1940 (self.egl.eglDestroyImageKHR)(self.egl_display, egl_image);
1941 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
1942 return Err("Failed to map EGL frame".into());
1943 }
1944
1945 let input = match (
1946 self.direct_dmabuf,
1947 direct_plane(&egl_frame, self.width, self.height),
1948 fourcc_nvenc_format(fmt.code),
1949 ) {
1950 (true, Some(plane), Some(format)) => {
1951 self.register_direct_input(&egl_frame, plane, format)
1952 }
1953 _ => DmaBufInput::Copy,
1954 };
1955 println!(
1956 "[NVENC] dmabuf imported as a {} frame ({} planes, {}x{}, pitch {}, {} channels of element format {}): {}.",
1957 match egl_frame.frame_type {
1958 CU_EGL_FRAME_TYPE_PITCH => "pitch-linear",
1959 CU_EGL_FRAME_TYPE_ARRAY => "CUDA-array",
1960 _ => "unknown-kind",
1961 },
1962 egl_frame.plane_count,
1963 egl_frame.width,
1964 egl_frame.height,
1965 egl_frame.pitch,
1966 egl_frame.num_channels,
1967 egl_frame.cu_format,
1968 match input {
1969 DmaBufInput::Direct { .. } => "encoding in place",
1970 DmaBufInput::Copy => "copying per frame",
1971 }
1972 );
1973
1974 self.dmabuf_cache.insert(
1975 fd,
1976 CachedDmaBuf {
1977 identity,
1978 egl_image,
1979 cuda_resource,
1980 egl_frame,
1981 input,
1982 },
1983 );
1984 }
1985
1986 let (egl_frame, input) = {
1987 let cached = self.dmabuf_cache.get(&fd).unwrap();
1988 (cached.egl_frame, cached.input)
1989 };
1990 let (mapped, format) = match input {
1991 DmaBufInput::Direct { mapped, format, .. } => (mapped, format),
1992 DmaBufInput::Copy => {
1993 let mut copy_params = CUDA_MEMCPY2D {
1994 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
1995 srcHost: ptr::null(),
1996 srcDevice: 0,
1997 srcArray: ptr::null_mut(),
1998 srcPitch: 0,
1999 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2000 dstHost: ptr::null_mut(),
2001 dstDevice: self.input_device_ptr,
2002 dstArray: ptr::null_mut(),
2003 dstPitch: self.input_pitch,
2004 WidthInBytes: (self.width * 4) as usize,
2005 Height: self.height as usize,
2006 ..Default::default()
2007 };
2008 if egl_frame.frame_type == CU_EGL_FRAME_TYPE_ARRAY {
2009 copy_params.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_ARRAY;
2010 copy_params.srcArray = egl_frame.frame.p_array[0];
2011 } else {
2012 copy_params.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_DEVICE;
2013 copy_params.srcDevice = egl_frame.frame.p_pitch[0] as CUdeviceptr;
2014 copy_params.srcPitch = egl_frame.pitch as usize;
2015 }
2016 if let Some(format) = fourcc_nvenc_format(fmt.code)
2019 && let Err(e) = self.set_input_format(format)
2020 {
2021 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2022 return Err(e);
2023 }
2024 if (self.cuda.cuMemcpy2DAsync_v2)(©_params, ptr::null_mut())
2025 != CUresult::CUDA_SUCCESS
2026 {
2027 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2028 return Err("Sanitization copy failed".into());
2029 }
2030 (self.mapped_input_buffer, self.input_format)
2031 }
2032 };
2033
2034 let result = self.submit_frame(mapped, format, frame_number, force_idr);
2035 if result.is_err() {
2036 (self.cuda.cuStreamSynchronize)(ptr::null_mut());
2037 }
2038 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2039 result
2040 }
2041 }
2042
2043 unsafe fn register_direct_input(
2049 &mut self,
2050 frame: &CUeglFrame,
2051 plane: DirectPlane,
2052 format: NV_ENC_BUFFER_FORMAT,
2053 ) -> DmaBufInput {
2054 let (resource_type, resource, pitch) = match plane {
2055 DirectPlane::Pitch(pitch) => (
2056 NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
2057 frame.frame.p_pitch[0],
2058 pitch,
2059 ),
2060 DirectPlane::Array(row_bytes) => (
2061 NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDAARRAY,
2062 frame.frame.p_array[0] as *mut c_void,
2063 row_bytes,
2064 ),
2065 };
2066 let mut reg_res = NV_ENC_REGISTER_RESOURCE {
2067 version: sv(NvStruct::RegisterResource),
2068 resourceType: resource_type,
2069 width: self.width,
2070 height: self.height,
2071 resourceToRegister: resource,
2072 pitch,
2073 bufferFormat: format,
2074 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
2075 ..Default::default()
2076 };
2077 let st = (self.nvenc_funcs.nvEncRegisterResource.unwrap())(self.encoder_session, &mut reg_res);
2078 if st != NVENCSTATUS::NV_ENC_SUCCESS {
2079 eprintln!("[NVENC] dmabuf plane registration ({plane:?}) refused ({st:?}); copying per frame.");
2080 return DmaBufInput::Copy;
2081 }
2082 let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
2083 version: sv(NvStruct::MapInputResource),
2084 registeredResource: reg_res.registeredResource,
2085 ..Default::default()
2086 };
2087 let st = (self.nvenc_funcs.nvEncMapInputResource.unwrap())(self.encoder_session, &mut map_params);
2088 if st != NVENCSTATUS::NV_ENC_SUCCESS {
2089 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
2090 self.encoder_session,
2091 reg_res.registeredResource,
2092 );
2093 eprintln!("[NVENC] dmabuf plane mapping refused ({st:?}); copying per frame.");
2094 return DmaBufInput::Copy;
2095 }
2096 DmaBufInput::Direct {
2097 registered: reg_res.registeredResource,
2098 mapped: map_params.mappedResource,
2099 format,
2100 }
2101 }
2102
2103 pub fn encode_cpu_argb(
2106 &mut self,
2107 argb: &[u8],
2108 src_stride: usize,
2109 frame_number: u64,
2110 target_qp: u32,
2111 force_idr: bool,
2112 ) -> Result<Vec<u8>, String> {
2113 self.encode_cpu_packed(argb, src_stride, false, frame_number, target_qp, force_idr)
2114 }
2115
2116 pub fn encode_cpu_packed(
2141 &mut self,
2142 pixels: &[u8],
2143 src_stride: usize,
2144 rgba_input: bool,
2145 frame_number: u64,
2146 target_qp: u32,
2147 force_idr: bool,
2148 ) -> Result<Vec<u8>, String> {
2149 unsafe {
2150 self.reconfigure_if_needed(target_qp);
2151 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
2152
2153 let width_bytes = (self.width * 4) as usize;
2154 let rows = self.height as usize;
2155 let needed = if rows == 0 { 0 } else { src_stride * (rows - 1) + width_bytes };
2156 if src_stride < width_bytes || pixels.len() < needed {
2157 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2158 return Err(format!(
2159 "packed buffer too small: len={} need>={} (stride={}, {}x{})",
2160 pixels.len(), needed, src_stride, self.width, self.height
2161 ));
2162 }
2163
2164 let format = if rgba_input {
2165 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
2166 } else {
2167 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
2168 };
2169 if let Err(e) = self.set_input_format(format) {
2170 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2171 return Err(e);
2172 }
2173
2174 if self.pin_uploads {
2175 self.pin_host_source(pixels.as_ptr() as usize, pixels.len());
2176 }
2177
2178 let copy = CUDA_MEMCPY2D {
2179 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2180 srcHost: pixels.as_ptr() as *const c_void,
2181 srcPitch: src_stride,
2182 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2183 dstDevice: self.input_device_ptr,
2184 dstPitch: self.input_pitch,
2185 WidthInBytes: width_bytes,
2186 Height: rows,
2187 ..Default::default()
2188 };
2189 if (self.cuda.cuMemcpy2DAsync_v2)(©, ptr::null_mut()) != CUresult::CUDA_SUCCESS {
2190 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2191 return Err("packed host->device upload failed".into());
2192 }
2193
2194 let result = self.submit_frame(
2195 self.mapped_input_buffer,
2196 self.input_format,
2197 frame_number,
2198 force_idr,
2199 );
2200 if result.is_err() {
2201 (self.cuda.cuStreamSynchronize)(ptr::null_mut());
2202 }
2203 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2204 result
2205 }
2206 }
2207
2208 pub fn encode_raw(
2226 &mut self,
2227 raw_data: &[u8],
2228 frame_number: u64,
2229 target_qp: u32,
2230 force_idr: bool,
2231 ) -> Result<Vec<u8>, String> {
2232 unsafe {
2233 self.reconfigure_if_needed(target_qp);
2234 let _ = (self.cuda.cuCtxPushCurrent_v2)(self.cuda_context);
2235
2236 if self.pin_uploads {
2237 self.pin_host_source(raw_data.as_ptr() as usize, raw_data.len());
2238 }
2239
2240 let is_444 = self.encode_config.encodeCodecConfig.h264Config.chromaFormatIDC == 3;
2241
2242 if self.nv12_device_ptr.is_none() {
2243 let mut d_ptr: CUdeviceptr = 0;
2244 let mut pitch: usize = 0;
2245
2246 let alloc_height = if is_444 {
2247 self.height * 3
2248 } else {
2249 self.height + (self.height / 2)
2250 };
2251
2252 let res = (self.cuda.cuMemAllocPitch_v2)(
2253 &mut d_ptr,
2254 &mut pitch,
2255 self.width as usize,
2256 alloc_height as usize,
2257 16,
2258 );
2259 if res != CUresult::CUDA_SUCCESS {
2260 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2261 return Err("Failed to allocate GPU buffer for raw input".into());
2262 }
2263
2264 let buffer_fmt = if is_444 {
2265 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444
2266 } else {
2267 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_NV12
2268 };
2269
2270 let mut reg_res = NV_ENC_REGISTER_RESOURCE {
2271 version: sv(NvStruct::RegisterResource),
2272 resourceType:
2273 NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
2274 width: self.width,
2275 height: self.height,
2276 resourceToRegister: d_ptr as *mut c_void,
2277 pitch: pitch as u32,
2278 bufferFormat: buffer_fmt,
2279 bufferUsage: NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
2280 ..Default::default()
2281 };
2282
2283 let register_fn = self.nvenc_funcs.nvEncRegisterResource.unwrap();
2284 if register_fn(self.encoder_session, &mut reg_res) != NVENCSTATUS::NV_ENC_SUCCESS {
2285 (self.cuda.cuMemFree_v2)(d_ptr);
2286 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2287 return Err("Failed to register raw input buffer".into());
2288 }
2289
2290 let mut map_params = NV_ENC_MAP_INPUT_RESOURCE {
2291 version: sv(NvStruct::MapInputResource),
2292 registeredResource: reg_res.registeredResource,
2293 ..Default::default()
2294 };
2295 let map_fn = self.nvenc_funcs.nvEncMapInputResource.unwrap();
2296 if map_fn(self.encoder_session, &mut map_params) != NVENCSTATUS::NV_ENC_SUCCESS {
2297 (self.nvenc_funcs.nvEncUnregisterResource.unwrap())(
2298 self.encoder_session,
2299 reg_res.registeredResource,
2300 );
2301 (self.cuda.cuMemFree_v2)(d_ptr);
2302 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2303 return Err("Failed to map raw input buffer".into());
2304 }
2305
2306 self.nv12_device_ptr = Some(d_ptr);
2307 self.nv12_pitch = pitch;
2308 self.nv12_registered_resource = Some(reg_res.registeredResource);
2309 self.nv12_mapped_buffer = Some(map_params.mappedResource);
2310 }
2311
2312 let dev_ptr = self.nv12_device_ptr.unwrap();
2313 let dev_pitch = self.nv12_pitch;
2314 let width_bytes = self.width as usize;
2315 let height = self.height as usize;
2316
2317 if is_444 {
2318 let plane_size = width_bytes * height;
2319 if raw_data.len() < plane_size {
2320 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2321 return Err("raw frame smaller than the Y plane (444)".into());
2322 }
2323
2324 let copy_y = CUDA_MEMCPY2D {
2325 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2326 srcHost: raw_data.as_ptr() as *const c_void,
2327 srcPitch: width_bytes,
2328 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2329 dstDevice: dev_ptr,
2330 dstPitch: dev_pitch,
2331 WidthInBytes: width_bytes,
2332 Height: height,
2333 ..Default::default()
2334 };
2335 if (self.cuda.cuMemcpy2D_v2)(©_y) != CUresult::CUDA_SUCCESS {
2336 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2337 return Err("Failed to copy Y plane (444)".into());
2338 }
2339
2340 if raw_data.len() >= 2 * plane_size {
2341 let copy_u = CUDA_MEMCPY2D {
2342 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2343 srcHost: raw_data[plane_size..].as_ptr() as *const c_void,
2344 srcPitch: width_bytes,
2345 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2346 dstDevice: dev_ptr + (dev_pitch * height) as u64,
2347 dstPitch: dev_pitch,
2348 WidthInBytes: width_bytes,
2349 Height: height,
2350 ..Default::default()
2351 };
2352 if (self.cuda.cuMemcpy2D_v2)(©_u) != CUresult::CUDA_SUCCESS {
2353 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2354 return Err("Failed to copy U plane (444)".into());
2355 }
2356 }
2357
2358 if raw_data.len() >= 3 * plane_size {
2359 let copy_v = CUDA_MEMCPY2D {
2360 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2361 srcHost: raw_data[2 * plane_size..].as_ptr() as *const c_void,
2362 srcPitch: width_bytes,
2363 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2364 dstDevice: dev_ptr + (dev_pitch * height * 2) as u64,
2365 dstPitch: dev_pitch,
2366 WidthInBytes: width_bytes,
2367 Height: height,
2368 ..Default::default()
2369 };
2370 if (self.cuda.cuMemcpy2D_v2)(©_v) != CUresult::CUDA_SUCCESS {
2371 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2372 return Err("Failed to copy V plane (444)".into());
2373 }
2374 }
2375 } else {
2376 let y_size = width_bytes * height;
2377 if raw_data.len() < y_size {
2378 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2379 return Err("raw frame smaller than the Y plane".into());
2380 }
2381 let copy_y = CUDA_MEMCPY2D {
2382 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2383 srcHost: raw_data.as_ptr() as *const c_void,
2384 srcPitch: width_bytes,
2385 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2386 dstDevice: dev_ptr,
2387 dstPitch: dev_pitch,
2388 WidthInBytes: width_bytes,
2389 Height: height,
2390 ..Default::default()
2391 };
2392 if (self.cuda.cuMemcpy2D_v2)(©_y) != CUresult::CUDA_SUCCESS {
2393 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2394 return Err("Failed to copy Y plane".into());
2395 }
2396
2397 let uv_offset = y_size;
2398 if raw_data.len() >= uv_offset + width_bytes * (height / 2) {
2399 let copy_uv = CUDA_MEMCPY2D {
2400 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2401 srcHost: raw_data[uv_offset..].as_ptr() as *const c_void,
2402 srcPitch: width_bytes,
2403 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2404 dstDevice: dev_ptr + (dev_pitch * height) as u64,
2405 dstPitch: dev_pitch,
2406 WidthInBytes: width_bytes,
2407 Height: height / 2,
2408 ..Default::default()
2409 };
2410 if (self.cuda.cuMemcpy2D_v2)(©_uv) != CUresult::CUDA_SUCCESS {
2411 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2412 return Err("Failed to copy UV plane".into());
2413 }
2414 }
2415 }
2416
2417 let raw_format = if self.encode_config.encodeCodecConfig.h264Config.chromaFormatIDC == 3 {
2418 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444
2419 } else {
2420 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_NV12
2421 };
2422 let result =
2423 self.submit_frame(self.nv12_mapped_buffer.unwrap(), raw_format, frame_number, force_idr);
2424 (self.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
2425 result
2426 }
2427 }
2428}
2429
2430#[cfg(test)]
2431mod gpu_tests {
2432 use super::*;
2433
2434 fn settings(w: i32, h: i32, fps: f64) -> RustCaptureSettings {
2436 RustCaptureSettings {
2437 width: w,
2438 height: h,
2439 output_mode: 1,
2440 target_fps: fps,
2441 video_crf: 25,
2442 ..Default::default()
2443 }
2444 }
2445
2446 fn frame(w: usize, h: usize, seed: u8) -> Vec<u8> {
2449 let mut f = vec![0u8; w * h * 4];
2450 for (i, px) in f.chunks_exact_mut(4).enumerate() {
2451 let v = ((i as u32).wrapping_mul(2654435761) >> 24) as u8;
2452 px[0] = v.wrapping_add(seed);
2453 px[1] = v ^ seed;
2454 px[2] = seed;
2455 px[3] = 255;
2456 }
2457 f
2458 }
2459
2460 fn wire_dims(pkt: &[u8]) -> (u16, u16) {
2462 (
2463 u16::from_be_bytes([pkt[6], pkt[7]]),
2464 u16::from_be_bytes([pkt[8], pkt[9]]),
2465 )
2466 }
2467
2468 #[test]
2475 #[ignore]
2476 fn gpu_resolution_reconfigure_roundtrip() {
2477 let mut s = settings(1280, 720, 60.0);
2478 let t0 = std::time::Instant::now();
2479 let mut enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init");
2480 let init_ms = t0.elapsed().as_secs_f64() * 1000.0;
2481
2482 let mut stream: Vec<u8> = Vec::new();
2483 let f720 = frame(1280, 720, 10);
2484 for i in 0..5u64 {
2485 let pkt = enc
2486 .encode_cpu_argb(&f720, 1280 * 4, i, 25, i == 0)
2487 .expect("encode 720p");
2488 assert_eq!(wire_dims(&pkt), (1280, 720));
2489 stream.extend_from_slice(&pkt[10..]);
2490 }
2491
2492 s.width = 1920;
2493 s.height = 1080;
2494 let t1 = std::time::Instant::now();
2495 enc.reconfigure_resolution(&s).expect("grow reconfigure");
2496 let grow_ms = t1.elapsed().as_secs_f64() * 1000.0;
2497 let f1080 = frame(1920, 1080, 40);
2498 let pkt = enc
2499 .encode_cpu_argb(&f1080, 1920 * 4, 5, 25, false)
2500 .expect("encode 1080p");
2501 assert_eq!(pkt[0], 0x04);
2502 assert_eq!(pkt[1], 0x01, "first frame after a resize must be an IDR");
2503 assert_eq!(wire_dims(&pkt), (1920, 1080));
2504 stream.extend_from_slice(&pkt[10..]);
2505 for i in 6..10u64 {
2506 let pkt = enc
2507 .encode_cpu_argb(&f1080, 1920 * 4, i, 25, false)
2508 .expect("encode 1080p");
2509 assert_eq!(pkt[1], 0x00, "steady frames after the IDR are P frames");
2510 stream.extend_from_slice(&pkt[10..]);
2511 }
2512
2513 s.width = 640;
2514 s.height = 480;
2515 let t2 = std::time::Instant::now();
2516 enc.reconfigure_resolution(&s).expect("shrink reconfigure");
2517 let shrink_ms = t2.elapsed().as_secs_f64() * 1000.0;
2518 let f480 = frame(640, 480, 70);
2519 let pkt = enc
2520 .encode_cpu_argb(&f480, 640 * 4, 10, 25, false)
2521 .expect("encode 480p");
2522 assert_eq!(pkt[1], 0x01);
2523 assert_eq!(wire_dims(&pkt), (640, 480));
2524 stream.extend_from_slice(&pkt[10..]);
2525
2526 s.width = 4100;
2527 s.height = 2400;
2528 assert!(enc.reconfigure_resolution(&s).is_err(), "beyond headroom");
2529 s.width = 640;
2530 s.height = 480;
2531 s.video_fullcolor = true;
2532 assert!(enc.reconfigure_resolution(&s).is_err(), "chroma flip");
2533 s.video_fullcolor = false;
2534 s.video_cbr_mode = true;
2535 assert!(enc.reconfigure_resolution(&s).is_err(), "RC mode flip");
2536 s.video_cbr_mode = false;
2537 let pkt = enc
2538 .encode_cpu_argb(&f480, 640 * 4, 11, 25, false)
2539 .expect("session survives rejected reconfigures");
2540 stream.extend_from_slice(&pkt[10..]);
2541
2542 println!(
2543 "init={init_ms:.1}ms grow(720p->1080p)={grow_ms:.1}ms shrink(1080p->480p)={shrink_ms:.1}ms"
2544 );
2545 if let Ok(path) = std::env::var("NVENC_TEST_DUMP") {
2546 std::fs::write(&path, &stream).unwrap();
2547 println!("wrote {} bytes to {path}", stream.len());
2548 }
2549 }
2550
2551 #[test]
2555 #[ignore]
2556 fn gpu_resolution_reconfigure_cbr() {
2557 let mut s = settings(1280, 720, 60.0);
2558 s.video_cbr_mode = true;
2559 s.video_bitrate_kbps = 4000;
2560 let mut enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init");
2561 let f720 = frame(1280, 720, 10);
2562 for i in 0..3u64 {
2563 enc.encode_cpu_argb(&f720, 1280 * 4, i, 25, i == 0)
2564 .expect("encode 720p");
2565 }
2566 s.width = 1920;
2567 s.height = 1080;
2568 s.video_bitrate_kbps = 8000;
2569 enc.reconfigure_resolution(&s).expect("cbr resize+rate");
2570 assert_eq!(enc.encode_config.rcParams.averageBitRate, 8_000_000);
2571 let f1080 = frame(1920, 1080, 40);
2572 let pkt = enc
2573 .encode_cpu_argb(&f1080, 1920 * 4, 3, 25, false)
2574 .expect("encode 1080p");
2575 assert_eq!(pkt[1], 0x01);
2576 assert_eq!(wire_dims(&pkt), (1920, 1080));
2577 }
2578
2579 #[test]
2582 #[ignore]
2583 fn gpu_vram_probe() {
2584 fn used_mb() -> i64 {
2585 let out = std::process::Command::new("nvidia-smi")
2586 .args(["--query-gpu=memory.used", "--format=csv,noheader,nounits"])
2587 .output()
2588 .expect("nvidia-smi");
2589 String::from_utf8_lossy(&out.stdout).trim().parse().expect("parse MiB")
2590 }
2591 let s = settings(1920, 1080, 60.0);
2592 let before = used_mb();
2593 let mut enc = NvencEncoder::new(&s, ptr::null()).expect("init");
2594 let f = frame(1920, 1080, 5);
2595 for i in 0..3u64 {
2596 enc.encode_cpu_argb(&f, 1920 * 4, i, 25, i == 0).expect("encode");
2597 }
2598 println!("VRAM delta for one 1080p session: {} MiB", used_mb() - before);
2599 }
2600
2601 #[test]
2610 #[ignore]
2611 fn gpu_vui_describes_the_hardware_csc() {
2612 for fullcolor in [false, true] {
2613 let mut s = settings(1280, 720, 60.0);
2614 s.video_fullcolor = fullcolor;
2615 let enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init");
2616 let h264 = unsafe { &enc.encode_config.encodeCodecConfig.h264Config };
2618 let vui = &h264.h264VUIParameters;
2619 assert_eq!(vui.colourMatrix as u32,
2620 NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M as u32);
2621 assert_eq!(vui.colourPrimaries as u32,
2622 NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709 as u32);
2623 assert_eq!(vui.transferCharacteristics as u32,
2624 NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709 as u32);
2625 assert_eq!(vui.videoFullRangeFlag, 0, "fullcolor={fullcolor}");
2626 assert_eq!(h264.chromaFormatIDC, if fullcolor { 3 } else { 1 });
2627 }
2628 }
2629
2630 #[test]
2631 #[ignore]
2632 fn gpu_init_above_default_headroom() {
2633 let s = settings(2160, 4096, 30.0);
2634 let mut enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init portrait 4K");
2635 assert_eq!(enc.init_params.maxEncodeWidth, 4096);
2636 assert_eq!(enc.init_params.maxEncodeHeight, 4096);
2637 let f = frame(2160, 4096, 20);
2638 let pkt = enc
2639 .encode_cpu_argb(&f, 2160 * 4, 0, 25, true)
2640 .expect("encode portrait 4K");
2641 assert_eq!(wire_dims(&pkt), (2160, 4096));
2642 }
2643
2644 fn thread_cpu() -> std::time::Duration {
2647 let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
2648 unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) };
2649 std::time::Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
2650 }
2651
2652 fn per_frame(label: &str, n: usize, mut f: impl FnMut(usize)) -> (f64, f64) {
2654 let t0 = std::time::Instant::now();
2655 let c0 = thread_cpu();
2656 for i in 0..n {
2657 f(i);
2658 }
2659 let wall = t0.elapsed().as_secs_f64() * 1e6 / n as f64;
2660 let cpu = (thread_cpu() - c0).as_secs_f64() * 1e6 / n as f64;
2661 println!("{label}: {wall:.0} us wall/frame, {cpu:.0} us cpu/frame ({n} frames)");
2662 (wall, cpu)
2663 }
2664
2665 fn gpu_render() -> (gbm::Device<std::fs::File>, smithay::backend::renderer::gles::GlesRenderer) {
2669 let node = std::env::var("PIXELFLUX_TEST_RENDER_NODE")
2670 .unwrap_or_else(|_| "/dev/dri/renderD128".to_string());
2671 crate::gpu_render_init(std::path::Path::new(&node)).expect("GPU render init")
2672 }
2673
2674 const BG: [f32; 3] = [0.1, 0.2, 0.8];
2676 const FG: [f32; 3] = [0.9, 0.3, 0.1];
2677
2678 fn ycbcr_601(rgb: [f32; 3]) -> [f64; 3] {
2680 let [r, g, b] = rgb.map(|c| c as f64);
2681 [
2682 16.0 + 219.0 * (0.299 * r + 0.587 * g + 0.114 * b),
2683 128.0 + 224.0 * (-0.168736 * r - 0.331264 * g + 0.5 * b),
2684 128.0 + 224.0 * (0.5 * r - 0.418688 * g - 0.081312 * b),
2685 ]
2686 }
2687
2688 fn block_rect(w: u32, h: u32, seed: u32) -> (i32, i32, i32, i32) {
2691 let x = ((seed * 37) % (w / 2)) as i32 & !1;
2692 let y = ((seed * 53) % (h / 2)) as i32 & !1;
2693 (x, y, (w / 4) as i32 & !1, (h / 4) as i32 & !1)
2694 }
2695
2696 fn painted_dmabuf(
2700 gbm: &gbm::Device<std::fs::File>,
2701 renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
2702 w: u32,
2703 h: u32,
2704 seed: u32,
2705 ) -> (gbm::BufferObject<()>, Dmabuf) {
2706 use gbm::{BufferObjectFlags, Format as GbmFormat};
2707 use smithay::backend::renderer::{Bind, Color32F, Frame, Renderer};
2708 use smithay::utils::{Physical, Rectangle, Size, Transform};
2709 let bo = gbm
2710 .create_buffer_object::<()>(w, h, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
2711 .expect("GBM buffer");
2712 let mut dmabuf = crate::create_dmabuf_from_bo(&bo);
2713 {
2714 let mut fb = renderer.bind(&mut dmabuf).expect("bind dmabuf");
2715 let size: Size<i32, Physical> = (w as i32, h as i32).into();
2716 let mut frame = renderer.render(&mut fb, size, Transform::Normal).expect("render");
2717 let full: Rectangle<i32, Physical> = Rectangle::from_size(size);
2718 frame.clear(Color32F::new(BG[0], BG[1], BG[2], 1.0), &[full]).expect("clear");
2719 let (x, y, bw, bh) = block_rect(w, h, seed);
2720 let block: Rectangle<i32, Physical> = Rectangle::new((x, y).into(), (bw, bh).into());
2721 frame
2722 .draw_solid(
2723 block,
2724 &[Rectangle::from_size(block.size)],
2725 Color32F::new(FG[0], FG[1], FG[2], 1.0),
2726 )
2727 .expect("draw block");
2728 let sync = frame.finish().expect("finish");
2729 let _ = sync.wait();
2730 }
2731 (bo, dmabuf)
2732 }
2733
2734 fn decoded_means(
2737 dec: &mut crate::webcam::decode::AvDecoder,
2738 pkt: &[u8],
2739 rect: (i32, i32, i32, i32),
2740 ) -> ([f64; 3], [f64; 3]) {
2741 use crate::webcam::decode::Decoder;
2742 assert!(dec.decode(&pkt[10..]).expect("decode"), "no picture from this access unit");
2743 let v = dec.frame().expect("decoded frame");
2744 let (rx, ry, rw, rh) = rect;
2745 let inside = |x: usize, y: usize| {
2746 x as i32 >= rx && (x as i32) < rx + rw && y as i32 >= ry && (y as i32) < ry + rh
2747 };
2748 let mut acc = [[0f64; 3]; 2];
2749 let mut cnt = [0f64; 2];
2750 for y in 0..v.height {
2751 for x in 0..v.width {
2752 let k = if inside(x, y) { 0 } else { 1 };
2753 acc[k][0] += v.y[y * v.y_stride + x] as f64;
2754 acc[k][1] += v.u[(y / 2) * v.uv_stride + x / 2] as f64;
2755 acc[k][2] += v.v[(y / 2) * v.uv_stride + x / 2] as f64;
2756 cnt[k] += 1.0;
2757 }
2758 }
2759 let mean = |k: usize| [acc[k][0] / cnt[k], acc[k][1] / cnt[k], acc[k][2] / cnt[k]];
2760 (mean(0), mean(1))
2761 }
2762
2763 fn assert_painted(label: &str, block: [f64; 3], bg: [f64; 3], tol: f64) {
2766 let (eb, eg) = (ycbcr_601(FG), ycbcr_601(BG));
2767 for i in 0..3 {
2768 assert!(
2769 (block[i] - eb[i]).abs() <= tol,
2770 "{label}: block plane {i} = {:.1}, expected {:.1}",
2771 block[i],
2772 eb[i]
2773 );
2774 assert!(
2775 (bg[i] - eg[i]).abs() <= tol,
2776 "{label}: background plane {i} = {:.1}, expected {:.1}",
2777 bg[i],
2778 eg[i]
2779 );
2780 }
2781 }
2782
2783 fn all_direct(enc: &NvencEncoder) -> bool {
2785 !enc.dmabuf_cache.is_empty()
2786 && enc.dmabuf_cache.values().all(|c| matches!(c.input, DmaBufInput::Direct { .. }))
2787 }
2788
2789 fn mapped_kind(enc: &NvencEncoder) -> &'static str {
2791 match enc.dmabuf_cache.values().next().map(|c| c.egl_frame.frame_type) {
2792 Some(CU_EGL_FRAME_TYPE_PITCH) => "pitch-linear",
2793 Some(CU_EGL_FRAME_TYPE_ARRAY) => "as a CUDA array",
2794 _ => "in an unknown frame kind",
2795 }
2796 }
2797
2798 #[test]
2805 #[ignore]
2806 fn gpu_dmabuf_direct_and_copy_paths_decode_to_the_paint() {
2807 use crate::webcam::decode::{AvDecoder, Codec};
2808 let (w, h) = (1920u32, 1080u32);
2809 let s = settings(w as i32, h as i32, 60.0);
2810 let (gbm, mut renderer) = gpu_render();
2811 let egl_display = renderer.egl_context().display().get_display_handle().handle;
2812 let bufs: Vec<_> = (1..=2u32).map(|seed| (seed, painted_dmabuf(&gbm, &mut renderer, w, h, seed))).collect();
2813 let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
2814
2815 let run = |enc: &mut NvencEncoder, label: &str| -> Vec<Vec<u8>> {
2816 let mut dec = AvDecoder::new(Codec::H264).expect("avcodec h264");
2817 let mut out = Vec::new();
2818 for i in 0..6u64 {
2819 let (seed, (_, dmabuf)) = &bufs[(i % 2) as usize];
2820 let pkt = enc.encode(dmabuf, i, 25, i == 0).expect("dmabuf encode");
2821 assert_eq!(wire_dims(&pkt), (w as u16, h as u16));
2822 let (block, bg) = decoded_means(&mut dec, &pkt, block_rect(w, h, *seed));
2823 assert_painted(&format!("{label} frame {i}"), block, bg, 6.0);
2824 out.push(pkt[10..].to_vec());
2825 }
2826 out
2827 };
2828
2829 let direct = run(&mut enc, "direct");
2830 println!(
2831 "driver mapped the dmabuf {}: {}",
2832 mapped_kind(&enc),
2833 if all_direct(&enc) { "registered in place" } else { "direct registration unavailable, copy arm used" }
2834 );
2835
2836 enc.direct_dmabuf = false;
2837 enc.reconfigure_resolution(&s).expect("same-size reconfigure drains the import cache");
2838 let copied = run(&mut enc, "copy");
2839 assert!(!all_direct(&enc));
2840 let identical = direct.iter().zip(&copied).all(|(a, b)| a == b);
2841 println!(
2842 "direct vs copy streams: {} ({} vs {} bytes)",
2843 if identical { "byte-identical" } else { "differ" },
2844 direct.iter().map(Vec::len).sum::<usize>(),
2845 copied.iter().map(Vec::len).sum::<usize>()
2846 );
2847 if let Ok(dir) = std::env::var("NVENC_TEST_DUMP_DIR") {
2848 std::fs::write(format!("{dir}/dmabuf-direct.h264"), direct.concat()).unwrap();
2849 std::fs::write(format!("{dir}/dmabuf-copy.h264"), copied.concat()).unwrap();
2850 }
2851 }
2852
2853 #[test]
2858 #[ignore]
2859 fn gpu_packed_bgra_and_rgba_inputs_agree() {
2860 use crate::webcam::decode::{AvDecoder, Codec};
2861 let (w, h) = (1280u32, 720u32);
2862 let s = settings(w as i32, h as i32, 60.0);
2863 let rect = block_rect(w, h, 3);
2864 let paint = |rgba: bool| -> Vec<u8> {
2865 let to_u8 = |c: f32| (c * 255.0).round() as u8;
2866 let mut f = vec![0u8; (w * h * 4) as usize];
2867 for y in 0..h as i32 {
2868 for x in 0..w as i32 {
2869 let inside = x >= rect.0 && x < rect.0 + rect.2 && y >= rect.1 && y < rect.1 + rect.3;
2870 let c = if inside { FG } else { BG };
2871 let px = &mut f[((y as u32 * w + x as u32) * 4) as usize..][..4];
2872 let (r, g, b) = (to_u8(c[0]), to_u8(c[1]), to_u8(c[2]));
2873 if rgba {
2874 px.copy_from_slice(&[r, g, b, 255]);
2875 } else {
2876 px.copy_from_slice(&[b, g, r, 255]);
2877 }
2878 }
2879 }
2880 f
2881 };
2882 let bgra = paint(false);
2883 let rgba = paint(true);
2884 let mut enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init");
2885 let mut dec = AvDecoder::new(Codec::H264).expect("avcodec h264");
2886 let stride = (w * 4) as usize;
2887 for (i, (buf, is_rgba)) in [(&bgra, false), (&rgba, true), (&bgra, false), (&rgba, true)]
2888 .into_iter()
2889 .enumerate()
2890 {
2891 let pkt = enc
2892 .encode_cpu_packed(buf, stride, is_rgba, i as u64, 25, i == 0)
2893 .expect("packed encode");
2894 assert_eq!(
2895 enc.input_format,
2896 if is_rgba {
2897 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR
2898 } else {
2899 NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB
2900 }
2901 );
2902 let (block, bg) = decoded_means(&mut dec, &pkt, rect);
2903 assert_painted(&format!("frame {i} rgba={is_rgba}"), block, bg, 6.0);
2904 }
2905 }
2906
2907 #[test]
2911 #[ignore]
2912 fn gpu_bench_dmabuf_paths() {
2913 let (w, h) = (1920u32, 1080u32);
2914 let n: usize = std::env::var("NVENC_BENCH_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(300);
2915 let s = settings(w as i32, h as i32, 60.0);
2916 let (gbm, mut renderer) = gpu_render();
2917 let egl_display = renderer.egl_context().display().get_display_handle().handle;
2918 let bufs: Vec<_> = (1..=2u32).map(|seed| painted_dmabuf(&gbm, &mut renderer, w, h, seed).1).collect();
2919 let mut enc = NvencEncoder::new(&s, egl_display).expect("NVENC init");
2920 for pass in 0..2 {
2921 enc.direct_dmabuf = pass == 0;
2922 enc.reconfigure_resolution(&s).expect("reconfigure drains the import cache");
2923 enc.encode(&bufs[0], 0, 25, true).expect("warm-up");
2924 enc.encode(&bufs[1], 1, 25, false).expect("warm-up");
2925 let label = if all_direct(&enc) {
2926 format!("dmabuf registered in place ({})", mapped_kind(&enc))
2927 } else if pass == 0 {
2928 format!("dmabuf copy arm (direct registration unavailable, mapped {})", mapped_kind(&enc))
2929 } else {
2930 format!("dmabuf per-frame copy (mapped {})", mapped_kind(&enc))
2931 };
2932 per_frame(&label, n, |i| {
2933 enc.encode(&bufs[i % 2], 2 + i as u64, 25, false).expect("encode");
2934 });
2935 }
2936 }
2937
2938 #[test]
2943 #[ignore]
2944 fn gpu_bench_readback_upload() {
2945 use yuv::{BufferStoreMut, YuvBiPlanarImageMut, YuvConversionMode, YuvRange, YuvStandardMatrix};
2946 let (w, h) = (1920u32, 1080u32);
2947 let n: usize = std::env::var("NVENC_BENCH_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(300);
2948 let s = settings(w as i32, h as i32, 60.0);
2949 let frames: Vec<Vec<u8>> = (0..4u8).map(|k| frame(w as usize, h as usize, 10 + 40 * k)).collect();
2950 let stride = (w * 4) as usize;
2951 let mut enc = NvencEncoder::new(&s, ptr::null()).expect("NVENC init");
2952
2953 let mut nv12 = vec![0u8; (w * h * 3 / 2) as usize];
2954 enc.encode_raw(&nv12, 0, 25, true).expect("warm-up");
2955 per_frame("CPU NV12 conversion + encode_raw (former readback path)", n, |i| {
2956 let (y, uv) = nv12.split_at_mut((w * h) as usize);
2957 let mut planar = YuvBiPlanarImageMut {
2958 y_plane: BufferStoreMut::Borrowed(y),
2959 y_stride: w,
2960 uv_plane: BufferStoreMut::Borrowed(uv),
2961 uv_stride: w,
2962 width: w,
2963 height: h,
2964 };
2965 yuv::bgra_to_yuv_nv12(&mut planar, &frames[i % 4], w * 4, YuvRange::Limited, YuvStandardMatrix::Bt601, YuvConversionMode::Fast)
2966 .expect("csc");
2967 enc.encode_raw(&nv12, 1 + i as u64, 25, false).expect("encode_raw");
2968 });
2969
2970 enc.reconfigure_resolution(&s).expect("reconfigure");
2971 enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true).expect("warm-up");
2972 per_frame("encode_cpu_packed BGRA (pinned, async upload, hardware CSC)", n, |i| {
2973 enc.encode_cpu_packed(&frames[i % 4], stride, false, 1 + i as u64, 25, false).expect("packed");
2974 });
2975
2976 enc.reconfigure_resolution(&s).expect("reconfigure");
2977 enc.encode_cpu_packed(&frames[0], stride, true, 0, 25, true).expect("warm-up");
2978 per_frame("encode_cpu_packed RGBA (pinned, async upload, hardware CSC)", n, |i| {
2979 enc.encode_cpu_packed(&frames[i % 4], stride, true, 1 + i as u64, 25, false).expect("packed");
2980 });
2981
2982 enc.reconfigure_resolution(&s).expect("reconfigure");
2983 enc.encode_cpu_packed(&frames[0], stride, false, 0, 25, true).expect("warm-up");
2984 per_frame("packed BGRA with a synchronous cuMemcpy2D upload", n, |i| unsafe {
2985 let _ = (enc.cuda.cuCtxPushCurrent_v2)(enc.cuda_context);
2986 let src = &frames[i % 4];
2987 enc.pin_host_source(src.as_ptr() as usize, src.len());
2988 let copy = CUDA_MEMCPY2D {
2989 srcMemoryType: CUmemorytype::CU_MEMORYTYPE_HOST,
2990 srcHost: src.as_ptr() as *const c_void,
2991 srcPitch: stride,
2992 dstMemoryType: CUmemorytype::CU_MEMORYTYPE_DEVICE,
2993 dstDevice: enc.input_device_ptr,
2994 dstPitch: enc.input_pitch,
2995 WidthInBytes: stride,
2996 Height: h as usize,
2997 ..Default::default()
2998 };
2999 assert_eq!((enc.cuda.cuMemcpy2D_v2)(©), CUresult::CUDA_SUCCESS);
3000 enc.submit_frame(enc.mapped_input_buffer, enc.input_format, 1 + i as u64, false)
3001 .expect("submit");
3002 (enc.cuda.cuCtxPopCurrent_v2)(ptr::null_mut());
3003 });
3004 }
3005}
3006
3007#[cfg(test)]
3008mod version_tests {
3009 use super::*;
3010
3011 const ALL: [(NvStruct, u32); 13] = [
3014 (NvStruct::FunctionList, NV_ENCODE_API_FUNCTION_LIST_VER),
3015 (NvStruct::OpenSessionExParams, NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER),
3016 (NvStruct::Config, NV_ENC_CONFIG_VER),
3017 (NvStruct::RcParams, NV_ENC_RC_PARAMS_VER),
3018 (NvStruct::PresetConfig, NV_ENC_PRESET_CONFIG_VER),
3019 (NvStruct::InitializeParams, NV_ENC_INITIALIZE_PARAMS_VER),
3020 (NvStruct::ReconfigureParams, NV_ENC_RECONFIGURE_PARAMS_VER),
3021 (NvStruct::RegisterResource, NV_ENC_REGISTER_RESOURCE_VER),
3022 (NvStruct::MapInputResource, NV_ENC_MAP_INPUT_RESOURCE_VER),
3023 (NvStruct::CreateBitstreamBuffer, NV_ENC_CREATE_BITSTREAM_BUFFER_VER),
3024 (NvStruct::PicParams, NV_ENC_PIC_PARAMS_VER),
3025 (NvStruct::LockBitstream, NV_ENC_LOCK_BITSTREAM_VER),
3026 (NvStruct::CapsParam, NV_ENC_CAPS_PARAM_VER),
3027 ];
3028
3029 #[test]
3034 fn table_is_identity_for_pinned_version() {
3035 let maj = NVENCAPI_VERSION & 0xFF;
3036 let min = (NVENCAPI_VERSION >> 24) & 0xFF;
3037 for (s, base) in ALL {
3038 assert_eq!(nvenc_struct_ver(s, maj, min), base, "{:?}", s);
3039 }
3040 assert_eq!(maj | (min << 24), NVENCAPI_VERSION);
3041 }
3042
3043 #[test]
3051 fn table_matches_historical_headers() {
3052 #[rustfmt::skip]
3053 let expected: [(u32, u32, [u32; 12]); 7] = [
3054 (10, 0, [0x7002000A, 0x7001000A, 0xF007000A, 0x7001000A, 0xF004000A, 0xF005000A, 0xF001000A,
3055 0x7003000A, 0x7004000A, 0x7001000A, 0xF004000A, 0x7001000A]),
3056 (11, 0, [0x7002000B, 0x7001000B, 0xF007000B, 0x7001000B, 0xF004000B, 0xF005000B, 0xF001000B,
3057 0x7003000B, 0x7004000B, 0x7001000B, 0xF004000B, 0x7001000B]),
3058 (11, 1, [0x7102000B, 0x7101000B, 0xF107000B, 0x7101000B, 0xF104000B, 0xF105000B, 0xF101000B,
3059 0x7103000B, 0x7104000B, 0x7101000B, 0xF104000B, 0x7101000B]),
3060 (12, 0, [0x7002000C, 0x7001000C, 0xF008000C, 0x7001000C, 0xF004000C, 0xF005000C, 0xF001000C,
3061 0x7004000C, 0x7004000C, 0x7001000C, 0xF006000C, 0x7002000C]),
3062 (12, 1, [0x7102000C, 0x7101000C, 0xF108000C, 0x7101000C, 0xF104000C, 0xF106000C, 0xF101000C,
3063 0x7104000C, 0x7104000C, 0x7101000C, 0xF106000C, 0xF101000C]),
3064 (12, 2, [0x7202000C, 0x7201000C, 0xF209000C, 0x7201000C, 0xF205000C, 0xF207000C, 0xF202000C,
3065 0x7205000C, 0x7204000C, 0x7201000C, 0xF207000C, 0xF202000C]),
3066 (13, 0, [0x7002000D, 0x7001000D, 0xF009000D, 0x7001000D, 0xF005000D, 0xF007000D, 0xF002000D,
3067 0x7005000D, 0x7004000D, 0x7001000D, 0xF007000D, 0xF002000D]),
3068 ];
3069 for (maj, min, words) in expected {
3070 for ((s, _), want) in ALL.iter().zip(words) {
3071 assert_eq!(
3072 nvenc_struct_ver(*s, maj, min),
3073 want,
3074 "{:?} at {}.{}",
3075 s,
3076 maj,
3077 min
3078 );
3079 }
3080 }
3081 }
3082}
3083
3084#[cfg(test)]
3085mod decision_tests {
3086 use super::*;
3087
3088 #[test]
3092 fn caps_chroma_downgrade() {
3093 let d = decide_caps(true, 1920, 1080, Some(0), None, None);
3094 assert!(!d.fullcolor && d.downgraded_color && d.too_large.is_none());
3095
3096 let d = decide_caps(true, 1920, 1080, Some(1), None, None);
3097 assert!(d.fullcolor && !d.downgraded_color);
3098
3099 let d = decide_caps(true, 1920, 1080, None, None, None);
3100 assert!(d.fullcolor && !d.downgraded_color);
3101
3102 let d = decide_caps(false, 1920, 1080, Some(0), None, None);
3104 assert!(!d.fullcolor && !d.downgraded_color);
3105 }
3106
3107 #[test]
3111 fn caps_dimension_gate() {
3112 assert_eq!(
3113 decide_caps(false, 5120, 2160, None, Some(4096), Some(4096)).too_large,
3114 Some((4096, 4096))
3115 );
3116 assert_eq!(
3117 decide_caps(false, 3840, 4320, None, Some(4096), Some(4096)).too_large,
3118 Some((4096, 4096))
3119 );
3120 assert!(decide_caps(false, 3840, 2160, None, Some(4096), Some(4096)).too_large.is_none());
3121 assert!(decide_caps(false, 7680, 4320, None, None, None).too_large.is_none());
3122 assert!(decide_caps(false, 3840, 2160, None, Some(0), Some(0)).too_large.is_none());
3124 }
3125
3126 #[test]
3129 fn headroom_is_floored_and_capped() {
3130 assert_eq!(nvenc_headroom(1920, 4096, Some(8192)), 4096);
3131 assert_eq!(nvenc_headroom(3840, 4096, Some(4096)), 4096);
3132 assert_eq!(nvenc_headroom(6000, 4096, Some(8192)), 6000);
3133 assert_eq!(nvenc_headroom(1920, 4096, None), 4096);
3134 assert_eq!(nvenc_headroom(1920, 4096, Some(2048)), 2048);
3135 }
3136
3137 #[test]
3141 fn dmabuf_identity_distinguishes_recycled_fd() {
3142 let base = DmaBufIdentity { dev: 1, ino: 10, size: 100, modifier: 0, width: 1920, height: 1080 };
3143 assert_eq!(base, base);
3144 let mut new_ino = base;
3145 new_ino.ino = 11;
3146 assert_ne!(base, new_ino);
3147 let mut new_size = base;
3148 new_size.size = 200;
3149 assert_ne!(base, new_size);
3150 let mut new_mod = base;
3151 new_mod.modifier = 1;
3152 assert_ne!(base, new_mod);
3153 let mut new_geom = base;
3154 new_geom.width = 1280;
3155 assert_ne!(base, new_geom);
3156 }
3157
3158 #[test]
3162 fn dmabuf_identity_probe_is_stable_and_parameterized() {
3163 use std::os::fd::AsRawFd;
3164 let f = std::fs::File::open("/dev/null").expect("open /dev/null");
3165 let fd = f.as_raw_fd();
3166 let a = DmaBufIdentity::probe(fd, 0x1234, 1920, 1080);
3167 assert_eq!(a, DmaBufIdentity::probe(fd, 0x1234, 1920, 1080));
3168 assert_ne!(a, DmaBufIdentity::probe(fd, 0x9999, 1920, 1080));
3169 assert_ne!(a, DmaBufIdentity::probe(fd, 0x1234, 1280, 720));
3170 }
3171
3172 fn egl_frame(frame_type: u32, w: u32, h: u32, pitch: u32, plane: usize) -> CUeglFrame {
3176 let mut f: CUeglFrame = unsafe { std::mem::zeroed() };
3177 f.frame_type = frame_type;
3178 f.plane_count = 1;
3179 f.width = w;
3180 f.height = h;
3181 f.pitch = pitch;
3182 f.num_channels = 4;
3183 f.cu_format = CU_AD_FORMAT_U8;
3184 f.frame.p_pitch = [plane as *mut c_void, ptr::null_mut(), ptr::null_mut()];
3185 f
3186 }
3187
3188 #[test]
3193 fn pitch_linear_frame_direct_registration_rules() {
3194 let pitch_ok = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7680, 0x1000);
3195 assert_eq!(direct_plane(&pitch_ok, 1920, 1080), Some(DirectPlane::Pitch(7680)));
3196 let padded = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 8192, 0x1000);
3197 assert_eq!(direct_plane(&padded, 1920, 1080), Some(DirectPlane::Pitch(8192)));
3198 let larger = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 2048, 1200, 8192, 0x1000);
3199 assert_eq!(direct_plane(&larger, 1920, 1080), Some(DirectPlane::Pitch(8192)));
3200
3201 let null_plane = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7680, 0);
3202 assert_eq!(direct_plane(&null_plane, 1920, 1080), None);
3203 let short_pitch = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7676, 0x1000);
3204 assert_eq!(direct_plane(&short_pitch, 1920, 1080), None);
3205 let unaligned = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1920, 1080, 7682, 0x1000);
3206 assert_eq!(direct_plane(&unaligned, 1920, 1080), None);
3207 let smaller = egl_frame(CU_EGL_FRAME_TYPE_PITCH, 1280, 720, 7680, 0x1000);
3208 assert_eq!(direct_plane(&smaller, 1920, 1080), None);
3209 let mut no_planes = pitch_ok;
3210 no_planes.plane_count = 0;
3211 assert_eq!(direct_plane(&no_planes, 1920, 1080), None);
3212 assert_eq!(direct_plane(&pitch_ok, 0, 1080), None);
3213 let mut unknown_kind = pitch_ok;
3214 unknown_kind.frame_type = 7;
3215 assert_eq!(direct_plane(&unknown_kind, 1920, 1080), None);
3216 }
3217
3218 #[test]
3222 fn cuda_array_frame_direct_registration_rules() {
3223 let array = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1920, 1080, 0, 0x2000);
3224 assert_eq!(direct_plane(&array, 1920, 1080), Some(DirectPlane::Array(7680)));
3225 let wider = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 2048, 1080, 0, 0x2000);
3226 assert_eq!(direct_plane(&wider, 1920, 1080), Some(DirectPlane::Array(8192)));
3227
3228 let null_array = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1920, 1080, 0, 0);
3229 assert_eq!(direct_plane(&null_array, 1920, 1080), None);
3230 let mut one_channel = array;
3231 one_channel.num_channels = 1;
3232 assert_eq!(direct_plane(&one_channel, 1920, 1080), None);
3233 let mut wide_elements = array;
3234 wide_elements.cu_format = 3;
3235 assert_eq!(direct_plane(&wide_elements, 1920, 1080), None);
3236 let smaller = egl_frame(CU_EGL_FRAME_TYPE_ARRAY, 1280, 720, 0, 0x2000);
3237 assert_eq!(direct_plane(&smaller, 1920, 1080), None);
3238 }
3239
3240 #[test]
3244 fn fourcc_selects_nvenc_byte_order() {
3245 for code in [Fourcc::Argb8888, Fourcc::Xrgb8888] {
3246 assert_eq!(fourcc_nvenc_format(code), Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB));
3247 }
3248 for code in [Fourcc::Abgr8888, Fourcc::Xbgr8888] {
3249 assert_eq!(fourcc_nvenc_format(code), Some(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR));
3250 }
3251 for code in [Fourcc::Rgb565, Fourcc::Nv12, Fourcc::Argb2101010, Fourcc::Bgra8888, Fourcc::Rgba8888] {
3252 assert_eq!(fourcc_nvenc_format(code), None, "{code:?}");
3253 }
3254 }
3255}