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::connection::Connection;
58use x11rb::protocol::Event;
59use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as XprotoExt, EventMask};
60use x11rb::rust_connection::RustConnection;
61
62use super::Controls;
63use crate::CaptureSettings;
64use crate::encoders::nvenc::NvencEncoder;
65use crate::encoders::software::{EncodedStripe, FrameTiming, StripeState};
66use crate::pipeline::{
67 DAMAGE_BAND_ROWS, Damage, EncoderQuality, decide_hw_fullframe, hashed_damage, periodic_idr_due,
68 plan_refinement, refinement_coded,
69};
70use crate::recording_sink::RecordingSink;
71use nvcodec_sys::cuda::CUdeviceptr;
72
73const NVFBC_VERSION: u32 = 8 | (1 << 8);
75
76const fn struct_ver(size: usize, rev: u32) -> u32 {
80 (size as u32) | (rev << 16) | (NVFBC_VERSION << 24)
81}
82
83type NVFBCSTATUS = u32;
84const NVFBC_SUCCESS: NVFBCSTATUS = 0;
85const NVFBC_ERR_X: NVFBCSTATUS = 10;
87const NVFBC_ERR_MUST_RECREATE: NVFBCSTATUS = 16;
89
90type NVFBC_BOOL = u32;
91const NVFBC_FALSE: NVFBC_BOOL = 0;
92const NVFBC_TRUE: NVFBC_BOOL = 1;
93
94type NVFBC_SESSION_HANDLE = u64;
95
96const NVFBC_CAPTURE_SHARED_CUDA: u32 = 1;
98const NVFBC_TRACKING_SCREEN: u32 = 2;
100const NVFBC_BUFFER_FORMAT_BGRA: u32 = 5;
102const NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT: u32 = 1 << 0;
104const NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY: u32 = 1 << 2;
106
107const NVFBC_OUTPUT_MAX: usize = 5;
108const NVFBC_OUTPUT_NAME_LEN: usize = 128;
109
110#[repr(C)]
112#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
113struct NVFBC_BOX {
114 x: u32,
115 y: u32,
116 w: u32,
117 h: u32,
118}
119
120#[repr(C)]
122#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
123struct NVFBC_SIZE {
124 w: u32,
125 h: u32,
126}
127
128#[repr(C)]
132#[derive(Clone, Copy, Default)]
133struct NVFBC_FRAME_GRAB_INFO {
134 dwWidth: u32,
135 dwHeight: u32,
136 dwByteSize: u32,
137 dwCurrentFrame: u32,
138 bIsNewFrame: NVFBC_BOOL,
139 ulTimestampUs: u64,
140 dwMissedFrames: u32,
141 bRequiredPostProcessing: NVFBC_BOOL,
142 bDirectCapture: NVFBC_BOOL,
143}
144
145#[repr(C)]
146#[derive(Clone, Copy, Default)]
147struct NVFBC_CREATE_HANDLE_PARAMS {
148 dwVersion: u32,
149 privateData: *const c_void,
150 privateDataSize: u32,
151 bExternallyManagedContext: NVFBC_BOOL,
152 glxCtx: *mut c_void,
153 glxFBConfig: *mut c_void,
154}
155
156#[repr(C)]
158#[derive(Clone, Copy, Default)]
159struct NVFBC_VERSION_ONLY_PARAMS {
160 dwVersion: u32,
161}
162
163#[repr(C)]
165#[derive(Clone, Copy)]
166struct NVFBC_RANDR_OUTPUT_INFO {
167 dwId: u32,
168 name: [c_char; NVFBC_OUTPUT_NAME_LEN],
169 trackedBox: NVFBC_BOX,
170}
171
172impl Default for NVFBC_RANDR_OUTPUT_INFO {
173 fn default() -> Self {
174 Self {
175 dwId: 0,
176 name: [0; NVFBC_OUTPUT_NAME_LEN],
177 trackedBox: NVFBC_BOX::default(),
178 }
179 }
180}
181
182#[repr(C)]
185#[derive(Clone, Copy)]
186struct NVFBC_GET_STATUS_PARAMS {
187 dwVersion: u32,
188 bIsCapturePossible: NVFBC_BOOL,
189 bCurrentlyCapturing: NVFBC_BOOL,
190 bCanCreateNow: NVFBC_BOOL,
191 screenSize: NVFBC_SIZE,
192 bXRandRAvailable: NVFBC_BOOL,
193 outputs: [NVFBC_RANDR_OUTPUT_INFO; NVFBC_OUTPUT_MAX],
194 dwOutputNum: u32,
195 dwNvFBCVersion: u32,
196 bInModeset: NVFBC_BOOL,
197}
198
199impl Default for NVFBC_GET_STATUS_PARAMS {
200 fn default() -> Self {
201 Self {
202 dwVersion: 0,
203 bIsCapturePossible: 0,
204 bCurrentlyCapturing: 0,
205 bCanCreateNow: 0,
206 screenSize: NVFBC_SIZE::default(),
207 bXRandRAvailable: 0,
208 outputs: [NVFBC_RANDR_OUTPUT_INFO::default(); NVFBC_OUTPUT_MAX],
209 dwOutputNum: 0,
210 dwNvFBCVersion: 0,
211 bInModeset: 0,
212 }
213 }
214}
215
216#[repr(C)]
217#[derive(Clone, Copy, Default)]
218struct NVFBC_CREATE_CAPTURE_SESSION_PARAMS {
219 dwVersion: u32,
220 eCaptureType: u32,
221 eTrackingType: u32,
222 dwOutputId: u32,
223 captureBox: NVFBC_BOX,
224 frameSize: NVFBC_SIZE,
225 bWithCursor: NVFBC_BOOL,
226 bDisableAutoModesetRecovery: NVFBC_BOOL,
227 bRoundFrameSize: NVFBC_BOOL,
228 dwSamplingRateMs: u32,
229 bPushModel: NVFBC_BOOL,
230 bAllowDirectCapture: NVFBC_BOOL,
231}
232
233#[repr(C)]
234#[derive(Clone, Copy, Default)]
235struct NVFBC_TOCUDA_SETUP_PARAMS {
236 dwVersion: u32,
237 eBufferFormat: u32,
238}
239
240#[repr(C)]
241#[derive(Clone, Copy, Default)]
242struct NVFBC_TOCUDA_GRAB_FRAME_PARAMS {
243 dwVersion: u32,
244 dwFlags: u32,
245 pCUDADeviceBuffer: *mut c_void,
246 pFrameGrabInfo: *mut NVFBC_FRAME_GRAB_INFO,
247 dwTimeoutMs: u32,
248}
249
250#[repr(C)]
253#[derive(Clone, Copy)]
254struct NVFBC_API_FUNCTION_LIST {
255 dwVersion: u32,
256 nvFBCGetLastErrorStr: Option<unsafe extern "C" fn(NVFBC_SESSION_HANDLE) -> *const c_char>,
257 nvFBCCreateHandle: Option<
258 unsafe extern "C" fn(
259 *mut NVFBC_SESSION_HANDLE,
260 *mut NVFBC_CREATE_HANDLE_PARAMS,
261 ) -> NVFBCSTATUS,
262 >,
263 nvFBCDestroyHandle: Option<
264 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
265 >,
266 nvFBCGetStatus: Option<
267 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_GET_STATUS_PARAMS) -> NVFBCSTATUS,
268 >,
269 nvFBCCreateCaptureSession: Option<
270 unsafe extern "C" fn(
271 NVFBC_SESSION_HANDLE,
272 *mut NVFBC_CREATE_CAPTURE_SESSION_PARAMS,
273 ) -> NVFBCSTATUS,
274 >,
275 nvFBCDestroyCaptureSession: Option<
276 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
277 >,
278 nvFBCToSysSetUp: *mut c_void,
279 nvFBCToSysGrabFrame: *mut c_void,
280 nvFBCToCudaSetUp: Option<
281 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_TOCUDA_SETUP_PARAMS) -> NVFBCSTATUS,
282 >,
283 nvFBCToCudaGrabFrame: Option<
284 unsafe extern "C" fn(
285 NVFBC_SESSION_HANDLE,
286 *mut NVFBC_TOCUDA_GRAB_FRAME_PARAMS,
287 ) -> NVFBCSTATUS,
288 >,
289 pad1: *mut c_void,
290 pad2: *mut c_void,
291 pad3: *mut c_void,
292 nvFBCBindContext: Option<
293 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
294 >,
295 nvFBCReleaseContext: Option<
296 unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
297 >,
298 pad4: *mut c_void,
299 pad5: *mut c_void,
300 pad6: *mut c_void,
301 pad7: *mut c_void,
302 nvFBCToGLSetUp: *mut c_void,
303 nvFBCToGLGrabFrame: *mut c_void,
304}
305
306impl Default for NVFBC_API_FUNCTION_LIST {
307 fn default() -> Self {
308 unsafe { std::mem::zeroed() }
309 }
310}
311
312type NvFBCCreateInstanceFn = unsafe extern "C" fn(*mut NVFBC_API_FUNCTION_LIST) -> NVFBCSTATUS;
313
314const NVFBC_LIBRARY: &str = "libnvidia-fbc.so.1";
317
318#[derive(Debug)]
324struct NvfbcError {
325 status: NVFBCSTATUS,
326 message: String,
327}
328
329impl std::fmt::Display for NvfbcError {
330 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331 if self.message.is_empty() {
332 write!(f, "({})", self.status)
333 } else {
334 write!(f, "{} ({})", self.message, self.status)
335 }
336 }
337}
338
339struct GrabbedFrame {
341 device_ptr: CUdeviceptr,
342 width: u32,
343 height: u32,
344 byte_size: u32,
345 is_new: bool,
348 direct: bool,
351 post_processed: bool,
354}
355
356struct NvfbcSession {
365 _lib: Library,
366 funcs: NVFBC_API_FUNCTION_LIST,
367 handle: NVFBC_SESSION_HANDLE,
368 capture_open: bool,
369 region: NVFBC_BOX,
371 size: NVFBC_SIZE,
372 with_cursor: bool,
373}
374
375impl NvfbcSession {
376 fn load() -> Result<(Library, NVFBC_API_FUNCTION_LIST), String> {
380 unsafe {
381 let lib = Library::new(NVFBC_LIBRARY)
382 .map_err(|e| format!("could not load {NVFBC_LIBRARY}: {e}"))?;
383 let create: libloading::Symbol<NvFBCCreateInstanceFn> = lib
384 .get(b"NvFBCCreateInstance\0")
385 .map_err(|e| format!("missing symbol NvFBCCreateInstance: {e}"))?;
386 let mut funcs = NVFBC_API_FUNCTION_LIST {
387 dwVersion: NVFBC_VERSION,
388 ..Default::default()
389 };
390 let st = create(&mut funcs);
391 if st != NVFBC_SUCCESS {
392 return Err(format!("NvFBCCreateInstance failed ({st})"));
393 }
394 if funcs.nvFBCCreateHandle.is_none()
395 || funcs.nvFBCGetStatus.is_none()
396 || funcs.nvFBCCreateCaptureSession.is_none()
397 || funcs.nvFBCToCudaSetUp.is_none()
398 || funcs.nvFBCToCudaGrabFrame.is_none()
399 {
400 return Err("the driver's NvFBC table has no CUDA capture entry points".into());
401 }
402 Ok((lib, funcs))
403 }
404 }
405
406 fn last_error(&self) -> String {
409 match self.funcs.nvFBCGetLastErrorStr {
410 Some(f) => unsafe {
411 let p = f(self.handle);
412 if p.is_null() {
413 String::new()
414 } else {
415 CStr::from_ptr(p).to_string_lossy().into_owned()
416 }
417 },
418 None => String::new(),
419 }
420 }
421
422 fn fail(&self, what: &str, status: NVFBCSTATUS) -> NvfbcError {
424 let detail = self.last_error();
425 let message = if detail.is_empty() {
426 format!("{what} failed")
427 } else {
428 format!("{what} failed: {detail}")
429 };
430 NvfbcError { status, message }
431 }
432
433 fn open() -> Result<Self, String> {
442 if !nvidia_driven_x_server()? {
443 return Err(format!("the X server offers no {NVIDIA_GLX}"));
444 }
445 let (lib, funcs) = Self::load()?;
446 let mut handle: NVFBC_SESSION_HANDLE = 0;
447 let mut params = NVFBC_CREATE_HANDLE_PARAMS {
448 dwVersion: struct_ver(std::mem::size_of::<NVFBC_CREATE_HANDLE_PARAMS>(), 2),
449 bExternallyManagedContext: NVFBC_FALSE,
450 ..Default::default()
451 };
452 let st = unsafe { (funcs.nvFBCCreateHandle.unwrap())(&mut handle, &mut params) };
453 if st != NVFBC_SUCCESS {
454 return Err(format!("NvFBCCreateHandle failed ({st})"));
455 }
456 Ok(Self {
457 _lib: lib,
458 funcs,
459 handle,
460 capture_open: false,
461 region: NVFBC_BOX::default(),
462 size: NVFBC_SIZE::default(),
463 with_cursor: false,
464 })
465 }
466
467 fn status(&self) -> Result<NVFBC_GET_STATUS_PARAMS, NvfbcError> {
469 let mut params = NVFBC_GET_STATUS_PARAMS {
470 dwVersion: struct_ver(std::mem::size_of::<NVFBC_GET_STATUS_PARAMS>(), 2),
471 ..Default::default()
472 };
473 let st = unsafe { (self.funcs.nvFBCGetStatus.unwrap())(self.handle, &mut params) };
474 if st != NVFBC_SUCCESS {
475 return Err(self.fail("NvFBCGetStatus", st));
476 }
477 Ok(params)
478 }
479
480 fn start(
490 &mut self,
491 region: NVFBC_BOX,
492 size: NVFBC_SIZE,
493 with_cursor: bool,
494 ) -> Result<(), NvfbcError> {
495 self.stop();
496 let mut params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS {
497 dwVersion: struct_ver(
498 std::mem::size_of::<NVFBC_CREATE_CAPTURE_SESSION_PARAMS>(),
499 6,
500 ),
501 eCaptureType: NVFBC_CAPTURE_SHARED_CUDA,
502 eTrackingType: NVFBC_TRACKING_SCREEN,
503 dwOutputId: 0,
504 captureBox: region,
505 frameSize: size,
506 bWithCursor: if with_cursor { NVFBC_TRUE } else { NVFBC_FALSE },
507 bDisableAutoModesetRecovery: NVFBC_FALSE,
508 bRoundFrameSize: NVFBC_FALSE,
509 dwSamplingRateMs: 0,
510 bPushModel: NVFBC_TRUE,
511 bAllowDirectCapture: NVFBC_TRUE,
512 };
513 let st =
514 unsafe { (self.funcs.nvFBCCreateCaptureSession.unwrap())(self.handle, &mut params) };
515 if st != NVFBC_SUCCESS {
516 return Err(self.fail("NvFBCCreateCaptureSession", st));
517 }
518 self.capture_open = true;
519 let mut setup = NVFBC_TOCUDA_SETUP_PARAMS {
520 dwVersion: struct_ver(std::mem::size_of::<NVFBC_TOCUDA_SETUP_PARAMS>(), 1),
521 eBufferFormat: NVFBC_BUFFER_FORMAT_BGRA,
522 };
523 let st = unsafe { (self.funcs.nvFBCToCudaSetUp.unwrap())(self.handle, &mut setup) };
524 if st != NVFBC_SUCCESS {
525 let msg = self.fail("NvFBCToCudaSetUp", st);
526 self.stop();
527 return Err(msg);
528 }
529 self.region = region;
530 self.size = size;
531 self.with_cursor = with_cursor;
532 Ok(())
533 }
534
535 fn stop(&mut self) {
537 if !self.capture_open {
538 return;
539 }
540 let mut params = NVFBC_VERSION_ONLY_PARAMS {
541 dwVersion: struct_ver(std::mem::size_of::<NVFBC_VERSION_ONLY_PARAMS>(), 1),
542 };
543 if let Some(f) = self.funcs.nvFBCDestroyCaptureSession {
544 unsafe { f(self.handle, &mut params) };
545 }
546 self.capture_open = false;
547 }
548
549 fn grab(&mut self, timeout: Duration) -> Result<GrabbedFrame, NvfbcError> {
556 self.grab_with(NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY, timeout)
557 }
558
559 fn grab_with(&mut self, flags: u32, timeout: Duration) -> Result<GrabbedFrame, NvfbcError> {
561 let mut device_ptr: CUdeviceptr = 0;
562 let mut info = NVFBC_FRAME_GRAB_INFO::default();
563 let mut params = NVFBC_TOCUDA_GRAB_FRAME_PARAMS {
564 dwVersion: struct_ver(std::mem::size_of::<NVFBC_TOCUDA_GRAB_FRAME_PARAMS>(), 2),
565 dwFlags: flags,
566 pCUDADeviceBuffer: &mut device_ptr as *mut CUdeviceptr as *mut c_void,
567 pFrameGrabInfo: &mut info,
568 dwTimeoutMs: timeout
571 .as_micros()
572 .div_ceil(1000)
573 .clamp(1, u128::from(u32::MAX)) as u32,
574 };
575 let st = unsafe { (self.funcs.nvFBCToCudaGrabFrame.unwrap())(self.handle, &mut params) };
576 if st != NVFBC_SUCCESS {
577 return Err(self.fail("NvFBCToCudaGrabFrame", st));
578 }
579 Ok(GrabbedFrame {
580 device_ptr,
581 width: info.dwWidth,
582 height: info.dwHeight,
583 byte_size: info.dwByteSize,
584 is_new: info.bIsNewFrame == NVFBC_TRUE,
585 direct: info.bDirectCapture == NVFBC_TRUE,
586 post_processed: info.bRequiredPostProcessing == NVFBC_TRUE,
587 })
588 }
589}
590
591impl Drop for NvfbcSession {
592 fn drop(&mut self) {
593 self.stop();
594 let mut params = NVFBC_VERSION_ONLY_PARAMS {
595 dwVersion: struct_ver(std::mem::size_of::<NVFBC_VERSION_ONLY_PARAMS>(), 1),
596 };
597 if let Some(f) = self.funcs.nvFBCDestroyHandle {
598 unsafe { f(self.handle, &mut params) };
599 }
600 }
601}
602
603#[derive(Clone, Copy, PartialEq, Eq)]
605enum Recovery {
606 Session,
608 Handle,
612 None,
614}
615
616fn recovery_for(err: &NvfbcError) -> Recovery {
617 match err.status {
618 NVFBC_ERR_MUST_RECREATE => Recovery::Session,
619 NVFBC_ERR_X => Recovery::Handle,
620 _ => Recovery::None,
621 }
622}
623
624fn resolve_region(screen: NVFBC_SIZE, s: &CaptureSettings) -> (NVFBC_BOX, NVFBC_SIZE) {
632 let (w, h) = super::resolve_dims(
633 screen.w.min(u32::from(u16::MAX)) as u16,
634 screen.h.min(u32::from(u16::MAX)) as u16,
635 s,
636 );
637 let x =
638 super::clamp_offset(s.capture_x, screen.w.min(u32::from(u16::MAX)) as u16).max(0) as u32;
639 let y =
640 super::clamp_offset(s.capture_y, screen.h.min(u32::from(u16::MAX)) as u16).max(0) as u32;
641 let size = NVFBC_SIZE {
642 w: u32::from(w),
643 h: u32::from(h),
644 };
645 let full = x == 0 && y == 0 && size.w == screen.w && size.h == screen.h;
650 let region = if full {
651 NVFBC_BOX::default()
652 } else {
653 NVFBC_BOX {
654 x,
655 y,
656 w: size.w,
657 h: size.h,
658 }
659 };
660 (region, size)
661}
662
663struct GpuCapture {
666 nvfbc: NvfbcSession,
667 encoder: NvencEncoder,
668 settings: CaptureSettings,
671 request: CaptureSettings,
675 screen: NVFBC_SIZE,
676}
677
678struct RootWatch {
682 conn: RustConnection,
683}
684
685impl RootWatch {
686 fn open() -> Option<Self> {
687 let (conn, screen) = x11rb::connect(None).ok()?;
688 let root = conn.setup().roots.get(screen)?.root;
689 conn.change_window_attributes(
690 root,
691 &ChangeWindowAttributesAux::new().event_mask(EventMask::STRUCTURE_NOTIFY),
692 )
693 .ok()?
694 .check()
695 .ok()?;
696 Some(Self { conn })
697 }
698
699 fn changed(&self) -> bool {
702 let mut changed = false;
703 loop {
704 match self.conn.poll_for_event() {
705 Ok(Some(Event::ConfigureNotify(_))) => changed = true,
706 Ok(Some(_)) => {}
707 Ok(None) => return changed,
708 Err(_) => return true,
709 }
710 }
711 }
712}
713
714const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
716
717fn answered_within<T: Send + 'static>(
721 timeout: Duration,
722 what: &str,
723 f: impl FnOnce() -> Result<T, String> + Send + 'static,
724) -> Result<T, String> {
725 let (tx, rx) = std::sync::mpsc::channel();
726 std::thread::Builder::new()
727 .name(format!("pf-{}-probe", what.to_lowercase()))
728 .spawn(move || {
729 let _ = tx.send(f());
730 })
731 .map_err(|e| format!("{what} probe thread: {e}"))?;
732 match rx.recv_timeout(timeout) {
733 Ok(answer) => answer,
734 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(format!(
735 "{what} did not answer within {} s",
736 timeout.as_secs()
737 )),
738 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
739 Err(format!("{what} ended without an answer"))
740 }
741 }
742}
743
744const NVIDIA_GLX: &str = "NV-GLX";
750
751fn nvidia_driven_x_server() -> Result<bool, String> {
755 let (conn, _) = x11rb::connect(None).map_err(|e| format!("X11 connect failed: {e}"))?;
756 let cookie = conn
757 .query_extension(NVIDIA_GLX.as_bytes())
758 .map_err(|e| format!("{NVIDIA_GLX} query failed: {e}"))?;
759 let reply = cookie
760 .reply()
761 .map_err(|e| format!("{NVIDIA_GLX} query failed: {e}"))?;
762 Ok(reply.present)
763}
764
765fn foreign_libxcb() -> Option<String> {
777 unsafe {
778 let x11 = libc::dlopen(c"libX11.so.6".as_ptr(), libc::RTLD_LAZY | libc::RTLD_NOLOAD);
779 if x11.is_null() {
780 return None;
781 }
782 let open = libc::dlsym(x11, c"XOpenDisplay".as_ptr());
783 if !open.is_null() {
784 let open: unsafe extern "C" fn(*const c_char) -> *mut c_void =
785 std::mem::transmute(open);
786 open(c"pixelflux".as_ptr());
787 }
788 libc::dlclose(x11);
789 let bound = crate::nvgpufilter::bound_import("libX11.so", b"xcb_parse_display")?;
790 let xcb = libc::dlopen(c"libxcb.so.1".as_ptr(), libc::RTLD_LAZY | libc::RTLD_NOLOAD);
791 if xcb.is_null() {
792 return None;
793 }
794 let host = libc::dlsym(xcb, c"xcb_parse_display".as_ptr());
795 libc::dlclose(xcb);
796 let object = |addr: *const c_void| {
797 let mut info: libc::Dl_info = std::mem::zeroed();
798 (libc::dladdr(addr, &mut info) != 0
799 && !info.dli_sname.is_null()
800 && !info.dli_fname.is_null())
801 .then(|| {
802 (
803 info.dli_fbase,
804 CStr::from_ptr(info.dli_sname),
805 CStr::from_ptr(info.dli_fname),
806 )
807 })
808 };
809 let (bound_base, bound_name, bound_path) = object(bound as *const c_void)?;
810 let (host_base, _, host_path) = object(host)?;
811 (bound_name.to_bytes() == b"xcb_parse_display" && bound_base != host_base).then(|| {
812 format!(
813 "libX11 calls {}, not the {} the NVIDIA library calls on the same connection",
814 bound_path.to_string_lossy(),
815 host_path.to_string_lossy()
816 )
817 })
818 }
819}
820
821fn declined(reason: &str) -> Option<GpuCapture> {
823 println!("[X11] Zero-copy capture (NvFBC) unavailable: {reason}.");
824 crate::report::capture_declined("NvFBC", reason);
825 None
826}
827
828fn open(settings: &CaptureSettings) -> Option<GpuCapture> {
841 if !settings.codec.is_video() {
842 return declined("the codec is JPEG");
843 }
844 if settings.use_cpu || settings.encode_node_index == -1 {
845 return declined("software encoding was requested");
846 }
847 if !settings.watermark_path.is_empty() {
848 return declined("a watermark is composited into the frame");
849 }
850 let node = settings.encode_node_index.max(0);
851 let driver = crate::get_gpu_driver(node);
852 if !crate::driver_selects_nvenc(&driver) {
853 return declined(&format!("the encode node's driver is {driver}"));
854 }
855 if crate::encoders::ten_bit_is_softwares(settings) {
856 return declined(&format!(
857 "NVENC encodes no 10-bit {}, which the software encoder does",
858 settings.codec.display()
859 ));
860 }
861 if let Some(reason) = foreign_libxcb() {
862 return declined(&reason);
863 }
864 let probed = answered_within(PROBE_TIMEOUT, "NvFBC", || {
869 let nvfbc = NvfbcSession::open()?;
870 let status = nvfbc.status().map_err(|e| e.to_string())?;
871 if status.bIsCapturePossible != NVFBC_TRUE {
872 return Err("the driver reports capture is not possible on this X server".into());
873 }
874 if status.bInModeset == NVFBC_TRUE {
875 return Err("the X server is in a modeset".into());
876 }
877 Ok(status.screenSize)
878 });
879 let screen = match probed {
880 Ok(v) => v,
881 Err(e) => return declined(&e),
882 };
883 crate::report::zero_copy_available(true);
884 let mut nvfbc = match NvfbcSession::open() {
885 Ok(v) => v,
886 Err(e) => return declined(&e),
887 };
888
889 let (region, size) = resolve_region(screen, settings);
893 let request = settings.clone();
894 let mut settings = settings.clone();
895 settings.width = size.w as i32;
896 settings.height = size.h as i32;
897
898 let mut encoder = match NvencEncoder::new(&settings, std::ptr::null()) {
899 Ok(enc) => enc,
900 Err(e) => {
901 return declined(&format!(
902 "NVENC {} did not open: {e}",
903 settings.codec.display()
904 ));
905 }
906 };
907 if !encoder.push_context() {
910 return declined("the encoder's CUDA context could not be made current");
911 }
912 if let Err(e) = nvfbc.start(region, size, settings.capture_cursor) {
913 encoder.pop_context();
914 return declined(&e.to_string());
915 }
916
917 settings.width = nvfbc.size.w as i32;
918 settings.height = nvfbc.size.h as i32;
919 if (settings.width != encoder.width() as i32 || settings.height != encoder.height() as i32)
920 && let Err(e) = encoder.reconfigure_resolution(&settings)
921 {
922 nvfbc.stop();
923 encoder.pop_context();
924 return declined(&format!("NVENC could not follow the captured size: {e}"));
925 }
926 println!(
927 "[X11] Zero-copy capture (NvFBC): {}x{} composited into video memory, encoded in place on NVENC.",
928 settings.width, settings.height
929 );
930 crate::report::capture("NvFBC", true);
931 crate::report::hardware_encoder(
932 encoder.device_name(),
933 crate::encoders::driver_name(&driver),
934 node,
935 );
936 crate::log_stream_settings_of(
937 "X11",
938 &settings,
939 1,
940 Some(("NVENC", true)),
941 None,
942 true,
943 encoder.capped(),
944 encoder.is_fullcolor(),
945 false,
946 );
947 crate::report::bit_depth(encoder.bit_depth());
948 crate::report::capped(encoder.capped());
949 Some(GpuCapture {
950 nvfbc,
951 encoder,
952 settings,
953 request,
954 screen,
955 })
956}
957
958impl Drop for GpuCapture {
959 fn drop(&mut self) {
963 self.encoder.release_external_input();
964 self.nvfbc.stop();
965 self.encoder.pop_context();
966 }
967}
968
969const GEOMETRY_POLL_FRAMES: i32 = 30;
971
972pub fn run_capture<F>(
996 settings: CaptureSettings,
997 controls: Arc<Controls>,
998 encode_tid_tx: std::sync::mpsc::Sender<std::thread::ThreadId>,
999 on_frame: &mut F,
1000) -> Option<Result<(), String>>
1001where
1002 F: FnMut(Vec<EncodedStripe>),
1003{
1004 let mut gpu = open(&settings)?;
1005 let _ = encode_tid_tx.send(std::thread::current().id());
1008 controls
1009 .codec
1010 .store(gpu.encoder.codec().id(), Ordering::Relaxed);
1011
1012 let recording_sink = RecordingSink::try_bind(&settings.recording_socket, settings.target_fps);
1013 let mut state = StripeState::default();
1014 let mut hashes: Vec<u64> = Vec::new();
1015 let mut lagged = Damage::None;
1016 let mut frame_counter: u16 = 0;
1017 let mut pending_force_idr = false;
1018 let mut next_frame = Instant::now();
1019 let mut geometry_check = GEOMETRY_POLL_FRAMES;
1020 let root_watch = RootWatch::open();
1021 let mut grab_errors = 0u32;
1022 let mut session_down = false;
1025 let mut encode_errors = 0u32;
1026 let mut last_log = Instant::now();
1027 let mut sent_frames: u64 = 0;
1028 let mut new_frames: u64 = 0;
1029 let mut direct_frames: u64 = 0;
1030
1031 while !controls.stop.load(Ordering::Relaxed) {
1032 let fps = f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
1033 gpu.settings.video_link_kbps = controls.link_kbps.load(Ordering::Relaxed);
1034 let frame_dur = Duration::from_secs_f64(1.0 / fps.max(1.0));
1035 let now = Instant::now();
1036 if now < next_frame {
1037 std::thread::sleep(next_frame - now);
1038 }
1039 next_frame += frame_dur;
1040 let now = Instant::now();
1041 if next_frame < now {
1042 next_frame = now;
1043 }
1044 if controls.stop.load(Ordering::Relaxed) {
1045 break;
1046 }
1047
1048 for report in std::mem::take(&mut *controls.reference_reports.lock().unwrap()) {
1049 state.take_report(report);
1050 if !gpu.encoder.take_report(report) {
1051 pending_force_idr = true;
1052 }
1053 }
1054 if controls.force_idr.swap(false, Ordering::Relaxed)
1055 || recording_sink
1056 .as_ref()
1057 .is_some_and(|s| s.should_force_idr())
1058 {
1059 pending_force_idr = true;
1060 }
1061 if controls.rate_dirty.swap(false, Ordering::Acquire) {
1062 gpu.settings.video_bitrate_kbps = controls.bitrate_kbps.load(Ordering::Relaxed);
1063 gpu.settings.video_vbv_multiplier =
1064 f64::from(controls.vbv_mult_milli.load(Ordering::Relaxed)) / 1000.0;
1065 gpu.settings.target_fps =
1066 f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
1067 gpu.encoder.reconfigure_rate(&gpu.settings);
1068 }
1069 if controls.tunables_dirty.swap(false, Ordering::Acquire)
1070 && let Some(t) = controls.tunables.lock().unwrap().take()
1071 {
1072 t.apply_to(&mut gpu.settings);
1073 }
1074
1075 let want_cursor = controls.capture_cursor.load(Ordering::Relaxed);
1076 let mut recheck_geometry = controls.region_dirty.swap(false, Ordering::Acquire);
1077 if recheck_geometry {
1078 let (nx, ny, nw, nh) = *controls.region.lock().unwrap();
1079 gpu.request.capture_x = nx;
1080 gpu.request.capture_y = ny;
1081 gpu.request.width = nw;
1082 gpu.request.height = nh;
1083 gpu.request.auto_adjust_screen_capture_size = nw <= 0 || nh <= 0;
1085 }
1086 let ask_driver = match root_watch.as_ref() {
1089 Some(watch) => watch.changed(),
1090 None => {
1091 geometry_check -= 1;
1092 geometry_check <= 0
1093 }
1094 };
1095 if ask_driver {
1096 geometry_check = GEOMETRY_POLL_FRAMES;
1097 if let Ok(status) = gpu.nvfbc.status() {
1098 recheck_geometry |= status.screenSize != gpu.screen;
1099 gpu.screen = status.screenSize;
1100 }
1101 }
1102 if session_down || recheck_geometry || want_cursor != gpu.nvfbc.with_cursor {
1103 if (session_down || recheck_geometry)
1106 && let Ok(status) = gpu.nvfbc.status()
1107 {
1108 gpu.screen = status.screenSize;
1109 }
1110 let (region, size) = resolve_region(gpu.screen, &gpu.request);
1111 if session_down
1112 || region != gpu.nvfbc.region
1113 || size != gpu.nvfbc.size
1114 || want_cursor != gpu.nvfbc.with_cursor
1115 {
1116 match restart_session(&mut gpu, region, size, want_cursor) {
1117 Ok(()) => {
1118 session_down = false;
1119 state = StripeState::default();
1120 hashes.clear();
1121 lagged = Damage::None;
1122 pending_force_idr = true;
1123 }
1124 Err(e) => {
1125 grab_errors += 1;
1128 if grab_errors > 5 {
1129 return Some(Err(format!("NvFBC capture could not be rebuilt: {e}")));
1130 }
1131 eprintln!("[X11] NvFBC refused the capture session ({e}); trying again.");
1132 session_down = true;
1133 continue;
1134 }
1135 }
1136 }
1137 }
1138
1139 let wait = next_frame
1142 .saturating_duration_since(Instant::now())
1143 .max(Duration::from_millis(1));
1144 let frame = match gpu.nvfbc.grab(wait) {
1145 Ok(f) => {
1146 grab_errors = 0;
1147 f
1148 }
1149 Err(e) => {
1150 grab_errors += 1;
1151 let recovery = recovery_for(&e);
1152 if recovery == Recovery::None || grab_errors > 5 {
1153 return Some(Err(format!("NvFBC capture ended: {e}")));
1154 }
1155 if recovery == Recovery::Handle {
1156 eprintln!("[X11] NvFBC lost the X server ({e}); rebuilding the client handle.");
1157 if let Err(e) = reopen_handle(&mut gpu) {
1158 return Some(Err(format!("NvFBC capture could not be rebuilt: {e}")));
1159 }
1160 } else {
1161 eprintln!("[X11] NvFBC grab failed ({e}); rebuilding the capture session.");
1162 }
1163 session_down = true;
1164 continue;
1165 }
1166 };
1167 let grabbed_ns = crate::wayland::host::now_ns();
1168 if frame.device_ptr == 0 || frame.width == 0 || frame.height == 0 {
1169 continue;
1170 }
1171 new_frames += u64::from(frame.is_new);
1172 direct_frames += u64::from(frame.direct);
1173
1174 if frame.width != gpu.encoder.width() || frame.height != gpu.encoder.height() {
1175 gpu.settings.width = frame.width as i32;
1176 gpu.settings.height = frame.height as i32;
1177 if let Err(e) = gpu.encoder.reconfigure_resolution(&gpu.settings) {
1178 return Some(Err(format!(
1179 "NVENC could not follow the captured size: {e}"
1180 )));
1181 }
1182 state = StripeState::default();
1183 hashes.clear();
1184 lagged = Damage::None;
1185 pending_force_idr = true;
1186 }
1187
1188 let pitch = frame_pitch(frame.byte_size, frame.width, frame.height);
1189 let hash = |encoder: &mut NvencEncoder, hashes: &mut Vec<u64>| {
1190 encoder
1191 .band_hashes(
1192 frame.device_ptr,
1193 pitch,
1194 frame.width,
1195 frame.height,
1196 DAMAGE_BAND_ROWS as u32,
1197 )
1198 .map_or(Damage::Unknown, |h| hashed_damage(hashes, h))
1199 };
1200 let lags = gpu.settings.video_streaming_mode && gpu.settings.cleans_up();
1209 let damage = if lags {
1210 std::mem::replace(&mut lagged, Damage::None)
1211 } else if !frame.is_new {
1212 Damage::None
1213 } else if gpu.settings.video_streaming_mode || !gpu.settings.cleans_up() {
1214 hashes.clear();
1215 Damage::Unknown
1216 } else {
1217 hash(&mut gpu.encoder, &mut hashes)
1218 };
1219 if gpu.encoder.take_lost_cleanup() {
1220 state.lost_frame();
1221 }
1222 let mut decision = decide_hw_fullframe(
1223 &mut state,
1224 &gpu.settings,
1225 frame_counter,
1226 damage,
1227 false,
1228 pending_force_idr,
1229 EncoderQuality {
1230 last: gpu.encoder.last_quality(),
1231 bytes: gpu.encoder.last_size(),
1232 holds: true,
1233 reopens: false,
1234 keys: true,
1235 band: gpu.encoder.band_size(),
1236 measures: false,
1237 psnr: None,
1238 refit: gpu.encoder.refit(),
1239 follows_cap: false,
1240 finer: 0,
1241 ends_on_key: false,
1242 },
1243 );
1244 let normal = gpu.settings.video_crf as u32;
1245 let mut extent = None;
1246 if lags
1247 && frame.is_new
1248 && decision.send
1249 && (decision.cleanup_quality(normal).is_some()
1250 || decision.cap.is_some() && decision.hold_band.is_none())
1251 {
1252 let d = hash(&mut gpu.encoder, &mut hashes);
1253 if d.is_motion() {
1254 decision.hold_qp = None;
1255 decision.cap = None;
1256 decision.target_qp = normal;
1257 decision.force_idr = pending_force_idr
1258 || state.scheduled_key(periodic_idr_due(&gpu.settings, frame_counter), true);
1259 }
1260 extent = Some(d);
1261 }
1262 let capped = gpu.encoder.capped();
1268 let bound = gpu.encoder.peak_bound();
1269 if capped {
1270 let refines = gpu
1271 .encoder
1272 .refines()
1273 .then_some(crate::ledger::Refiner::finer(
1274 crate::encoders::nvenc::REFINE_FINER,
1275 ));
1276 plan_refinement(
1277 &mut state,
1278 &gpu.settings,
1279 &mut decision,
1280 damage,
1281 None,
1282 refines,
1283 gpu.encoder.last_quality(),
1284 bound,
1285 );
1286 if !decision.send {
1287 gpu.encoder.idle();
1288 }
1289 }
1290 let mut delivered = false;
1291 if decision.send {
1292 let encode_start_ns = crate::wayland::host::now_ns();
1293 if let Some(q) = decision.hold_qp {
1294 gpu.encoder.hold_quantizer(q, decision.hold_band);
1295 }
1296 if let Some(bytes) = decision.cap {
1297 gpu.encoder.cap(bytes);
1298 }
1299 if let Some(refinement) = &decision.refine {
1300 gpu.encoder.refine_blocks(refinement);
1301 }
1302 gpu.encoder.defer_blocks(decision.defer.as_ref());
1303 let outcome = gpu.encoder.encode_cuda_pitch(
1304 frame.device_ptr,
1305 pitch,
1306 false,
1307 u64::from(frame_counter),
1308 decision.target_qp,
1309 decision.force_idr,
1310 );
1311 if capped {
1312 let quality = EncoderQuality {
1313 last: gpu.encoder.last_quality(),
1314 bytes: gpu.encoder.last_size(),
1315 holds: true,
1316 reopens: false,
1317 keys: true,
1318 band: gpu.encoder.band_size(),
1319 measures: false,
1320 psnr: None,
1321 refit: gpu.encoder.refit(),
1322 follows_cap: false,
1323 finer: 0,
1324 ends_on_key: false,
1325 };
1326 refinement_coded(
1327 &mut state,
1328 &gpu.settings,
1329 &decision,
1330 quality,
1331 outcome.as_ref().map_or(0, Vec::len),
1332 bound,
1333 gpu.encoder.coded_capped(),
1334 );
1335 }
1336 match outcome {
1337 Ok(data) if !data.is_empty() => {
1338 encode_errors = 0;
1339 let stripes = vec![EncodedStripe {
1340 data: Arc::new(data),
1341 codec: gpu.settings.codec,
1342 stripe_y_start: 0,
1343 stripe_height: gpu.settings.height,
1344 frame_id: i32::from(frame_counter),
1345 timing: FrameTiming {
1346 capture_ns: grabbed_ns,
1347 encode_start_ns,
1348 encode_end_ns: crate::wayland::host::now_ns(),
1349 },
1350 reference: gpu.encoder.last_reference(),
1351 }];
1352 if let Some(sink) = &recording_sink {
1353 sink.write_frame(&stripes, gpu.settings.width, gpu.settings.height);
1354 }
1355 sent_frames += 1;
1356 delivered = true;
1357 on_frame(stripes);
1358 }
1359 Ok(_) => {}
1360 Err(e) => {
1361 encode_errors += 1;
1363 if encode_errors % crate::HW_ERROR_RECOVERY_THRESHOLD == 1 {
1364 eprintln!("[X11] NVENC encode error on the zero-copy path: {e}");
1365 }
1366 if encode_errors >= crate::HW_ERROR_RECOVERY_THRESHOLD {
1367 return Some(Err(
1368 "NVENC failed repeatedly on the zero-copy path".to_string()
1369 ));
1370 }
1371 }
1372 }
1373 }
1374 if lags {
1375 lagged = match extent {
1376 Some(d) => d,
1377 None if frame.is_new => hash(&mut gpu.encoder, &mut hashes),
1378 None => Damage::None,
1379 };
1380 }
1381 pending_force_idr = (pending_force_idr || decision.force_idr) && !delivered;
1384 frame_counter = frame_counter.wrapping_add(1);
1385
1386 let elapsed = last_log.elapsed().as_secs_f64();
1387 if elapsed >= 1.0 {
1388 crate::log::debug!(
1389 "[X11] NvFBC {}x{} Encoder: NVENC EncFPS: {:.2} NewFrames/s: {:.2} Direct: {}",
1390 gpu.settings.width,
1391 gpu.settings.height,
1392 sent_frames as f64 / elapsed,
1393 new_frames as f64 / elapsed,
1394 direct_frames > 0
1395 );
1396 sent_frames = 0;
1397 new_frames = 0;
1398 direct_frames = 0;
1399 last_log = Instant::now();
1400 }
1401 }
1402 Some(Ok(()))
1403}
1404
1405fn restart_session(
1409 gpu: &mut GpuCapture,
1410 region: NVFBC_BOX,
1411 size: NVFBC_SIZE,
1412 with_cursor: bool,
1413) -> Result<(), NvfbcError> {
1414 gpu.encoder.release_external_input();
1417 gpu.nvfbc.start(region, size, with_cursor)?;
1418 gpu.settings.width = size.w as i32;
1419 gpu.settings.height = size.h as i32;
1420 if gpu.settings.width != gpu.encoder.width() as i32
1421 || gpu.settings.height != gpu.encoder.height() as i32
1422 {
1423 gpu.encoder
1424 .reconfigure_resolution(&gpu.settings)
1425 .map_err(|e| NvfbcError {
1426 status: NVFBC_SUCCESS,
1427 message: e,
1428 })?;
1429 }
1430 Ok(())
1431}
1432
1433fn reopen_handle(gpu: &mut GpuCapture) -> Result<(), String> {
1441 gpu.encoder.release_external_input();
1442 gpu.nvfbc.stop();
1443 gpu.nvfbc = NvfbcSession::open()?;
1444 gpu.screen = gpu.nvfbc.status().map_err(|e| e.to_string())?.screenSize;
1445 Ok(())
1446}
1447
1448fn frame_pitch(byte_size: u32, width: u32, height: u32) -> usize {
1451 let packed = width as usize * 4;
1452 if height == 0 {
1453 return packed;
1454 }
1455 let rows = byte_size as usize / height as usize;
1456 if rows >= packed && rows.is_multiple_of(4) && rows * height as usize == byte_size as usize {
1457 rows
1458 } else {
1459 packed
1460 }
1461}
1462
1463#[cfg(test)]
1464mod abi_tests {
1465 use super::*;
1466
1467 #[test]
1473 fn probe_answers_are_bounded() {
1474 let stuck = answered_within(
1475 Duration::from_millis(50),
1476 "Stuck",
1477 || -> Result<u32, String> {
1478 std::thread::sleep(Duration::from_millis(400));
1479 Ok(1)
1480 },
1481 );
1482 assert_eq!(stuck, Err("Stuck did not answer within 0 s".to_string()));
1483 assert_eq!(
1484 answered_within(Duration::from_secs(1), "Quick", || Ok(7u32)),
1485 Ok(7)
1486 );
1487 assert_eq!(
1488 answered_within(
1489 Duration::from_secs(1),
1490 "Refused",
1491 || -> Result<u32, String> { Err("no".into()) }
1492 ),
1493 Err("no".to_string())
1494 );
1495 }
1496
1497 #[test]
1501 fn the_nvidia_gate_is_answered_by_the_server() {
1502 let answered = answered_within(Duration::from_secs(5), "Gate", || {
1503 Ok(nvidia_driven_x_server())
1504 })
1505 .expect("the NV-GLX gate must answer");
1506 let Ok(present) = answered else { return };
1507 let (conn, _) = x11rb::connect(None).expect("a server that answered once answers again");
1508 let listed = conn
1509 .list_extensions()
1510 .expect("the server takes the request")
1511 .reply()
1512 .expect("the server lists its extensions")
1513 .names
1514 .iter()
1515 .any(|name| name.name == NVIDIA_GLX.as_bytes());
1516 assert_eq!(present, listed);
1517 }
1518
1519 #[test]
1523 fn the_host_libx11_calls_the_host_libxcb() {
1524 let x11 = unsafe { libc::dlopen(c"libX11.so.6".as_ptr(), libc::RTLD_LAZY) };
1525 assert!(!x11.is_null(), "libX11.so.6 does not load here");
1526 assert_eq!(foreign_libxcb(), None);
1527 let bound = crate::nvgpufilter::bound_import("libX11.so", b"xcb_parse_display")
1528 .expect("libX11 imports xcb_parse_display");
1529 let mut info: libc::Dl_info = unsafe { std::mem::zeroed() };
1530 assert_ne!(
1531 unsafe { libc::dladdr(bound as *const c_void, &mut info) },
1532 0
1533 );
1534 assert!(!info.dli_sname.is_null(), "the slot names no symbol");
1535 assert_eq!(
1536 unsafe { CStr::from_ptr(info.dli_sname) }.to_bytes(),
1537 b"xcb_parse_display"
1538 );
1539 unsafe { libc::dlclose(x11) };
1540 }
1541
1542 #[test]
1543 fn struct_layouts_match_the_sdk() {
1544 assert_eq!(std::mem::size_of::<NVFBC_BOX>(), 16);
1545 assert_eq!(std::mem::size_of::<NVFBC_SIZE>(), 8);
1546 assert_eq!(std::mem::size_of::<NVFBC_FRAME_GRAB_INFO>(), 48);
1547 assert_eq!(std::mem::size_of::<NVFBC_CREATE_HANDLE_PARAMS>(), 40);
1548 assert_eq!(std::mem::size_of::<NVFBC_VERSION_ONLY_PARAMS>(), 4);
1549 assert_eq!(std::mem::size_of::<NVFBC_RANDR_OUTPUT_INFO>(), 148);
1550 assert_eq!(std::mem::size_of::<NVFBC_GET_STATUS_PARAMS>(), 780);
1551 assert_eq!(
1552 std::mem::size_of::<NVFBC_CREATE_CAPTURE_SESSION_PARAMS>(),
1553 64
1554 );
1555 assert_eq!(std::mem::size_of::<NVFBC_TOCUDA_SETUP_PARAMS>(), 8);
1556 assert_eq!(std::mem::size_of::<NVFBC_TOCUDA_GRAB_FRAME_PARAMS>(), 32);
1557 assert_eq!(std::mem::size_of::<NVFBC_API_FUNCTION_LIST>(), 176);
1558 }
1559
1560 #[test]
1563 fn version_words_match_the_sdk() {
1564 assert_eq!(NVFBC_VERSION, 0x108);
1565 assert_eq!(struct_ver(40, 2), 0x0802_0028);
1566 assert_eq!(struct_ver(4, 1), 0x0801_0004);
1567 assert_eq!(struct_ver(780, 2), 0x0802_030C);
1568 assert_eq!(struct_ver(64, 6), 0x0806_0040);
1569 assert_eq!(struct_ver(8, 1), 0x0801_0008);
1570 assert_eq!(struct_ver(32, 2), 0x0802_0020);
1571 }
1572}
1573
1574#[cfg(test)]
1575mod region_tests {
1576 use super::*;
1577 use crate::encoders::codec::Codec;
1578
1579 fn settings(w: i32, h: i32) -> CaptureSettings {
1580 CaptureSettings {
1581 width: w,
1582 height: h,
1583 codec: Codec::H264,
1584 ..Default::default()
1585 }
1586 }
1587
1588 #[test]
1591 fn whole_screen_is_an_empty_box() {
1592 let screen = NVFBC_SIZE { w: 1920, h: 1080 };
1593 let mut s = settings(1920, 1080);
1594 let (region, size) = resolve_region(screen, &s);
1595 assert_eq!(region, NVFBC_BOX::default());
1596 assert_eq!(size, NVFBC_SIZE { w: 1920, h: 1080 });
1597
1598 s.auto_adjust_screen_capture_size = true;
1599 let (region, size) = resolve_region(screen, &s);
1600 assert_eq!(region, NVFBC_BOX::default());
1601 assert_eq!(size, NVFBC_SIZE { w: 1920, h: 1080 });
1602
1603 let mut region_only = settings(1280, 720);
1604 region_only.capture_x = 100;
1605 region_only.capture_y = 50;
1606 let (region, size) = resolve_region(screen, ®ion_only);
1607 assert_eq!(
1608 region,
1609 NVFBC_BOX {
1610 x: 100,
1611 y: 50,
1612 w: 1280,
1613 h: 720
1614 }
1615 );
1616 assert_eq!(size, NVFBC_SIZE { w: 1280, h: 720 });
1617 }
1618
1619 #[test]
1626 fn region_is_even_and_inside_the_screen() {
1627 let screen = NVFBC_SIZE { w: 1919, h: 1081 };
1628 let (region, size) = resolve_region(screen, &settings(0, 0));
1629 assert_eq!(size, NVFBC_SIZE { w: 1918, h: 1080 });
1630 assert_eq!(
1631 region,
1632 NVFBC_BOX {
1633 x: 0,
1634 y: 0,
1635 w: 1918,
1636 h: 1080
1637 }
1638 );
1639
1640 let mut s = settings(1920, 1080);
1641 s.capture_x = 1000;
1642 s.capture_y = 800;
1643 let (region, size) = resolve_region(NVFBC_SIZE { w: 1920, h: 1080 }, &s);
1644 assert_eq!(region.x, 1000);
1645 assert_eq!(region.y, 800);
1646 assert_eq!(size, NVFBC_SIZE { w: 920, h: 280 });
1647 assert_eq!(region.w, size.w);
1648 assert_eq!(region.h, size.h);
1649 }
1650
1651 #[test]
1656 fn recovery_follows_the_status() {
1657 let session = |status| NvfbcError {
1658 status,
1659 message: String::new(),
1660 };
1661 assert!(matches!(
1662 recovery_for(&session(NVFBC_ERR_MUST_RECREATE)),
1663 Recovery::Session
1664 ));
1665 assert!(matches!(
1666 recovery_for(&session(NVFBC_ERR_X)),
1667 Recovery::Handle
1668 ));
1669 assert!(matches!(recovery_for(&session(1)), Recovery::None));
1670 let misleading = NvfbcError {
1671 status: 1,
1672 message: "the display (16) went away (10)".into(),
1673 };
1674 assert!(matches!(recovery_for(&misleading), Recovery::None));
1675 }
1676
1677 #[test]
1680 fn pitch_follows_the_reported_byte_size() {
1681 assert_eq!(frame_pitch(1920 * 1080 * 4, 1920, 1080), 1920 * 4);
1682 assert_eq!(frame_pitch(2048 * 4 * 1080, 1920, 1080), 2048 * 4);
1683 assert_eq!(frame_pitch(12345, 1920, 1080), 1920 * 4);
1684 assert_eq!(frame_pitch(0, 1920, 0), 1920 * 4);
1685 }
1686}
1687
1688#[cfg(test)]
1689mod gpu_tests {
1690 use super::super::gpu_test_support::{
1691 capped_crf_codes_again_what_it_coded_bound, decoded_mean, paint_root, painted_ycbcr,
1692 settings,
1693 };
1694 use super::*;
1695 use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, VIDEO_HEADER_LEN, parse_video_type};
1696 use crate::webcam::decode::{Codec as DecCodec, VideoDecoder};
1697
1698 fn thread_cpu() -> Duration {
1700 let mut ts = libc::timespec {
1701 tv_sec: 0,
1702 tv_nsec: 0,
1703 };
1704 unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) };
1705 Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
1706 }
1707
1708 #[test]
1713 #[ignore]
1714 fn gpu_nvfbc_opens_a_session_that_follows_the_screen() {
1715 let mut s = settings(crate::encoders::codec::Codec::H264);
1716 s.width = 0;
1717 s.height = 0;
1718 s.auto_adjust_screen_capture_size = true;
1719 let Some(gpu) = open(&s) else {
1720 println!("the NvFBC path declined this host; nothing to open");
1721 return;
1722 };
1723 assert!(
1724 gpu.settings.width > 0 && gpu.settings.height > 0,
1725 "a session following the screen opened at {}x{}",
1726 gpu.settings.width,
1727 gpu.settings.height
1728 );
1729 println!(
1730 "a session following the screen opened at {}x{}",
1731 gpu.settings.width, gpu.settings.height
1732 );
1733 }
1734
1735 #[test]
1739 #[ignore]
1740 fn gpu_nvfbc_status_reports_capture() {
1741 let Ok(nvfbc) = NvfbcSession::open() else {
1742 println!("NvFBC handed out no capture handle on this host; nothing to report");
1743 return;
1744 };
1745 let status = nvfbc.status().expect("NvFBC status");
1746 println!(
1747 "capture possible={} can create now={} screen={}x{} outputs={} nvfbc version={} in modeset={}",
1748 status.bIsCapturePossible,
1749 status.bCanCreateNow,
1750 status.screenSize.w,
1751 status.screenSize.h,
1752 status.dwOutputNum,
1753 status.dwNvFBCVersion,
1754 status.bInModeset
1755 );
1756 assert_eq!(
1757 status.bIsCapturePossible, NVFBC_TRUE,
1758 "this GPU offers no NvFBC"
1759 );
1760 assert!(status.screenSize.w > 0 && status.screenSize.h > 0);
1761 }
1762
1763 fn captured_luma(gpu: &mut GpuCapture, dec: &mut VideoDecoder, n: &mut u64) -> f64 {
1766 let mut frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1767 for _ in 0..10 {
1768 if frame.is_new {
1769 break;
1770 }
1771 frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1772 }
1773 let pitch = frame_pitch(frame.byte_size, frame.width, frame.height);
1774 let pkt = gpu
1775 .encoder
1776 .encode_cuda_pitch(frame.device_ptr, pitch, false, *n, 25, *n == 0)
1777 .expect("encode in place");
1778 *n += 1;
1779 decoded_mean(dec, &pkt[VIDEO_HEADER_LEN..])[0]
1780 }
1781
1782 #[test]
1790 #[ignore]
1791 fn gpu_nvfbc_two_sessions_on_one_screen() {
1792 const FIRST: (u8, u8, u8) = (0x20, 0x40, 0xc0);
1793 const SECOND: (u8, u8, u8) = (0xd0, 0x50, 0x18);
1794 const THIRD: (u8, u8, u8) = (0x30, 0xc0, 0x30);
1795 if !paint_root(FIRST) {
1796 println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1797 return;
1798 }
1799 let Some(mut first) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1800 println!("the NvFBC path declined the first capture on this host; nothing to compare");
1801 return;
1802 };
1803 let (go, wait) = std::sync::mpsc::channel::<()>();
1804 let (tell, seen) = std::sync::mpsc::channel::<Option<f64>>();
1805 let second = std::thread::spawn(move || {
1806 let Some(mut gpu) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1807 let _ = tell.send(None);
1808 return;
1809 };
1810 let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1811 let mut n = 0;
1812 let _ = tell.send(Some(captured_luma(&mut gpu, &mut dec, &mut n)));
1813 while wait.recv().is_ok() {
1814 let _ = tell.send(Some(captured_luma(&mut gpu, &mut dec, &mut n)));
1815 }
1816 });
1817 let opened = seen.recv().expect("the second capture's thread").is_some();
1818 println!(
1819 "a second concurrent NvFBC capture of the same screen: {}",
1820 if opened {
1821 "opens"
1822 } else {
1823 "is declined, and falls back to XShm"
1824 }
1825 );
1826 let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1827 let mut n = 0;
1828 captured_luma(&mut first, &mut dec, &mut n);
1829 assert!(paint_root(SECOND));
1830 let want = painted_ycbcr(SECOND)[0];
1831 let got = captured_luma(&mut first, &mut dec, &mut n);
1832 assert!(
1833 (got - want).abs() <= 8.0,
1834 "the first captured {got:.1} beside the second, painted {want:.1}"
1835 );
1836 if opened {
1837 go.send(()).expect("the second capture's thread");
1838 let got = seen
1839 .recv()
1840 .expect("the second capture's thread")
1841 .expect("a capture");
1842 assert!(
1843 (got - want).abs() <= 8.0,
1844 "the second captured {got:.1}, painted {want:.1}"
1845 );
1846 }
1847 drop(go);
1848 second.join().expect("the second capture's thread");
1849 assert!(paint_root(THIRD));
1850 let want = painted_ycbcr(THIRD)[0];
1851 let got = captured_luma(&mut first, &mut dec, &mut n);
1852 assert!(
1853 (got - want).abs() <= 8.0,
1854 "the first captured {got:.1} once the second closed, painted {want:.1}"
1855 );
1856 }
1857
1858 #[test]
1868 #[ignore]
1869 fn gpu_nvfbc_zero_copy_encodes_the_painted_root() {
1870 const FIRST: (u8, u8, u8) = (0x20, 0x40, 0xc0);
1871 const SECOND: (u8, u8, u8) = (0xd0, 0x50, 0x18);
1872 if !paint_root(FIRST) {
1873 println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1874 return;
1875 }
1876 let Some(mut gpu) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1877 println!("the NvFBC path declined this session on this host; nothing to capture");
1878 return;
1879 };
1880 let _ = gpu.nvfbc.grab(Duration::from_millis(200));
1882 assert!(
1883 paint_root(FIRST),
1884 "the root was painted once, so a repaint must work"
1885 );
1886 let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1887 let mut pointers = Vec::new();
1888
1889 let encode =
1890 |gpu: &mut GpuCapture, i: u64, key: bool, pointers: &mut Vec<CUdeviceptr>| -> Vec<u8> {
1891 let mut frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1893 for _ in 0..30 {
1894 if frame.is_new {
1895 break;
1896 }
1897 frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1898 }
1899 assert_eq!(frame.width, gpu.encoder.width());
1900 assert_eq!(frame.height, gpu.encoder.height());
1901 assert!(
1902 !frame.post_processed,
1903 "the native format needed a conversion pass, which is an extra frame copy"
1904 );
1905 pointers.push(frame.device_ptr);
1906 let pitch = frame_pitch(frame.byte_size, frame.width, frame.height);
1907 gpu.encoder
1908 .encode_cuda_pitch(frame.device_ptr, pitch, false, i, 25, key)
1909 .expect("encode in place")
1910 };
1911
1912 let pkt = encode(&mut gpu, 0, true, &mut pointers);
1913 assert_eq!(
1914 parse_video_type(pkt[1]),
1915 Some((crate::encoders::codec::Codec::H264, FRAME_KEY))
1916 );
1917 let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1918 let want = painted_ycbcr(FIRST);
1919 for i in 0..3 {
1920 assert!(
1921 (mean[i] - want[i]).abs() <= 8.0,
1922 "plane {i}: captured {:.1}, painted {:.1}",
1923 mean[i],
1924 want[i]
1925 );
1926 }
1927
1928 for i in 1..4u64 {
1929 let pkt = encode(&mut gpu, i, false, &mut pointers);
1930 assert_eq!(
1931 parse_video_type(pkt[1]),
1932 Some((crate::encoders::codec::Codec::H264, FRAME_DELTA))
1933 );
1934 let _ = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1935 }
1936
1937 assert!(
1938 paint_root(SECOND),
1939 "the root was painted once, so a repaint must work"
1940 );
1941 let pkt = encode(&mut gpu, 4, false, &mut pointers);
1942 let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1943 let want = painted_ycbcr(SECOND);
1944 for i in 0..3 {
1945 assert!(
1946 (mean[i] - want[i]).abs() <= 8.0,
1947 "after repaint, plane {i}: captured {:.1}, painted {:.1}",
1948 mean[i],
1949 want[i]
1950 );
1951 }
1952
1953 let mut buffers = pointers.clone();
1954 buffers.sort_unstable();
1955 buffers.dedup();
1956 assert!(
1957 buffers.len() <= crate::encoders::nvenc::EXTERNAL_INPUTS,
1958 "the capture used more buffers than the encoder keeps registered: {pointers:x?}"
1959 );
1960 println!(
1961 "captured {}x{} into {} device buffers {buffers:x?} across {} frames, no post-processing pass",
1962 gpu.encoder.width(),
1963 gpu.encoder.height(),
1964 buffers.len(),
1965 pointers.len()
1966 );
1967 }
1968
1969 #[test]
1976 #[ignore]
1977 fn gpu_nvfbc_rebuilds_inside_a_root_that_shrank() {
1978 use std::sync::atomic::AtomicU64;
1979 let fb = |w: i32, h: i32| {
1980 std::process::Command::new("xrandr")
1981 .args(["--fb", &format!("{w}x{h}")])
1982 .output()
1983 .map(|o| o.status.success())
1984 .unwrap_or(false)
1985 };
1986 if !fb(1920, 1080) || !paint_root((0x20, 0x40, 0xc0)) {
1987 println!("no X root RandR can resize here (xrandr --fb, $DISPLAY); nothing to capture");
1988 return;
1989 }
1990 let mut s = settings(crate::encoders::codec::Codec::H264);
1991 s.width = 1918;
1992 s.height = 1080;
1993 let controls = Arc::new(Controls::new(&s));
1994 let frames = Arc::new(AtomicU64::new(0));
1995 let capture = {
1996 let (controls, frames) = (controls.clone(), frames.clone());
1997 std::thread::spawn(move || {
1998 let (tx, _rx) = std::sync::mpsc::channel();
1999 run_capture(s, controls, tx, &mut |_: Vec<EncodedStripe>| {
2000 frames.fetch_add(1, Ordering::Relaxed);
2001 })
2002 })
2003 };
2004 let delivered_over = |ms: u64| {
2006 let before = frames.load(Ordering::Relaxed);
2007 let end = Instant::now() + Duration::from_millis(ms);
2008 let mut v = 0u8;
2009 while Instant::now() < end {
2010 paint_root((v, 0x40, 0x80));
2011 v = v.wrapping_add(17);
2012 std::thread::sleep(Duration::from_millis(50));
2013 }
2014 frames.load(Ordering::Relaxed) - before
2015 };
2016 let end = Instant::now() + Duration::from_secs(15);
2018 while frames.load(Ordering::Relaxed) == 0 && !capture.is_finished() && Instant::now() < end
2019 {
2020 delivered_over(100);
2021 }
2022 if capture.is_finished() {
2023 println!("the NvFBC path declined this host; nothing to capture");
2024 return;
2025 }
2026 assert!(delivered_over(1000) > 0, "frames before the resize");
2027 for (w, h) in [(1920, 1000), (1920, 1080), (1920, 1000)] {
2028 assert!(fb(w, h), "xrandr --fb {w}x{h}");
2029 delivered_over(300);
2031 assert!(
2032 !capture.is_finished(),
2033 "the capture ended when the root went to {w}x{h}"
2034 );
2035 *controls.region.lock().unwrap() = (0, 0, w - 2, h);
2036 controls.region_dirty.store(true, Ordering::Release);
2037 let n = delivered_over(1500);
2038 assert!(
2039 !capture.is_finished(),
2040 "the capture ended after the root went to {w}x{h}"
2041 );
2042 assert!(n > 0, "frames after the root went to {w}x{h}");
2043 println!("{w}x{h}: {n} frames delivered in 1.5 s");
2044 }
2045 controls.stop.store(true, Ordering::Relaxed);
2046 let ended = capture.join().expect("capture thread");
2047 let _ = fb(1920, 1080);
2048 assert!(matches!(ended, Some(Ok(()))), "{ended:?}");
2049 }
2050
2051 #[test]
2055 #[ignore]
2056 fn gpu_nvfbc_keeps_its_frame_rate_on_a_still_screen() {
2057 use std::sync::atomic::AtomicU64;
2058 if !paint_root((0x30, 0x60, 0x90)) {
2059 println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
2060 return;
2061 }
2062 let s = settings(crate::encoders::codec::Codec::H264);
2063 let fps = s.target_fps;
2064 let controls = Arc::new(Controls::new(&s));
2065 let frames = Arc::new(AtomicU64::new(0));
2066 let capture = {
2067 let (controls, frames) = (controls.clone(), frames.clone());
2068 std::thread::spawn(move || {
2069 let (tx, _rx) = std::sync::mpsc::channel();
2070 run_capture(s, controls, tx, &mut |_: Vec<EncodedStripe>| {
2071 frames.fetch_add(1, Ordering::Relaxed);
2072 })
2073 })
2074 };
2075 let end = Instant::now() + Duration::from_secs(15);
2077 while frames.load(Ordering::Relaxed) == 0 && !capture.is_finished() && Instant::now() < end
2078 {
2079 std::thread::sleep(Duration::from_millis(20));
2080 }
2081 if capture.is_finished() {
2082 println!("the NvFBC path declined this host; nothing to capture");
2083 return;
2084 }
2085 std::thread::sleep(Duration::from_millis(500));
2086 let before = frames.load(Ordering::Relaxed);
2087 std::thread::sleep(Duration::from_secs(3));
2088 let rate = (frames.load(Ordering::Relaxed) - before) as f64 / 3.0;
2089 controls.stop.store(true, Ordering::Relaxed);
2090 let ended = capture.join().expect("capture thread");
2091 println!("{rate:.1} frames a second on a still screen at {fps}");
2092 assert!(matches!(ended, Some(Ok(()))), "{ended:?}");
2093 assert!(rate >= fps * 0.95, "{rate:.1} frames a second at {fps}");
2094 }
2095
2096 #[test]
2101 #[ignore]
2102 fn gpu_nvfbc_capped_crf_codes_again_what_it_coded_bound() {
2103 let Some([plain, (capped, sent, bytes, keys)]) = capped_crf_codes_again_what_it_coded_bound(
2104 |s, c, t, mut f| run_capture(s, c, t, &mut f),
2105 false,
2106 ) else {
2107 println!("the NvFBC path declined this host; nothing to capture");
2108 return;
2109 };
2110 println!(
2111 "NvFBC: uncapped {:.2} dB ({} frames while still), capped and refreshed {capped:.2} dB, \
2112 {sent} frames of {bytes} bytes while still, {keys} key frames",
2113 plain.0, plain.1
2114 );
2115 assert_eq!(keys, 0, "a key frame");
2116 assert!(sent > 0, "nothing refreshed the still page");
2117 assert!(
2118 capped >= plain.0 - 1.0,
2119 "refreshed to {capped:.2} dB against {:.2}",
2120 plain.0
2121 );
2122 }
2123
2124 #[test]
2129 #[ignore]
2130 fn gpu_nvfbc_capped_crf_comes_to_the_paint_over_once_its_peak_lets_go() {
2131 let Some([plain, (capped, sent, bytes, keys)]) = capped_crf_codes_again_what_it_coded_bound(
2132 |s, c, t, mut f| run_capture(s, c, t, &mut f),
2133 true,
2134 ) else {
2135 println!("the NvFBC path declined this host; nothing to capture");
2136 return;
2137 };
2138 println!(
2139 "NvFBC paint-over: uncapped {:.2} dB ({} frames, {} bytes while still), capped \
2140 {capped:.2} dB ({sent} frames, {bytes} bytes), {keys} key frames",
2141 plain.0, plain.1, plain.2
2142 );
2143 assert_eq!(keys, 0, "a key frame");
2144 assert!(
2145 capped >= plain.0 - 0.5,
2146 "{capped:.2} dB against the uncapped paint-over's {:.2}",
2147 plain.0
2148 );
2149 }
2150
2151 #[test]
2160 #[ignore]
2161 fn gpu_nvfbc_throughput() {
2162 let n: usize = std::env::var("NVFBC_BENCH_FRAMES")
2163 .ok()
2164 .and_then(|v| v.parse().ok())
2165 .unwrap_or(240);
2166 for codec in [
2167 crate::encoders::codec::Codec::H264,
2168 crate::encoders::codec::Codec::H265,
2169 crate::encoders::codec::Codec::Av1,
2170 ] {
2171 let mut s = settings(codec);
2172 s.auto_adjust_screen_capture_size = true;
2173 let Some(mut gpu) = open(&s) else {
2174 println!("{}: declined", codec.display());
2175 continue;
2176 };
2177 let (w, h) = (gpu.encoder.width(), gpu.encoder.height());
2178 let run = |label: &str, repaint: bool, gpu: &mut GpuCapture| {
2179 let f = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
2180 let pitch = frame_pitch(f.byte_size, f.width, f.height);
2181 gpu.encoder
2182 .encode_cuda_pitch(f.device_ptr, pitch, false, 0, 25, true)
2183 .expect("warm-up");
2184 let (mut wall, mut cpu, mut bytes, mut news) =
2185 (Duration::ZERO, Duration::ZERO, 0usize, 0usize);
2186 for i in 0..n {
2187 if repaint {
2188 let v = (i * 9) as u8;
2189 let _ = std::process::Command::new("xsetroot")
2190 .args(["-solid", &format!("#{:02x}{:02x}{:02x}", v, 0x40, 0x80)])
2191 .output();
2192 }
2193 let t = Instant::now();
2194 let c = thread_cpu();
2195 let f = gpu
2196 .nvfbc
2197 .grab_with(NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, Duration::from_millis(50))
2198 .expect("grab");
2199 let pitch = frame_pitch(f.byte_size, f.width, f.height);
2200 let pkt = gpu
2201 .encoder
2202 .encode_cuda_pitch(f.device_ptr, pitch, false, 1 + i as u64, 25, false)
2203 .expect("encode");
2204 wall += t.elapsed();
2205 cpu += thread_cpu() - c;
2206 bytes += pkt.len();
2207 news += usize::from(f.is_new);
2208 }
2209 println!(
2210 "{} {w}x{h} {label}: {:.0} fps ({:.2} ms/frame wall, {:.2} ms/frame host CPU), {news}/{n} new, {:.1} KiB/frame",
2211 codec.display(),
2212 n as f64 / wall.as_secs_f64(),
2213 wall.as_secs_f64() * 1000.0 / n as f64,
2214 cpu.as_secs_f64() * 1000.0 / n as f64,
2215 bytes as f64 / n as f64 / 1024.0
2216 );
2217 };
2218 run("steady state", false, &mut gpu);
2219 run("moving screen", true, &mut gpu);
2220 shm_upload_baseline(codec, w, h, n);
2221 }
2222 }
2223
2224 fn shm_upload_baseline(codec: crate::encoders::codec::Codec, w: u32, h: u32, n: usize) {
2229 use x11rb::connection::Connection;
2230 use x11rb::protocol::shm::ConnectionExt as ShmExt;
2231 let Ok((conn, screen)) = x11rb::connect(None) else {
2232 return;
2233 };
2234 if conn
2235 .shm_query_version()
2236 .ok()
2237 .and_then(|c| c.reply().ok())
2238 .is_none()
2239 {
2240 return;
2241 }
2242 let root = conn.setup().roots[screen].root;
2243 let Ok(mut surface) = crate::x11::ShmSurface::create(&conn, w as u16, h as u16) else {
2244 return;
2245 };
2246 let mut s = settings(codec);
2247 s.width = w as i32;
2248 s.height = h as i32;
2249 let mut enc = match NvencEncoder::new(&s, std::ptr::null()) {
2250 Ok(e) => e,
2251 Err(_) => {
2252 surface.destroy(&conn);
2253 return;
2254 }
2255 };
2256 let stride = w as usize * 4;
2257 let (mut wall, mut cpu) = (Duration::ZERO, Duration::ZERO);
2258 for i in 0..n {
2259 let t = Instant::now();
2260 let c = thread_cpu();
2261 if crate::x11::grab_frame(&conn, root, &surface, 0, 0, w as u16, h as u16).is_err() {
2262 break;
2263 }
2264 let _ = enc
2265 .encode_cpu_packed(surface.as_slice(), stride, false, i as u64, 25, i == 0)
2266 .expect("encode uploaded frame");
2267 wall += t.elapsed();
2268 cpu += thread_cpu() - c;
2269 }
2270 surface.destroy(&conn);
2271 println!(
2272 "{} {w}x{h} XShm grab + upload: {:.0} fps ({:.2} ms/frame wall, {:.2} ms/frame host CPU)",
2273 codec.display(),
2274 n as f64 / wall.as_secs_f64(),
2275 wall.as_secs_f64() * 1000.0 / n as f64,
2276 cpu.as_secs_f64() * 1000.0 / n as f64
2277 );
2278 }
2279}