1#![allow(non_camel_case_types)]
48#![allow(non_snake_case)]
49#![allow(clippy::upper_case_acronyms)]
50
51use std::ffi::{CStr, c_char, c_void};
52use std::sync::Arc;
53use std::sync::atomic::Ordering;
54use std::time::{Duration, Instant};
55
56use libloading::Library;
57use x11rb::protocol::xproto::ConnectionExt as XprotoExt;
58
59use super::Controls;
60use crate::RustCaptureSettings;
61use crate::encoders::nvenc::NvencEncoder;
62use crate::encoders::software::{EncodedStripe, FrameTiming, StripeState};
63use crate::pipeline::{
64 DAMAGE_BAND_ROWS, Damage, EncoderQuality, decide_hw_fullframe, hashed_damage, periodic_idr_due,
65};
66use crate::recording_sink::RecordingSink;
67use nvcodec_sys::cuda::CUdeviceptr;
68
69const NVFBC_VERSION: u32 = 8 | (1 << 8);
71
72const fn struct_ver(size: usize, rev: u32) -> u32 {
76 (size as u32) | (rev << 16) | (NVFBC_VERSION << 24)
77}
78
79type NVFBCSTATUS = u32;
80const NVFBC_SUCCESS: NVFBCSTATUS = 0;
81const NVFBC_ERR_X: NVFBCSTATUS = 10;
83const NVFBC_ERR_MUST_RECREATE: NVFBCSTATUS = 16;
85
86type NVFBC_BOOL = u32;
87const NVFBC_FALSE: NVFBC_BOOL = 0;
88const NVFBC_TRUE: NVFBC_BOOL = 1;
89
90type NVFBC_SESSION_HANDLE = u64;
91
92const NVFBC_CAPTURE_SHARED_CUDA: u32 = 1;
94const NVFBC_TRACKING_SCREEN: u32 = 2;
96const NVFBC_BUFFER_FORMAT_BGRA: u32 = 5;
98const NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT: u32 = 1 << 0;
100const NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY: u32 = 1 << 2;
102
103const NVFBC_OUTPUT_MAX: usize = 5;
104const NVFBC_OUTPUT_NAME_LEN: usize = 128;
105
106#[repr(C)]
108#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
109struct NVFBC_BOX {
110 x: u32,
111 y: u32,
112 w: u32,
113 h: u32,
114}
115
116#[repr(C)]
118#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
119struct NVFBC_SIZE {
120 w: u32,
121 h: u32,
122}
123
124#[repr(C)]
128#[derive(Clone, Copy, Default)]
129struct NVFBC_FRAME_GRAB_INFO {
130 dwWidth: u32,
131 dwHeight: u32,
132 dwByteSize: u32,
133 dwCurrentFrame: u32,
134 bIsNewFrame: NVFBC_BOOL,
135 ulTimestampUs: u64,
136 dwMissedFrames: u32,
137 bRequiredPostProcessing: NVFBC_BOOL,
138 bDirectCapture: NVFBC_BOOL,
139}
140
141#[repr(C)]
142#[derive(Clone, Copy, Default)]
143struct NVFBC_CREATE_HANDLE_PARAMS {
144 dwVersion: u32,
145 privateData: *const c_void,
146 privateDataSize: u32,
147 bExternallyManagedContext: NVFBC_BOOL,
148 glxCtx: *mut c_void,
149 glxFBConfig: *mut c_void,
150}
151
152#[repr(C)]
154#[derive(Clone, Copy, Default)]
155struct NVFBC_VERSION_ONLY_PARAMS {
156 dwVersion: u32,
157}
158
159#[repr(C)]
161#[derive(Clone, Copy)]
162struct NVFBC_RANDR_OUTPUT_INFO {
163 dwId: u32,
164 name: [c_char; NVFBC_OUTPUT_NAME_LEN],
165 trackedBox: NVFBC_BOX,
166}
167
168impl Default for NVFBC_RANDR_OUTPUT_INFO {
169 fn default() -> Self {
170 Self {
171 dwId: 0,
172 name: [0; NVFBC_OUTPUT_NAME_LEN],
173 trackedBox: NVFBC_BOX::default(),
174 }
175 }
176}
177
178#[repr(C)]
181#[derive(Clone, Copy)]
182struct NVFBC_GET_STATUS_PARAMS {
183 dwVersion: u32,
184 bIsCapturePossible: NVFBC_BOOL,
185 bCurrentlyCapturing: NVFBC_BOOL,
186 bCanCreateNow: NVFBC_BOOL,
187 screenSize: NVFBC_SIZE,
188 bXRandRAvailable: NVFBC_BOOL,
189 outputs: [NVFBC_RANDR_OUTPUT_INFO; NVFBC_OUTPUT_MAX],
190 dwOutputNum: u32,
191 dwNvFBCVersion: u32,
192 bInModeset: NVFBC_BOOL,
193}
194
195impl Default for NVFBC_GET_STATUS_PARAMS {
196 fn default() -> Self {
197 Self {
198 dwVersion: 0,
199 bIsCapturePossible: 0,
200 bCurrentlyCapturing: 0,
201 bCanCreateNow: 0,
202 screenSize: NVFBC_SIZE::default(),
203 bXRandRAvailable: 0,
204 outputs: [NVFBC_RANDR_OUTPUT_INFO::default(); NVFBC_OUTPUT_MAX],
205 dwOutputNum: 0,
206 dwNvFBCVersion: 0,
207 bInModeset: 0,
208 }
209 }
210}
211
212#[repr(C)]
213#[derive(Clone, Copy, Default)]
214struct NVFBC_CREATE_CAPTURE_SESSION_PARAMS {
215 dwVersion: u32,
216 eCaptureType: u32,
217 eTrackingType: u32,
218 dwOutputId: u32,
219 captureBox: NVFBC_BOX,
220 frameSize: NVFBC_SIZE,
221 bWithCursor: NVFBC_BOOL,
222 bDisableAutoModesetRecovery: NVFBC_BOOL,
223 bRoundFrameSize: NVFBC_BOOL,
224 dwSamplingRateMs: u32,
225 bPushModel: NVFBC_BOOL,
226 bAllowDirectCapture: NVFBC_BOOL,
227}
228
229#[repr(C)]
230#[derive(Clone, Copy, Default)]
231struct NVFBC_TOCUDA_SETUP_PARAMS {
232 dwVersion: u32,
233 eBufferFormat: u32,
234}
235
236#[repr(C)]
237#[derive(Clone, Copy, Default)]
238struct NVFBC_TOCUDA_GRAB_FRAME_PARAMS {
239 dwVersion: u32,
240 dwFlags: u32,
241 pCUDADeviceBuffer: *mut c_void,
242 pFrameGrabInfo: *mut NVFBC_FRAME_GRAB_INFO,
243 dwTimeoutMs: u32,
244}
245
246#[repr(C)]
249#[derive(Clone, Copy)]
250struct NVFBC_API_FUNCTION_LIST {
251 dwVersion: u32,
252 nvFBCGetLastErrorStr: Option<unsafe extern "C" fn(NVFBC_SESSION_HANDLE) -> *const c_char>,
253 nvFBCCreateHandle: Option<
254 unsafe extern "C" fn(
255 *mut NVFBC_SESSION_HANDLE,
256 *mut NVFBC_CREATE_HANDLE_PARAMS,
257 ) -> NVFBCSTATUS,
258 >,
259 nvFBCDestroyHandle: Option<
260 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
261 >,
262 nvFBCGetStatus: Option<
263 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_GET_STATUS_PARAMS) -> NVFBCSTATUS,
264 >,
265 nvFBCCreateCaptureSession: Option<
266 unsafe extern "C" fn(
267 NVFBC_SESSION_HANDLE,
268 *mut NVFBC_CREATE_CAPTURE_SESSION_PARAMS,
269 ) -> NVFBCSTATUS,
270 >,
271 nvFBCDestroyCaptureSession: Option<
272 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
273 >,
274 nvFBCToSysSetUp: *mut c_void,
275 nvFBCToSysGrabFrame: *mut c_void,
276 nvFBCToCudaSetUp: Option<
277 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_TOCUDA_SETUP_PARAMS) -> NVFBCSTATUS,
278 >,
279 nvFBCToCudaGrabFrame: Option<
280 unsafe extern "C" fn(
281 NVFBC_SESSION_HANDLE,
282 *mut NVFBC_TOCUDA_GRAB_FRAME_PARAMS,
283 ) -> NVFBCSTATUS,
284 >,
285 pad1: *mut c_void,
286 pad2: *mut c_void,
287 pad3: *mut c_void,
288 nvFBCBindContext: Option<
289 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
290 >,
291 nvFBCReleaseContext: Option<
292 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
293 >,
294 pad4: *mut c_void,
295 pad5: *mut c_void,
296 pad6: *mut c_void,
297 pad7: *mut c_void,
298 nvFBCToGLSetUp: *mut c_void,
299 nvFBCToGLGrabFrame: *mut c_void,
300}
301
302impl Default for NVFBC_API_FUNCTION_LIST {
303 fn default() -> Self {
304 unsafe { std::mem::zeroed() }
305 }
306}
307
308type NvFBCCreateInstanceFn = unsafe extern "C" fn(*mut NVFBC_API_FUNCTION_LIST) -> NVFBCSTATUS;
309
310const NVFBC_LIBRARY: &str = "libnvidia-fbc.so.1";
313
314#[derive(Debug)]
320struct NvfbcError {
321 status: NVFBCSTATUS,
322 message: String,
323}
324
325impl std::fmt::Display for NvfbcError {
326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 if self.message.is_empty() {
328 write!(f, "({})", self.status)
329 } else {
330 write!(f, "{} ({})", self.message, self.status)
331 }
332 }
333}
334
335struct GrabbedFrame {
337 device_ptr: CUdeviceptr,
338 width: u32,
339 height: u32,
340 byte_size: u32,
341 is_new: bool,
344 direct: bool,
347 post_processed: bool,
350}
351
352struct NvfbcSession {
361 _lib: Library,
362 funcs: NVFBC_API_FUNCTION_LIST,
363 handle: NVFBC_SESSION_HANDLE,
364 capture_open: bool,
365 region: NVFBC_BOX,
367 size: NVFBC_SIZE,
368 with_cursor: bool,
369}
370
371impl NvfbcSession {
372 fn load() -> Result<(Library, NVFBC_API_FUNCTION_LIST), String> {
376 unsafe {
377 let lib = Library::new(NVFBC_LIBRARY)
378 .map_err(|e| format!("could not load {NVFBC_LIBRARY}: {e}"))?;
379 let create: libloading::Symbol<NvFBCCreateInstanceFn> = lib
380 .get(b"NvFBCCreateInstance\0")
381 .map_err(|e| format!("missing symbol NvFBCCreateInstance: {e}"))?;
382 let mut funcs = NVFBC_API_FUNCTION_LIST {
383 dwVersion: NVFBC_VERSION,
384 ..Default::default()
385 };
386 let st = create(&mut funcs);
387 if st != NVFBC_SUCCESS {
388 return Err(format!("NvFBCCreateInstance failed ({st})"));
389 }
390 if funcs.nvFBCCreateHandle.is_none()
391 || funcs.nvFBCGetStatus.is_none()
392 || funcs.nvFBCCreateCaptureSession.is_none()
393 || funcs.nvFBCToCudaSetUp.is_none()
394 || funcs.nvFBCToCudaGrabFrame.is_none()
395 {
396 return Err("the driver's NvFBC table has no CUDA capture entry points".into());
397 }
398 Ok((lib, funcs))
399 }
400 }
401
402 fn last_error(&self) -> String {
405 match self.funcs.nvFBCGetLastErrorStr {
406 Some(f) => unsafe {
407 let p = f(self.handle);
408 if p.is_null() {
409 String::new()
410 } else {
411 CStr::from_ptr(p).to_string_lossy().into_owned()
412 }
413 },
414 None => String::new(),
415 }
416 }
417
418 fn fail(&self, what: &str, status: NVFBCSTATUS) -> NvfbcError {
420 let detail = self.last_error();
421 let message = if detail.is_empty() {
422 format!("{what} failed")
423 } else {
424 format!("{what} failed: {detail}")
425 };
426 NvfbcError { status, message }
427 }
428
429 fn open() -> Result<Self, String> {
438 if !nvidia_driven_x_server()? {
439 return Err(format!("the X server offers no {NVIDIA_GLX}"));
440 }
441 let (lib, funcs) = Self::load()?;
442 let mut handle: NVFBC_SESSION_HANDLE = 0;
443 let mut params = NVFBC_CREATE_HANDLE_PARAMS {
444 dwVersion: struct_ver(std::mem::size_of::<NVFBC_CREATE_HANDLE_PARAMS>(), 2),
445 bExternallyManagedContext: NVFBC_FALSE,
446 ..Default::default()
447 };
448 let st = unsafe { (funcs.nvFBCCreateHandle.unwrap())(&mut handle, &mut params) };
449 if st != NVFBC_SUCCESS {
450 return Err(format!("NvFBCCreateHandle failed ({st})"));
451 }
452 Ok(Self {
453 _lib: lib,
454 funcs,
455 handle,
456 capture_open: false,
457 region: NVFBC_BOX::default(),
458 size: NVFBC_SIZE::default(),
459 with_cursor: false,
460 })
461 }
462
463 fn status(&self) -> Result<NVFBC_GET_STATUS_PARAMS, NvfbcError> {
465 let mut params = NVFBC_GET_STATUS_PARAMS {
466 dwVersion: struct_ver(std::mem::size_of::<NVFBC_GET_STATUS_PARAMS>(), 2),
467 ..Default::default()
468 };
469 let st = unsafe { (self.funcs.nvFBCGetStatus.unwrap())(self.handle, &mut params) };
470 if st != NVFBC_SUCCESS {
471 return Err(self.fail("NvFBCGetStatus", st));
472 }
473 Ok(params)
474 }
475
476 fn start(
486 &mut self,
487 region: NVFBC_BOX,
488 size: NVFBC_SIZE,
489 with_cursor: bool,
490 ) -> Result<(), NvfbcError> {
491 self.stop();
492 let mut params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS {
493 dwVersion: struct_ver(
494 std::mem::size_of::<NVFBC_CREATE_CAPTURE_SESSION_PARAMS>(),
495 6,
496 ),
497 eCaptureType: NVFBC_CAPTURE_SHARED_CUDA,
498 eTrackingType: NVFBC_TRACKING_SCREEN,
499 dwOutputId: 0,
500 captureBox: region,
501 frameSize: size,
502 bWithCursor: if with_cursor { NVFBC_TRUE } else { NVFBC_FALSE },
503 bDisableAutoModesetRecovery: NVFBC_FALSE,
504 bRoundFrameSize: NVFBC_FALSE,
505 dwSamplingRateMs: 0,
506 bPushModel: NVFBC_TRUE,
507 bAllowDirectCapture: NVFBC_TRUE,
508 };
509 let st =
510 unsafe { (self.funcs.nvFBCCreateCaptureSession.unwrap())(self.handle, &mut params) };
511 if st != NVFBC_SUCCESS {
512 return Err(self.fail("NvFBCCreateCaptureSession", st));
513 }
514 self.capture_open = true;
515 let mut setup = NVFBC_TOCUDA_SETUP_PARAMS {
516 dwVersion: struct_ver(std::mem::size_of::<NVFBC_TOCUDA_SETUP_PARAMS>(), 1),
517 eBufferFormat: NVFBC_BUFFER_FORMAT_BGRA,
518 };
519 let st = unsafe { (self.funcs.nvFBCToCudaSetUp.unwrap())(self.handle, &mut setup) };
520 if st != NVFBC_SUCCESS {
521 let msg = self.fail("NvFBCToCudaSetUp", st);
522 self.stop();
523 return Err(msg);
524 }
525 self.region = region;
526 self.size = size;
527 self.with_cursor = with_cursor;
528 Ok(())
529 }
530
531 fn stop(&mut self) {
533 if !self.capture_open {
534 return;
535 }
536 let mut params = NVFBC_VERSION_ONLY_PARAMS {
537 dwVersion: struct_ver(std::mem::size_of::<NVFBC_VERSION_ONLY_PARAMS>(), 1),
538 };
539 if let Some(f) = self.funcs.nvFBCDestroyCaptureSession {
540 unsafe { f(self.handle, &mut params) };
541 }
542 self.capture_open = false;
543 }
544
545 fn grab(&mut self, timeout: Duration) -> Result<GrabbedFrame, NvfbcError> {
552 self.grab_with(NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY, timeout)
553 }
554
555 fn grab_with(&mut self, flags: u32, timeout: Duration) -> Result<GrabbedFrame, NvfbcError> {
557 let mut device_ptr: CUdeviceptr = 0;
558 let mut info = NVFBC_FRAME_GRAB_INFO::default();
559 let mut params = NVFBC_TOCUDA_GRAB_FRAME_PARAMS {
560 dwVersion: struct_ver(std::mem::size_of::<NVFBC_TOCUDA_GRAB_FRAME_PARAMS>(), 2),
561 dwFlags: flags,
562 pCUDADeviceBuffer: &mut device_ptr as *mut CUdeviceptr as *mut c_void,
563 pFrameGrabInfo: &mut info,
564 dwTimeoutMs: timeout.as_millis().clamp(1, u32::MAX as u128) as u32,
565 };
566 let st = unsafe { (self.funcs.nvFBCToCudaGrabFrame.unwrap())(self.handle, &mut params) };
567 if st != NVFBC_SUCCESS {
568 return Err(self.fail("NvFBCToCudaGrabFrame", st));
569 }
570 Ok(GrabbedFrame {
571 device_ptr,
572 width: info.dwWidth,
573 height: info.dwHeight,
574 byte_size: info.dwByteSize,
575 is_new: info.bIsNewFrame == NVFBC_TRUE,
576 direct: info.bDirectCapture == NVFBC_TRUE,
577 post_processed: info.bRequiredPostProcessing == NVFBC_TRUE,
578 })
579 }
580}
581
582impl Drop for NvfbcSession {
583 fn drop(&mut self) {
584 self.stop();
585 let mut params = NVFBC_VERSION_ONLY_PARAMS {
586 dwVersion: struct_ver(std::mem::size_of::<NVFBC_VERSION_ONLY_PARAMS>(), 1),
587 };
588 if let Some(f) = self.funcs.nvFBCDestroyHandle {
589 unsafe { f(self.handle, &mut params) };
590 }
591 }
592}
593
594#[derive(Clone, Copy, PartialEq, Eq)]
596enum Recovery {
597 Session,
599 Handle,
603 None,
605}
606
607fn recovery_for(err: &NvfbcError) -> Recovery {
608 match err.status {
609 NVFBC_ERR_MUST_RECREATE => Recovery::Session,
610 NVFBC_ERR_X => Recovery::Handle,
611 _ => Recovery::None,
612 }
613}
614
615fn resolve_region(screen: NVFBC_SIZE, s: &RustCaptureSettings) -> (NVFBC_BOX, NVFBC_SIZE) {
623 let (w, h) = super::resolve_dims(
624 screen.w.min(u16::MAX as u32) as u16,
625 screen.h.min(u16::MAX as u32) as u16,
626 s,
627 );
628 let x = super::clamp_offset(s.capture_x, screen.w.min(u16::MAX as u32) as u16).max(0) as u32;
629 let y = super::clamp_offset(s.capture_y, screen.h.min(u16::MAX as u32) as u16).max(0) as u32;
630 let size = NVFBC_SIZE {
631 w: w as u32,
632 h: h as u32,
633 };
634 let full = x == 0 && y == 0 && size.w == screen.w && size.h == screen.h;
639 let region = if full {
640 NVFBC_BOX::default()
641 } else {
642 NVFBC_BOX {
643 x,
644 y,
645 w: size.w,
646 h: size.h,
647 }
648 };
649 (region, size)
650}
651
652struct GpuCapture {
655 nvfbc: NvfbcSession,
656 encoder: NvencEncoder,
657 settings: RustCaptureSettings,
660 request: RustCaptureSettings,
664 screen: NVFBC_SIZE,
665}
666
667const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
669
670fn answered_within<T: Send + 'static>(
674 timeout: Duration,
675 what: &str,
676 f: impl FnOnce() -> Result<T, String> + Send + 'static,
677) -> Result<T, String> {
678 let (tx, rx) = std::sync::mpsc::channel();
679 std::thread::Builder::new()
680 .name(format!("pf-{}-probe", what.to_lowercase()))
681 .spawn(move || {
682 let _ = tx.send(f());
683 })
684 .map_err(|e| format!("{what} probe thread: {e}"))?;
685 match rx.recv_timeout(timeout) {
686 Ok(answer) => answer,
687 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(format!(
688 "{what} did not answer within {} s",
689 timeout.as_secs()
690 )),
691 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
692 Err(format!("{what} ended without an answer"))
693 }
694 }
695}
696
697const NVIDIA_GLX: &str = "NV-GLX";
703
704fn nvidia_driven_x_server() -> Result<bool, String> {
708 let (conn, _) = x11rb::connect(None).map_err(|e| format!("X11 connect failed: {e}"))?;
709 let cookie = conn
710 .query_extension(NVIDIA_GLX.as_bytes())
711 .map_err(|e| format!("{NVIDIA_GLX} query failed: {e}"))?;
712 let reply = cookie
713 .reply()
714 .map_err(|e| format!("{NVIDIA_GLX} query failed: {e}"))?;
715 Ok(reply.present)
716}
717
718fn foreign_libxcb() -> Option<String> {
730 unsafe {
731 let x11 = libc::dlopen(c"libX11.so.6".as_ptr(), libc::RTLD_LAZY | libc::RTLD_NOLOAD);
732 if x11.is_null() {
733 return None;
734 }
735 let open = libc::dlsym(x11, c"XOpenDisplay".as_ptr());
736 if !open.is_null() {
737 let open: unsafe extern "C" fn(*const c_char) -> *mut c_void =
738 std::mem::transmute(open);
739 open(c"pixelflux".as_ptr());
740 }
741 libc::dlclose(x11);
742 let bound = crate::nvgpufilter::bound_import("libX11.so", b"xcb_parse_display")?;
743 let xcb = libc::dlopen(c"libxcb.so.1".as_ptr(), libc::RTLD_LAZY | libc::RTLD_NOLOAD);
744 if xcb.is_null() {
745 return None;
746 }
747 let host = libc::dlsym(xcb, c"xcb_parse_display".as_ptr());
748 libc::dlclose(xcb);
749 let object = |addr: *const c_void| {
750 let mut info: libc::Dl_info = std::mem::zeroed();
751 (libc::dladdr(addr, &mut info) != 0
752 && !info.dli_sname.is_null()
753 && !info.dli_fname.is_null())
754 .then(|| {
755 (
756 info.dli_fbase,
757 CStr::from_ptr(info.dli_sname),
758 CStr::from_ptr(info.dli_fname),
759 )
760 })
761 };
762 let (bound_base, bound_name, bound_path) = object(bound as *const c_void)?;
763 let (host_base, _, host_path) = object(host)?;
764 (bound_name.to_bytes() == b"xcb_parse_display" && bound_base != host_base).then(|| {
765 format!(
766 "libX11 calls {}, not the {} the NVIDIA library calls on the same connection",
767 bound_path.to_string_lossy(),
768 host_path.to_string_lossy()
769 )
770 })
771 }
772}
773
774fn declined(reason: &str) -> Option<GpuCapture> {
776 println!("[X11] Zero-copy capture (NvFBC) unavailable: {reason}.");
777 crate::report::capture_declined("NvFBC", reason);
778 None
779}
780
781fn open(settings: &RustCaptureSettings) -> Option<GpuCapture> {
794 if !settings.codec.is_video() {
795 return declined("the codec is JPEG");
796 }
797 if settings.use_cpu || settings.encode_node_index == -1 {
798 return declined("software encoding was requested");
799 }
800 if !settings.watermark_path.is_empty() {
801 return declined("a watermark is composited into the frame");
802 }
803 let node = settings.encode_node_index.max(0);
804 let driver = crate::get_gpu_driver(node);
805 if !crate::driver_selects_nvenc(&driver) {
806 return declined(&format!("the encode node's driver is {driver}"));
807 }
808 if crate::encoders::ten_bit_is_softwares(settings) {
809 return declined(&format!(
810 "NVENC encodes no 10-bit {}, which the software encoder does",
811 settings.codec.display()
812 ));
813 }
814 if let Some(reason) = foreign_libxcb() {
815 return declined(&reason);
816 }
817 let probed = answered_within(PROBE_TIMEOUT, "NvFBC", || {
822 let nvfbc = NvfbcSession::open()?;
823 let status = nvfbc.status().map_err(|e| e.to_string())?;
824 if status.bIsCapturePossible != NVFBC_TRUE {
825 return Err("the driver reports capture is not possible on this X server".into());
826 }
827 if status.bInModeset == NVFBC_TRUE {
828 return Err("the X server is in a modeset".into());
829 }
830 Ok(status.screenSize)
831 });
832 let screen = match probed {
833 Ok(v) => v,
834 Err(e) => return declined(&e),
835 };
836 crate::report::zero_copy_available(true);
837 let mut nvfbc = match NvfbcSession::open() {
838 Ok(v) => v,
839 Err(e) => return declined(&e),
840 };
841
842 let (region, size) = resolve_region(screen, settings);
846 let request = settings.clone();
847 let mut settings = settings.clone();
848 settings.width = size.w as i32;
849 settings.height = size.h as i32;
850
851 let mut encoder = match NvencEncoder::new(&settings, std::ptr::null()) {
852 Ok(enc) => enc,
853 Err(e) => {
854 return declined(&format!(
855 "NVENC {} did not open: {e}",
856 settings.codec.display()
857 ));
858 }
859 };
860 if !encoder.push_context() {
863 return declined("the encoder's CUDA context could not be made current");
864 }
865 if let Err(e) = nvfbc.start(region, size, settings.capture_cursor) {
866 encoder.pop_context();
867 return declined(&e.to_string());
868 }
869
870 settings.width = nvfbc.size.w as i32;
871 settings.height = nvfbc.size.h as i32;
872 if (settings.width != encoder.width() as i32 || settings.height != encoder.height() as i32)
873 && let Err(e) = encoder.reconfigure_resolution(&settings)
874 {
875 nvfbc.stop();
876 encoder.pop_context();
877 return declined(&format!("NVENC could not follow the captured size: {e}"));
878 }
879 println!(
880 "[X11] Zero-copy capture (NvFBC): {}x{} composited into video memory, encoded in place on NVENC.",
881 settings.width, settings.height
882 );
883 crate::report::capture("NvFBC", true);
884 crate::report::hardware_encoder(
885 encoder.device_name(),
886 crate::encoders::driver_name(&driver),
887 node,
888 );
889 crate::log_stream_settings_of(
890 "X11",
891 &settings,
892 1,
893 Some(("NVENC", true)),
894 None,
895 true,
896 encoder.is_fullcolor(),
897 false,
898 );
899 crate::report::bit_depth(encoder.bit_depth());
900 Some(GpuCapture {
901 nvfbc,
902 encoder,
903 settings,
904 request,
905 screen,
906 })
907}
908
909impl Drop for GpuCapture {
910 fn drop(&mut self) {
914 self.encoder.release_external_input();
915 self.nvfbc.stop();
916 self.encoder.pop_context();
917 }
918}
919
920const GEOMETRY_POLL_FRAMES: i32 = 30;
922
923pub fn run_capture<F>(
947 settings: RustCaptureSettings,
948 controls: Arc<Controls>,
949 encode_tid_tx: std::sync::mpsc::Sender<std::thread::ThreadId>,
950 on_frame: &mut F,
951) -> Option<Result<(), String>>
952where
953 F: FnMut(Vec<EncodedStripe>),
954{
955 let mut gpu = open(&settings)?;
956 let _ = encode_tid_tx.send(std::thread::current().id());
959 controls
960 .codec
961 .store(gpu.encoder.codec().id(), Ordering::Relaxed);
962
963 let recording_sink = RecordingSink::try_bind(&settings.recording_socket, settings.target_fps);
964 let mut state = StripeState::default();
965 let mut hashes: Vec<u64> = Vec::new();
966 let mut lagged = Damage::None;
967 let mut frame_counter: u16 = 0;
968 let mut pending_force_idr = false;
969 let mut next_frame = Instant::now();
970 let mut geometry_check = GEOMETRY_POLL_FRAMES;
971 let mut grab_errors = 0u32;
972 let mut encode_errors = 0u32;
973 let mut last_log = Instant::now();
974 let mut sent_frames: u64 = 0;
975 let mut new_frames: u64 = 0;
976 let mut direct_frames: u64 = 0;
977
978 while !controls.stop.load(Ordering::Relaxed) {
979 let fps = f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
980 let frame_dur = Duration::from_secs_f64(1.0 / fps.max(1.0));
981 let now = Instant::now();
982 if now < next_frame {
983 std::thread::sleep(next_frame - now);
984 }
985 next_frame += frame_dur;
986 let now = Instant::now();
987 if next_frame < now {
988 next_frame = now;
989 }
990 if controls.stop.load(Ordering::Relaxed) {
991 break;
992 }
993
994 for report in std::mem::take(&mut *controls.reference_reports.lock().unwrap()) {
995 if !gpu.encoder.take_report(report) {
996 pending_force_idr = true;
997 }
998 }
999 if controls.force_idr.swap(false, Ordering::Relaxed)
1000 || recording_sink
1001 .as_ref()
1002 .is_some_and(|s| s.should_force_idr())
1003 {
1004 pending_force_idr = true;
1005 }
1006 if controls.rate_dirty.swap(false, Ordering::Acquire) {
1007 gpu.settings.video_bitrate_kbps = controls.bitrate_kbps.load(Ordering::Relaxed);
1008 gpu.settings.video_vbv_multiplier =
1009 controls.vbv_mult_milli.load(Ordering::Relaxed) as f64 / 1000.0;
1010 gpu.settings.target_fps = fps;
1011 gpu.encoder.reconfigure_rate(&gpu.settings);
1012 }
1013 if controls.tunables_dirty.swap(false, Ordering::Acquire)
1014 && let Some(t) = controls.tunables.lock().unwrap().take()
1015 {
1016 t.apply_to(&mut gpu.settings);
1017 }
1018
1019 let want_cursor = controls.capture_cursor.load(Ordering::Relaxed);
1020 let mut recheck_geometry = controls.region_dirty.swap(false, Ordering::Acquire);
1021 if recheck_geometry {
1022 let (nx, ny, nw, nh) = *controls.region.lock().unwrap();
1023 gpu.request.capture_x = nx;
1024 gpu.request.capture_y = ny;
1025 gpu.request.width = nw;
1026 gpu.request.height = nh;
1027 gpu.request.auto_adjust_screen_capture_size = nw <= 0 || nh <= 0;
1029 }
1030 geometry_check -= 1;
1031 if geometry_check <= 0 {
1032 geometry_check = GEOMETRY_POLL_FRAMES;
1033 if let Ok(status) = gpu.nvfbc.status() {
1034 recheck_geometry |= status.screenSize != gpu.screen;
1035 gpu.screen = status.screenSize;
1036 }
1037 }
1038 if recheck_geometry || want_cursor != gpu.nvfbc.with_cursor {
1039 let (region, size) = resolve_region(gpu.screen, &gpu.request);
1040 if region != gpu.nvfbc.region
1041 || size != gpu.nvfbc.size
1042 || want_cursor != gpu.nvfbc.with_cursor
1043 {
1044 match restart_session(&mut gpu, region, size, want_cursor) {
1045 Ok(()) => {
1046 state = StripeState::default();
1047 hashes.clear();
1048 lagged = Damage::None;
1049 pending_force_idr = true;
1050 }
1051 Err(e) => {
1052 return Some(Err(format!("NvFBC could not follow the new geometry: {e}")));
1053 }
1054 }
1055 }
1056 }
1057
1058 let frame = match gpu.nvfbc.grab(frame_dur) {
1059 Ok(f) => {
1060 grab_errors = 0;
1061 f
1062 }
1063 Err(e) => {
1064 grab_errors += 1;
1065 let recovery = recovery_for(&e);
1066 if recovery == Recovery::None || grab_errors > 5 {
1067 return Some(Err(format!("NvFBC capture ended: {e}")));
1068 }
1069 let (region, size) = resolve_region(gpu.screen, &gpu.request);
1070 let rebuilt = if recovery == Recovery::Handle {
1071 eprintln!("[X11] NvFBC lost the X server ({e}); rebuilding the client handle.");
1072 rebuild_handle(&mut gpu, region, size, want_cursor)
1073 } else {
1074 eprintln!("[X11] NvFBC grab failed ({e}); rebuilding the capture session.");
1075 restart_session(&mut gpu, region, size, want_cursor).map_err(|e| e.to_string())
1076 };
1077 if let Err(e) = rebuilt {
1078 return Some(Err(format!("NvFBC capture could not be rebuilt: {e}")));
1079 }
1080 state = StripeState::default();
1081 hashes.clear();
1082 lagged = Damage::None;
1083 pending_force_idr = true;
1084 continue;
1085 }
1086 };
1087 let grabbed_ns = crate::wayland::host::now_ns();
1088 if frame.device_ptr == 0 || frame.width == 0 || frame.height == 0 {
1089 continue;
1090 }
1091 new_frames += frame.is_new as u64;
1092 direct_frames += frame.direct as u64;
1093
1094 if frame.width != gpu.encoder.width() || frame.height != gpu.encoder.height() {
1095 gpu.settings.width = frame.width as i32;
1096 gpu.settings.height = frame.height as i32;
1097 if let Err(e) = gpu.encoder.reconfigure_resolution(&gpu.settings) {
1098 return Some(Err(format!(
1099 "NVENC could not follow the captured size: {e}"
1100 )));
1101 }
1102 state = StripeState::default();
1103 hashes.clear();
1104 lagged = Damage::None;
1105 pending_force_idr = true;
1106 }
1107
1108 let pitch = frame_pitch(frame.byte_size, frame.width, frame.height);
1109 let hash = |encoder: &mut NvencEncoder, hashes: &mut Vec<u64>| {
1110 encoder
1111 .band_hashes(
1112 frame.device_ptr,
1113 pitch,
1114 frame.width,
1115 frame.height,
1116 DAMAGE_BAND_ROWS as u32,
1117 )
1118 .map_or(Damage::Unknown, |h| hashed_damage(hashes, h))
1119 };
1120 let lags = gpu.settings.video_streaming_mode && gpu.settings.use_paint_over_quality;
1129 let damage = if lags {
1130 std::mem::replace(&mut lagged, Damage::None)
1131 } else if !frame.is_new {
1132 Damage::None
1133 } else if gpu.settings.video_streaming_mode || !gpu.settings.use_paint_over_quality {
1134 hashes.clear();
1135 Damage::Unknown
1136 } else {
1137 hash(&mut gpu.encoder, &mut hashes)
1138 };
1139 let mut decision = decide_hw_fullframe(
1140 &mut state,
1141 &gpu.settings,
1142 frame_counter,
1143 damage,
1144 false,
1145 pending_force_idr,
1146 EncoderQuality {
1147 last: gpu.encoder.last_quality(),
1148 bytes: gpu.encoder.last_size(),
1149 holds: true,
1150 reopens: false,
1151 keys: true,
1152 band: gpu.encoder.band_size(),
1153 measures: false,
1154 psnr: None,
1155 },
1156 );
1157 let normal = gpu.settings.video_crf as u32;
1158 let mut extent = None;
1159 if lags && frame.is_new && decision.send && decision.cleanup_quality(normal).is_some() {
1160 let d = hash(&mut gpu.encoder, &mut hashes);
1161 if d.is_motion() {
1162 decision.hold_qp = None;
1163 decision.target_qp = normal;
1164 decision.force_idr =
1165 pending_force_idr || periodic_idr_due(&gpu.settings, frame_counter);
1166 }
1167 extent = Some(d);
1168 }
1169 let mut delivered = false;
1170 if decision.send {
1171 let encode_start_ns = crate::wayland::host::now_ns();
1172 if let Some(q) = decision.hold_qp {
1173 gpu.encoder.hold_quantizer(q, decision.hold_band);
1174 }
1175 match gpu.encoder.encode_cuda_pitch(
1176 frame.device_ptr,
1177 pitch,
1178 false,
1179 frame_counter as u64,
1180 decision.target_qp,
1181 decision.force_idr,
1182 ) {
1183 Ok(data) if !data.is_empty() => {
1184 encode_errors = 0;
1185 let stripes = vec![EncodedStripe {
1186 data: Arc::new(data),
1187 codec: gpu.settings.codec,
1188 stripe_y_start: 0,
1189 stripe_height: gpu.settings.height,
1190 frame_id: frame_counter as i32,
1191 timing: FrameTiming {
1192 capture_ns: grabbed_ns,
1193 encode_start_ns,
1194 encode_end_ns: crate::wayland::host::now_ns(),
1195 },
1196 reference: gpu.encoder.last_reference(),
1197 }];
1198 if let Some(sink) = &recording_sink {
1199 sink.write_frame(&stripes, gpu.settings.width, gpu.settings.height);
1200 }
1201 sent_frames += 1;
1202 delivered = true;
1203 on_frame(stripes);
1204 }
1205 Ok(_) => {}
1206 Err(e) => {
1207 encode_errors += 1;
1209 if encode_errors % crate::HW_ERROR_RECOVERY_THRESHOLD == 1 {
1210 eprintln!("[X11] NVENC encode error on the zero-copy path: {e}");
1211 }
1212 if encode_errors >= crate::HW_ERROR_RECOVERY_THRESHOLD {
1213 return Some(Err(
1214 "NVENC failed repeatedly on the zero-copy path".to_string()
1215 ));
1216 }
1217 }
1218 }
1219 }
1220 if lags {
1221 lagged = match extent {
1222 Some(d) => d,
1223 None if frame.is_new => hash(&mut gpu.encoder, &mut hashes),
1224 None => Damage::None,
1225 };
1226 }
1227 pending_force_idr = (pending_force_idr || decision.force_idr) && !delivered;
1230 frame_counter = frame_counter.wrapping_add(1);
1231
1232 let elapsed = last_log.elapsed().as_secs_f64();
1233 if elapsed >= 1.0 {
1234 crate::log::debug!(
1235 "[X11] NvFBC {}x{} Encoder: NVENC EncFPS: {:.2} NewFrames/s: {:.2} Direct: {}",
1236 gpu.settings.width,
1237 gpu.settings.height,
1238 sent_frames as f64 / elapsed,
1239 new_frames as f64 / elapsed,
1240 direct_frames > 0
1241 );
1242 sent_frames = 0;
1243 new_frames = 0;
1244 direct_frames = 0;
1245 last_log = Instant::now();
1246 }
1247 }
1248 Some(Ok(()))
1249}
1250
1251fn restart_session(
1255 gpu: &mut GpuCapture,
1256 region: NVFBC_BOX,
1257 size: NVFBC_SIZE,
1258 with_cursor: bool,
1259) -> Result<(), NvfbcError> {
1260 gpu.encoder.release_external_input();
1263 gpu.nvfbc.start(region, size, with_cursor)?;
1264 gpu.settings.width = size.w as i32;
1265 gpu.settings.height = size.h as i32;
1266 if gpu.settings.width != gpu.encoder.width() as i32
1267 || gpu.settings.height != gpu.encoder.height() as i32
1268 {
1269 gpu.encoder
1270 .reconfigure_resolution(&gpu.settings)
1271 .map_err(|e| NvfbcError {
1272 status: NVFBC_SUCCESS,
1273 message: e,
1274 })?;
1275 }
1276 Ok(())
1277}
1278
1279fn rebuild_handle(
1286 gpu: &mut GpuCapture,
1287 region: NVFBC_BOX,
1288 size: NVFBC_SIZE,
1289 with_cursor: bool,
1290) -> Result<(), String> {
1291 gpu.encoder.release_external_input();
1292 gpu.nvfbc.stop();
1293 gpu.nvfbc = NvfbcSession::open()?;
1294 gpu.screen = gpu.nvfbc.status().map_err(|e| e.to_string())?.screenSize;
1295 let (region, size) = if region == NVFBC_BOX::default() {
1296 resolve_region(gpu.screen, &gpu.request)
1297 } else {
1298 (region, size)
1299 };
1300 restart_session(gpu, region, size, with_cursor).map_err(|e| e.to_string())
1301}
1302
1303fn frame_pitch(byte_size: u32, width: u32, height: u32) -> usize {
1306 let packed = width as usize * 4;
1307 if height == 0 {
1308 return packed;
1309 }
1310 let rows = byte_size as usize / height as usize;
1311 if rows >= packed && rows.is_multiple_of(4) && rows * height as usize == byte_size as usize {
1312 rows
1313 } else {
1314 packed
1315 }
1316}
1317
1318#[cfg(test)]
1319mod abi_tests {
1320 use super::*;
1321
1322 #[test]
1328 fn probe_answers_are_bounded() {
1329 let stuck = answered_within(
1330 Duration::from_millis(50),
1331 "Stuck",
1332 || -> Result<u32, String> {
1333 std::thread::sleep(Duration::from_millis(400));
1334 Ok(1)
1335 },
1336 );
1337 assert_eq!(stuck, Err("Stuck did not answer within 0 s".to_string()));
1338 assert_eq!(
1339 answered_within(Duration::from_secs(1), "Quick", || Ok(7u32)),
1340 Ok(7)
1341 );
1342 assert_eq!(
1343 answered_within(
1344 Duration::from_secs(1),
1345 "Refused",
1346 || -> Result<u32, String> { Err("no".into()) }
1347 ),
1348 Err("no".to_string())
1349 );
1350 }
1351
1352 #[test]
1356 fn the_nvidia_gate_is_answered_by_the_server() {
1357 let answered = answered_within(Duration::from_secs(5), "Gate", || {
1358 Ok(nvidia_driven_x_server())
1359 })
1360 .expect("the NV-GLX gate must answer");
1361 let Ok(present) = answered else { return };
1362 let (conn, _) = x11rb::connect(None).expect("a server that answered once answers again");
1363 let listed = conn
1364 .list_extensions()
1365 .expect("the server takes the request")
1366 .reply()
1367 .expect("the server lists its extensions")
1368 .names
1369 .iter()
1370 .any(|name| name.name == NVIDIA_GLX.as_bytes());
1371 assert_eq!(present, listed);
1372 }
1373
1374 #[test]
1378 fn the_host_libx11_calls_the_host_libxcb() {
1379 let x11 = unsafe { libc::dlopen(c"libX11.so.6".as_ptr(), libc::RTLD_LAZY) };
1380 assert!(!x11.is_null(), "libX11.so.6 does not load here");
1381 assert_eq!(foreign_libxcb(), None);
1382 let bound = crate::nvgpufilter::bound_import("libX11.so", b"xcb_parse_display")
1383 .expect("libX11 imports xcb_parse_display");
1384 let mut info: libc::Dl_info = unsafe { std::mem::zeroed() };
1385 assert_ne!(
1386 unsafe { libc::dladdr(bound as *const c_void, &mut info) },
1387 0
1388 );
1389 assert!(!info.dli_sname.is_null(), "the slot names no symbol");
1390 assert_eq!(
1391 unsafe { CStr::from_ptr(info.dli_sname) }.to_bytes(),
1392 b"xcb_parse_display"
1393 );
1394 unsafe { libc::dlclose(x11) };
1395 }
1396
1397 #[test]
1398 fn struct_layouts_match_the_sdk() {
1399 assert_eq!(std::mem::size_of::<NVFBC_BOX>(), 16);
1400 assert_eq!(std::mem::size_of::<NVFBC_SIZE>(), 8);
1401 assert_eq!(std::mem::size_of::<NVFBC_FRAME_GRAB_INFO>(), 48);
1402 assert_eq!(std::mem::size_of::<NVFBC_CREATE_HANDLE_PARAMS>(), 40);
1403 assert_eq!(std::mem::size_of::<NVFBC_VERSION_ONLY_PARAMS>(), 4);
1404 assert_eq!(std::mem::size_of::<NVFBC_RANDR_OUTPUT_INFO>(), 148);
1405 assert_eq!(std::mem::size_of::<NVFBC_GET_STATUS_PARAMS>(), 780);
1406 assert_eq!(
1407 std::mem::size_of::<NVFBC_CREATE_CAPTURE_SESSION_PARAMS>(),
1408 64
1409 );
1410 assert_eq!(std::mem::size_of::<NVFBC_TOCUDA_SETUP_PARAMS>(), 8);
1411 assert_eq!(std::mem::size_of::<NVFBC_TOCUDA_GRAB_FRAME_PARAMS>(), 32);
1412 assert_eq!(std::mem::size_of::<NVFBC_API_FUNCTION_LIST>(), 176);
1413 }
1414
1415 #[test]
1418 fn version_words_match_the_sdk() {
1419 assert_eq!(NVFBC_VERSION, 0x108);
1420 assert_eq!(struct_ver(40, 2), 0x0802_0028);
1421 assert_eq!(struct_ver(4, 1), 0x0801_0004);
1422 assert_eq!(struct_ver(780, 2), 0x0802_030C);
1423 assert_eq!(struct_ver(64, 6), 0x0806_0040);
1424 assert_eq!(struct_ver(8, 1), 0x0801_0008);
1425 assert_eq!(struct_ver(32, 2), 0x0802_0020);
1426 }
1427}
1428
1429#[cfg(test)]
1430mod region_tests {
1431 use super::*;
1432 use crate::encoders::codec::Codec;
1433
1434 fn settings(w: i32, h: i32) -> RustCaptureSettings {
1435 RustCaptureSettings {
1436 width: w,
1437 height: h,
1438 codec: Codec::H264,
1439 ..Default::default()
1440 }
1441 }
1442
1443 #[test]
1446 fn whole_screen_is_an_empty_box() {
1447 let screen = NVFBC_SIZE { w: 1920, h: 1080 };
1448 let mut s = settings(1920, 1080);
1449 let (region, size) = resolve_region(screen, &s);
1450 assert_eq!(region, NVFBC_BOX::default());
1451 assert_eq!(size, NVFBC_SIZE { w: 1920, h: 1080 });
1452
1453 s.auto_adjust_screen_capture_size = true;
1454 let (region, size) = resolve_region(screen, &s);
1455 assert_eq!(region, NVFBC_BOX::default());
1456 assert_eq!(size, NVFBC_SIZE { w: 1920, h: 1080 });
1457
1458 let mut region_only = settings(1280, 720);
1459 region_only.capture_x = 100;
1460 region_only.capture_y = 50;
1461 let (region, size) = resolve_region(screen, ®ion_only);
1462 assert_eq!(
1463 region,
1464 NVFBC_BOX {
1465 x: 100,
1466 y: 50,
1467 w: 1280,
1468 h: 720
1469 }
1470 );
1471 assert_eq!(size, NVFBC_SIZE { w: 1280, h: 720 });
1472 }
1473
1474 #[test]
1481 fn region_is_even_and_inside_the_screen() {
1482 let screen = NVFBC_SIZE { w: 1919, h: 1081 };
1483 let (region, size) = resolve_region(screen, &settings(0, 0));
1484 assert_eq!(size, NVFBC_SIZE { w: 1918, h: 1080 });
1485 assert_eq!(
1486 region,
1487 NVFBC_BOX {
1488 x: 0,
1489 y: 0,
1490 w: 1918,
1491 h: 1080
1492 }
1493 );
1494
1495 let mut s = settings(1920, 1080);
1496 s.capture_x = 1000;
1497 s.capture_y = 800;
1498 let (region, size) = resolve_region(NVFBC_SIZE { w: 1920, h: 1080 }, &s);
1499 assert_eq!(region.x, 1000);
1500 assert_eq!(region.y, 800);
1501 assert_eq!(size, NVFBC_SIZE { w: 920, h: 280 });
1502 assert_eq!(region.w, size.w);
1503 assert_eq!(region.h, size.h);
1504 }
1505
1506 #[test]
1511 fn recovery_follows_the_status() {
1512 let session = |status| NvfbcError {
1513 status,
1514 message: String::new(),
1515 };
1516 assert!(matches!(
1517 recovery_for(&session(NVFBC_ERR_MUST_RECREATE)),
1518 Recovery::Session
1519 ));
1520 assert!(matches!(
1521 recovery_for(&session(NVFBC_ERR_X)),
1522 Recovery::Handle
1523 ));
1524 assert!(matches!(recovery_for(&session(1)), Recovery::None));
1525 let misleading = NvfbcError {
1526 status: 1,
1527 message: "the display (16) went away (10)".into(),
1528 };
1529 assert!(matches!(recovery_for(&misleading), Recovery::None));
1530 }
1531
1532 #[test]
1535 fn pitch_follows_the_reported_byte_size() {
1536 assert_eq!(frame_pitch(1920 * 1080 * 4, 1920, 1080), 1920 * 4);
1537 assert_eq!(frame_pitch(2048 * 4 * 1080, 1920, 1080), 2048 * 4);
1538 assert_eq!(frame_pitch(12345, 1920, 1080), 1920 * 4);
1539 assert_eq!(frame_pitch(0, 1920, 0), 1920 * 4);
1540 }
1541}
1542
1543#[cfg(test)]
1544mod gpu_tests {
1545 use super::super::gpu_test_support::{decoded_mean, paint_root, painted_ycbcr, settings};
1546 use super::*;
1547 use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, VIDEO_HEADER_LEN, parse_video_type};
1548 use crate::webcam::decode::{Codec as DecCodec, VideoDecoder};
1549
1550 fn thread_cpu() -> Duration {
1552 let mut ts = libc::timespec {
1553 tv_sec: 0,
1554 tv_nsec: 0,
1555 };
1556 unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) };
1557 Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
1558 }
1559
1560 #[test]
1565 #[ignore]
1566 fn gpu_nvfbc_opens_a_session_that_follows_the_screen() {
1567 let mut s = settings(crate::encoders::codec::Codec::H264);
1568 s.width = 0;
1569 s.height = 0;
1570 s.auto_adjust_screen_capture_size = true;
1571 let Some(gpu) = open(&s) else {
1572 println!("the NvFBC path declined this host; nothing to open");
1573 return;
1574 };
1575 assert!(
1576 gpu.settings.width > 0 && gpu.settings.height > 0,
1577 "a session following the screen opened at {}x{}",
1578 gpu.settings.width,
1579 gpu.settings.height
1580 );
1581 println!(
1582 "a session following the screen opened at {}x{}",
1583 gpu.settings.width, gpu.settings.height
1584 );
1585 }
1586
1587 #[test]
1591 #[ignore]
1592 fn gpu_nvfbc_status_reports_capture() {
1593 let Ok(nvfbc) = NvfbcSession::open() else {
1594 println!("NvFBC handed out no capture handle on this host; nothing to report");
1595 return;
1596 };
1597 let status = nvfbc.status().expect("NvFBC status");
1598 println!(
1599 "capture possible={} can create now={} screen={}x{} outputs={} nvfbc version={} in modeset={}",
1600 status.bIsCapturePossible,
1601 status.bCanCreateNow,
1602 status.screenSize.w,
1603 status.screenSize.h,
1604 status.dwOutputNum,
1605 status.dwNvFBCVersion,
1606 status.bInModeset
1607 );
1608 assert_eq!(
1609 status.bIsCapturePossible, NVFBC_TRUE,
1610 "this GPU offers no NvFBC"
1611 );
1612 assert!(status.screenSize.w > 0 && status.screenSize.h > 0);
1613 }
1614
1615 fn captured_luma(gpu: &mut GpuCapture, dec: &mut VideoDecoder, n: &mut u64) -> f64 {
1618 let mut frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1619 for _ in 0..10 {
1620 if frame.is_new {
1621 break;
1622 }
1623 frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1624 }
1625 let pitch = frame_pitch(frame.byte_size, frame.width, frame.height);
1626 let pkt = gpu
1627 .encoder
1628 .encode_cuda_pitch(frame.device_ptr, pitch, false, *n, 25, *n == 0)
1629 .expect("encode in place");
1630 *n += 1;
1631 decoded_mean(dec, &pkt[VIDEO_HEADER_LEN..])[0]
1632 }
1633
1634 #[test]
1642 #[ignore]
1643 fn gpu_nvfbc_two_sessions_on_one_screen() {
1644 const FIRST: (u8, u8, u8) = (0x20, 0x40, 0xc0);
1645 const SECOND: (u8, u8, u8) = (0xd0, 0x50, 0x18);
1646 const THIRD: (u8, u8, u8) = (0x30, 0xc0, 0x30);
1647 if !paint_root(FIRST) {
1648 println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1649 return;
1650 }
1651 let Some(mut first) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1652 println!("the NvFBC path declined the first capture on this host; nothing to compare");
1653 return;
1654 };
1655 let (go, wait) = std::sync::mpsc::channel::<()>();
1656 let (tell, seen) = std::sync::mpsc::channel::<Option<f64>>();
1657 let second = std::thread::spawn(move || {
1658 let Some(mut gpu) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1659 let _ = tell.send(None);
1660 return;
1661 };
1662 let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1663 let mut n = 0;
1664 let _ = tell.send(Some(captured_luma(&mut gpu, &mut dec, &mut n)));
1665 while wait.recv().is_ok() {
1666 let _ = tell.send(Some(captured_luma(&mut gpu, &mut dec, &mut n)));
1667 }
1668 });
1669 let opened = seen.recv().expect("the second capture's thread").is_some();
1670 println!(
1671 "a second concurrent NvFBC capture of the same screen: {}",
1672 if opened {
1673 "opens"
1674 } else {
1675 "is declined, and falls back to XShm"
1676 }
1677 );
1678 let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1679 let mut n = 0;
1680 captured_luma(&mut first, &mut dec, &mut n);
1681 assert!(paint_root(SECOND));
1682 let want = painted_ycbcr(SECOND)[0];
1683 let got = captured_luma(&mut first, &mut dec, &mut n);
1684 assert!(
1685 (got - want).abs() <= 8.0,
1686 "the first captured {got:.1} beside the second, painted {want:.1}"
1687 );
1688 if opened {
1689 go.send(()).expect("the second capture's thread");
1690 let got = seen
1691 .recv()
1692 .expect("the second capture's thread")
1693 .expect("a capture");
1694 assert!(
1695 (got - want).abs() <= 8.0,
1696 "the second captured {got:.1}, painted {want:.1}"
1697 );
1698 }
1699 drop(go);
1700 second.join().expect("the second capture's thread");
1701 assert!(paint_root(THIRD));
1702 let want = painted_ycbcr(THIRD)[0];
1703 let got = captured_luma(&mut first, &mut dec, &mut n);
1704 assert!(
1705 (got - want).abs() <= 8.0,
1706 "the first captured {got:.1} once the second closed, painted {want:.1}"
1707 );
1708 }
1709
1710 #[test]
1720 #[ignore]
1721 fn gpu_nvfbc_zero_copy_encodes_the_painted_root() {
1722 const FIRST: (u8, u8, u8) = (0x20, 0x40, 0xc0);
1723 const SECOND: (u8, u8, u8) = (0xd0, 0x50, 0x18);
1724 if !paint_root(FIRST) {
1725 println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1726 return;
1727 }
1728 let Some(mut gpu) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1729 println!("the NvFBC path declined this session on this host; nothing to capture");
1730 return;
1731 };
1732 let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1733 let mut pointers = Vec::new();
1734
1735 let encode =
1736 |gpu: &mut GpuCapture, i: u64, key: bool, pointers: &mut Vec<CUdeviceptr>| -> Vec<u8> {
1737 let mut frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1739 for _ in 0..30 {
1740 if frame.is_new {
1741 break;
1742 }
1743 frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1744 }
1745 assert_eq!(frame.width, gpu.encoder.width());
1746 assert_eq!(frame.height, gpu.encoder.height());
1747 assert!(
1748 !frame.post_processed,
1749 "the native format needed a conversion pass, which is an extra frame copy"
1750 );
1751 pointers.push(frame.device_ptr);
1752 let pitch = frame_pitch(frame.byte_size, frame.width, frame.height);
1753 gpu.encoder
1754 .encode_cuda_pitch(frame.device_ptr, pitch, false, i, 25, key)
1755 .expect("encode in place")
1756 };
1757
1758 let pkt = encode(&mut gpu, 0, true, &mut pointers);
1759 assert_eq!(
1760 parse_video_type(pkt[1]),
1761 Some((crate::encoders::codec::Codec::H264, FRAME_KEY))
1762 );
1763 let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1764 let want = painted_ycbcr(FIRST);
1765 for i in 0..3 {
1766 assert!(
1767 (mean[i] - want[i]).abs() <= 8.0,
1768 "plane {i}: captured {:.1}, painted {:.1}",
1769 mean[i],
1770 want[i]
1771 );
1772 }
1773
1774 for i in 1..4u64 {
1775 let pkt = encode(&mut gpu, i, false, &mut pointers);
1776 assert_eq!(
1777 parse_video_type(pkt[1]),
1778 Some((crate::encoders::codec::Codec::H264, FRAME_DELTA))
1779 );
1780 let _ = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1781 }
1782
1783 assert!(
1784 paint_root(SECOND),
1785 "the root was painted once, so a repaint must work"
1786 );
1787 let pkt = encode(&mut gpu, 4, false, &mut pointers);
1788 let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1789 let want = painted_ycbcr(SECOND);
1790 for i in 0..3 {
1791 assert!(
1792 (mean[i] - want[i]).abs() <= 8.0,
1793 "after repaint, plane {i}: captured {:.1}, painted {:.1}",
1794 mean[i],
1795 want[i]
1796 );
1797 }
1798
1799 assert!(
1800 pointers.windows(2).all(|w| w[0] == w[1]),
1801 "the capture buffer moved between frames, so the registration cannot be reused: {pointers:?}"
1802 );
1803 println!(
1804 "captured {}x{} into one device pointer {:#x} across {} frames, no post-processing pass",
1805 gpu.encoder.width(),
1806 gpu.encoder.height(),
1807 pointers[0],
1808 pointers.len()
1809 );
1810 }
1811
1812 #[test]
1821 #[ignore]
1822 fn gpu_nvfbc_throughput() {
1823 let n: usize = std::env::var("NVFBC_BENCH_FRAMES")
1824 .ok()
1825 .and_then(|v| v.parse().ok())
1826 .unwrap_or(240);
1827 for codec in [
1828 crate::encoders::codec::Codec::H264,
1829 crate::encoders::codec::Codec::H265,
1830 crate::encoders::codec::Codec::Av1,
1831 ] {
1832 let mut s = settings(codec);
1833 s.auto_adjust_screen_capture_size = true;
1834 let Some(mut gpu) = open(&s) else {
1835 println!("{}: declined", codec.display());
1836 continue;
1837 };
1838 let (w, h) = (gpu.encoder.width(), gpu.encoder.height());
1839 let run = |label: &str, repaint: bool, gpu: &mut GpuCapture| {
1840 let f = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1841 let pitch = frame_pitch(f.byte_size, f.width, f.height);
1842 gpu.encoder
1843 .encode_cuda_pitch(f.device_ptr, pitch, false, 0, 25, true)
1844 .expect("warm-up");
1845 let (mut wall, mut cpu, mut bytes, mut news) =
1846 (Duration::ZERO, Duration::ZERO, 0usize, 0usize);
1847 for i in 0..n {
1848 if repaint {
1849 let v = (i * 9) as u8;
1850 let _ = std::process::Command::new("xsetroot")
1851 .args(["-solid", &format!("#{:02x}{:02x}{:02x}", v, 0x40, 0x80)])
1852 .output();
1853 }
1854 let t = Instant::now();
1855 let c = thread_cpu();
1856 let f = gpu
1857 .nvfbc
1858 .grab_with(NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, Duration::from_millis(50))
1859 .expect("grab");
1860 let pitch = frame_pitch(f.byte_size, f.width, f.height);
1861 let pkt = gpu
1862 .encoder
1863 .encode_cuda_pitch(f.device_ptr, pitch, false, 1 + i as u64, 25, false)
1864 .expect("encode");
1865 wall += t.elapsed();
1866 cpu += thread_cpu() - c;
1867 bytes += pkt.len();
1868 news += f.is_new as usize;
1869 }
1870 println!(
1871 "{} {w}x{h} {label}: {:.0} fps ({:.2} ms/frame wall, {:.2} ms/frame host CPU), {news}/{n} new, {:.1} KiB/frame",
1872 codec.display(),
1873 n as f64 / wall.as_secs_f64(),
1874 wall.as_secs_f64() * 1000.0 / n as f64,
1875 cpu.as_secs_f64() * 1000.0 / n as f64,
1876 bytes as f64 / n as f64 / 1024.0
1877 );
1878 };
1879 run("steady state", false, &mut gpu);
1880 run("moving screen", true, &mut gpu);
1881 shm_upload_baseline(codec, w, h, n);
1882 }
1883 }
1884
1885 fn shm_upload_baseline(codec: crate::encoders::codec::Codec, w: u32, h: u32, n: usize) {
1890 use x11rb::connection::Connection;
1891 use x11rb::protocol::shm::ConnectionExt as ShmExt;
1892 let Ok((conn, screen)) = x11rb::connect(None) else {
1893 return;
1894 };
1895 if conn
1896 .shm_query_version()
1897 .ok()
1898 .and_then(|c| c.reply().ok())
1899 .is_none()
1900 {
1901 return;
1902 }
1903 let root = conn.setup().roots[screen].root;
1904 let Ok(mut surface) = crate::x11::ShmSurface::create(&conn, w as u16, h as u16) else {
1905 return;
1906 };
1907 let mut s = settings(codec);
1908 s.width = w as i32;
1909 s.height = h as i32;
1910 let mut enc = match NvencEncoder::new(&s, std::ptr::null()) {
1911 Ok(e) => e,
1912 Err(_) => {
1913 surface.destroy(&conn);
1914 return;
1915 }
1916 };
1917 let stride = w as usize * 4;
1918 let (mut wall, mut cpu) = (Duration::ZERO, Duration::ZERO);
1919 for i in 0..n {
1920 let t = Instant::now();
1921 let c = thread_cpu();
1922 if crate::x11::grab_frame(&conn, root, &surface, 0, 0, w as u16, h as u16).is_err() {
1923 break;
1924 }
1925 let _ = enc
1926 .encode_cpu_packed(surface.as_slice(), stride, false, i as u64, 25, i == 0)
1927 .expect("encode uploaded frame");
1928 wall += t.elapsed();
1929 cpu += thread_cpu() - c;
1930 }
1931 surface.destroy(&conn);
1932 println!(
1933 "{} {w}x{h} XShm grab + upload: {:.0} fps ({:.2} ms/frame wall, {:.2} ms/frame host CPU)",
1934 codec.display(),
1935 n as f64 / wall.as_secs_f64(),
1936 wall.as_secs_f64() * 1000.0 / n as f64,
1937 cpu.as_secs_f64() * 1000.0 / n as f64
1938 );
1939 }
1940}