Skip to main content

pixelflux/x11/
nvfbc.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! NVIDIA Framebuffer Capture (NvFBC): the zero-copy X11 capture path, where the NVIDIA driver
8//! composites the X screen straight into video memory and NVENC encodes it there.
9//!
10//! The XShm path in [`super`] is the general one, and it copies once: the X server blits the
11//! screen into a shared-memory surface, and a hardware session then uploads that surface across
12//! PCIe. NvFBC removes both. The capture subsystem lives in the NVIDIA X driver, composites each
13//! frame into a buffer it owns in video memory, and hands back a `CUdeviceptr` — which is
14//! registered with NVENC **in place**, exactly as a Wayland dmabuf is, so a frame is never read,
15//! written, or copied by the CPU between the screen and the bitstream.
16//!
17//! Three properties follow from that, and the module is built around keeping all three:
18//!
19//! 1. **Zero copy.** The device pointers are stable for as long as the geometry is (recent drivers
20//!    alternate between two), so each is registered and mapped once
21//!    (`NvencEncoder::encode_cuda_pitch`) and every later frame from it reuses that registration.
22//! 2. **Lowest latency.** The session runs in push model — the driver generates a frame when an
23//!    application damages the screen rather than on a sampling timer — and each grab is issued
24//!    with `NOWAIT_IF_NEW_FRAME_READY`, so a frame that already exists is taken immediately and
25//!    one that does not is waited for only until the frame deadline. Direct capture is allowed,
26//!    which lets the driver attach to a fullscreen unoccluded application and have it present
27//!    into the capture buffer, bypassing the X server entirely.
28//! 3. **Unrestricted frame rate.** Capture and encode share one thread with no hand-off, and
29//!    neither does any per-pixel CPU work: no shm round-trip, no content hashing on the host
30//!    (the GPU hashes a new frame's bands where it lies), no cursor blend. What the loop can
31//!    sustain is what NVENC can sustain.
32//!
33//! Requirements are checked rather than assumed, because none of them holds everywhere: the
34//! session must encode on NVENC (the device pointer is meaningless to any other encoder), the X
35//! server must be one the NVIDIA driver drives, the driver must offer NvFBC on this GPU and X
36//! server, and a watermark keeps the XShm path since blending it would mean reading the frame
37//! back. `open` reports what it found and returns `None`, and the caller streams through XShm
38//! instead. The X server is asked first and over our own connection, because the library's own
39//! answer is the one that may never come.
40//!
41//! The API is reached the way NVENC's is: `libnvidia-fbc.so.1` is loaded at run time and every
42//! entry point comes from the function table `NvFBCCreateInstance` fills, so the build links no
43//! NVIDIA library and one binary runs wherever a driver is installed. The structures below are
44//! the ABI of NvFBC 1.8 (`nvcodec-sys/headers/NvFBC.h`); each carries a version word packing its
45//! own `size_of`, which is what makes the layout assertions in `abi_tests` a real check.
46
47#![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
73/// NvFBC API version this module speaks: minor in the low byte, major above it.
74const NVFBC_VERSION: u32 = 8 | (1 << 8);
75
76/// The version word an NvFBC parameter struct carries: its own size, its revision, and the
77/// API version, packed as the SDK's `NVFBC_STRUCT_VERSION` macro packs them (the API version
78/// lands at bit 24, where only its low byte survives the 32-bit result).
79const 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;
85/// The X server is gone; the handle cannot be reused, only destroyed and recreated.
86const NVFBC_ERR_X: NVFBCSTATUS = 10;
87/// A modeset invalidated the session; recreating it resumes capture.
88const 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
96/// Capture into a CUDA buffer in video memory, the only capture type this module uses.
97const NVFBC_CAPTURE_SHARED_CUDA: u32 = 1;
98/// Track the entire X screen (the framebuffer), rather than one RandR output.
99const NVFBC_TRACKING_SCREEN: u32 = 2;
100/// The driver's native buffer format: B,G,R,A in memory, 32 bpp, needing no conversion pass.
101const NVFBC_BUFFER_FORMAT_BGRA: u32 = 5;
102/// Return whatever is in the capture buffer without waiting for the driver to render.
103const NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT: u32 = 1 << 0;
104/// Take a frame that is already waiting without blocking, otherwise wait for one.
105const 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/// A region of the tracked framebuffer, in its own coordinates.
111#[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/// A frame's pixel dimensions.
121#[repr(C)]
122#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
123struct NVFBC_SIZE {
124    w: u32,
125    h: u32,
126}
127
128/// What the driver reports about a captured frame: its geometry and byte size, whether it is
129/// one the client has not seen, how many the driver generated meanwhile, and whether it needed
130/// a conversion pass or came straight from a fullscreen application.
131#[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/// The only parameter block of the destroy / context calls: a version word.
157#[repr(C)]
158#[derive(Clone, Copy, Default)]
159struct NVFBC_VERSION_ONLY_PARAMS {
160    dwVersion: u32,
161}
162
163/// One RandR output of the tracked X screen, as `NvFBCGetStatus` reports it.
164#[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/// What the driver says about capture on this X server: whether it is possible at all, whether
183/// a session can be created right now, the framebuffer size, and the connected outputs.
184#[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/// The driver's entry points, as `NvFBCCreateInstance` fills them. The retired slots are the
251/// deprecated hardware-encoder interface and are never called.
252#[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
314/// Runtime name of the NvFBC library. The NVIDIA container toolkit injects it under the `video`
315/// driver capability, alongside `libnvidia-encode.so.1`.
316const NVFBC_LIBRARY: &str = "libnvidia-fbc.so.1";
317
318/// A driver call that failed: the status the driver returned, and its own account of it.
319///
320/// The status is kept because what to do next follows from it and nothing else: a session
321/// invalidated by a modeset is rebuilt, a lost X server needs a whole new handle, and anything
322/// else ends the capture.
323#[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
339/// One frame the driver composited into video memory.
340struct GrabbedFrame {
341    device_ptr: CUdeviceptr,
342    width: u32,
343    height: u32,
344    byte_size: u32,
345    /// Whether the driver rendered this frame since the previous grab. A static screen reports
346    /// `false` and costs nothing to skip.
347    is_new: bool,
348    /// Whether a fullscreen application presented straight into the capture buffer, bypassing
349    /// the X server.
350    direct: bool,
351    /// Whether the driver had to run a conversion pass, which would mean an extra frame copy;
352    /// the native BGRA request is chosen so that it does not.
353    post_processed: bool,
354}
355
356/// A live NvFBC capture: the loaded library, the driver's function table, the client handle, and
357/// the capture session bound to it.
358///
359/// The handle owns an OpenGL and a CUDA context that the driver binds to the thread that created
360/// it, and the captured device pointer belongs to whichever CUDA context was current at setup —
361/// which is why the whole object is built, used, and dropped on one thread, with NVENC's own
362/// primary context pushed current. Sharing that context is what makes the capture zero-copy: the
363/// pointer the driver hands back is one NVENC can register directly.
364struct NvfbcSession {
365    _lib: Library,
366    funcs: NVFBC_API_FUNCTION_LIST,
367    handle: NVFBC_SESSION_HANDLE,
368    capture_open: bool,
369    /// The region of the framebuffer being captured, and the frame size it is delivered at.
370    region: NVFBC_BOX,
371    size: NVFBC_SIZE,
372    with_cursor: bool,
373}
374
375impl NvfbcSession {
376    /// Load `libnvidia-fbc.so.1` and fill the driver's function table, erroring when the library
377    /// is absent (no NVIDIA driver, or a container without the `video` capability) or too old to
378    /// answer for this API version.
379    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    /// The driver's message for the last failure on this handle, for a log line that says what
407    /// went wrong rather than only that something did.
408    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    /// Turn a driver status into an error carrying both the code and the driver's own message.
423    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    /// Open a client handle on the X server named by the environment, letting NvFBC create and
434    /// manage the OpenGL context it captures through.
435    ///
436    /// The server is asked for [`NVIDIA_GLX`] first, over a connection of our own: it is what the
437    /// library looks up here too, and where it is missing the library's own lookup is the call
438    /// that never returns.
439    ///
440    /// The handle is bound to the calling thread, and every later call has to come from it.
441    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    /// Ask the driver whether capture is possible here and how large the framebuffer is.
468    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    /// Start capturing `region` of the framebuffer into video memory, delivered at `size`.
481    ///
482    /// The session is programmed for latency rather than for throughput smoothing: push model, so
483    /// the driver generates a frame when an application damages the screen instead of polling on
484    /// a timer; direct capture allowed, so a fullscreen unoccluded application presents into the
485    /// capture buffer without going through the X server (which the driver takes up only when the
486    /// cursor is not composited); and the native BGRA format, which needs no conversion pass and
487    /// so no extra copy. Modeset recovery is left to the driver, which rebuilds the session and
488    /// resumes on its own.
489    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    /// End the capture session, leaving the handle open for another `start`.
536    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    /// Take the next frame, waiting at most `timeout` for the driver to produce one.
550    ///
551    /// A frame the client has not seen yet is returned without waiting at all; otherwise the call
552    /// blocks until one is rendered or the timeout expires, and then returns the frame already in
553    /// the buffer with `is_new` false. Nothing is copied either way: the device pointer addresses
554    /// the driver's own capture buffer, which stays valid until the next grab.
555    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    /// The grab above, with the driver's wait behavior named explicitly.
560    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            // Rounded up: a grab that gave up ahead of its deadline would send the frame before
569            // again and leave a change landing in the time left to the next one.
570            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/// What a failed driver call leaves the capture able to do.
604#[derive(Clone, Copy, PartialEq, Eq)]
605enum Recovery {
606    /// The capture session is gone but the client handle is live: create the session again.
607    Session,
608    /// The X server the handle talks to is gone. The API has no way back from this other than a
609    /// new handle, so the old one is destroyed (which may leak the X, GLX, and GL resources it can
610    /// no longer reach) and the capture rebuilt around a fresh one.
611    Handle,
612    /// Nothing this path can do; the caller stops.
613    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
624/// The capture region and delivered frame size for a set of settings against a framebuffer of
625/// `screen`, in the terms the XShm path resolves them: the origin clamped into the screen, the
626/// size following the root when auto-adjust is on and clamped to what is left of it otherwise,
627/// and both dimensions even because every video codec needs them to be.
628///
629/// A zero-sized box means the whole tracked region, so a capture that covers the screen asks for
630/// it that way and keeps following a resize.
631fn 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    // An empty box is the whole tracked region, which is what keeps a whole-screen capture
646    // following a resize. It may only stand in for a frame that *is* the screen: a frame size
647    // differing from the captured region makes the driver scale into it, which is both a
648    // conversion pass and the end of direct capture.
649    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
663/// A running zero-copy capture: the NvFBC session and the NVENC session it feeds, on the thread
664/// that owns both contexts.
665struct GpuCapture {
666    nvfbc: NvfbcSession,
667    encoder: NvencEncoder,
668    /// The live session: its geometry is what is actually being captured and encoded, and its
669    /// rate and quality fields follow the cross-thread controls.
670    settings: CaptureSettings,
671    /// The geometry that was asked for, which a region change updates and every region is
672    /// resolved from. Kept apart from `settings` because a capture that follows the framebuffer
673    /// would otherwise take its own current size for the request and stop following.
674    request: CaptureSettings,
675    screen: NVFBC_SIZE,
676}
677
678/// The X server's word that its root window changed size (a RandR resize), on a connection of
679/// our own: the capture polls it every frame without a round trip, where asking the driver
680/// (`NvFBCGetStatus`) took 15 ms a call on an NVIDIA Xorg, a frame and more at 240 fps.
681struct 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    /// Whether the root was reconfigured since the last call, by the events queued meanwhile;
700    /// a lost connection counts as one, so the driver is asked again.
701    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
714/// How long the driver may take to answer whether it can capture this X server at all.
715const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
716
717/// Run `f` on a thread of its own and wait at most `timeout` for its answer. A call into a
718/// driver library that never returns then costs a bounded wait and a leaked thread instead of
719/// the caller, whose name `what` gives the refusal.
720fn 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
744/// The extension the NVIDIA driver's GLX registers, and the one `libnvidia-fbc` resolves on its
745/// own X connection before it will do anything: NvFBC captures the screen through a GLX context
746/// only that driver provides. An X server without it — an Xvfb, a Mesa-driven Xorg — has nothing
747/// for the library to attach to, and the lookup there is not merely futile: it can block inside
748/// the library and never return, which costs the capture thread and then the display's stop path.
749const NVIDIA_GLX: &str = "NV-GLX";
750
751/// Whether the X server this session would capture is driven by the NVIDIA driver. Asked over a
752/// connection of our own, so it costs one round trip and is answered by the X server rather than
753/// by the library whose answer cannot be relied on to come.
754fn 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
765/// The libxcb this process's libX11 calls, when that is another copy than the host's
766/// `libxcb.so.1`, which is the one the NVIDIA library calls. The manylinux policy leaves libX11 to
767/// the host but lets a wheel bundle libxcb, and a wheel whose libraries load libX11 first binds the
768/// host's libX11 to its own copy (a pcmflux wheel whose libpulse carries libxcb does). NvFBC opens
769/// its display through libX11 and then calls the host's libxcb on that connection, whose structure
770/// is laid out by the other build: a mutex read at a shifted offset makes glibc abort the process,
771/// and a flag read at one leaves the library waiting forever.
772///
773/// Opening a display whose name carries no display number fails inside `xcb_parse_display`
774/// before anything connects, which resolves libX11's lazily bound libxcb imports through
775/// libX11's own scope, so the GOT slot then names the copy every later call of libX11 reaches.
776fn 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
821/// Why the NvFBC path was not taken, for the one line that says so.
822fn 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
828/// Build the zero-copy capture for these settings, or report why it cannot serve them.
829///
830/// Everything fallible happens here, before a single frame is delivered, so the caller can fall
831/// back to the XShm path with nothing half-built. The order matters: NVENC is opened first
832/// because it owns the CUDA context, and NvFBC is then set up with that context current, which is
833/// what makes the device pointer it hands back one NVENC can register in place.
834///
835/// The path is declined, rather than failed, when it cannot be zero-copy: a codec no NVENC engine
836/// serves, software encoding requested, a device that is not NVIDIA, a watermark (which is
837/// composited into host pixels and would mean reading the frame back), a libX11 bound to another
838/// libxcb than the driver's, an X server the NVIDIA driver does not drive, or a driver that offers
839/// no NvFBC on this X server.
840fn 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    // The driver is asked before an encoder is built, so a host without NvFBC never pays for an
865    // NVENC session the XShm path would immediately build again. It is asked on a thread of its
866    // own with a bound: a library that never answers (one at odds with the X server it finds)
867    // would otherwise hold the display's video with nothing said.
868    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    // The region is resolved before the encoder is built, because the encoder is built at the
890    // size it will be fed: a session that follows the screen carries no dimensions of its own,
891    // and NVENC opens no buffer at zero.
892    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    // The capture session is created with the encoder's context current, so the device pointer it
908    // hands back is one the encoder can register.
909    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    /// Tear down in the one order that keeps the driver's bookkeeping honest: the encoder's
960    /// registration of the capture buffer goes first, because the memory behind it belongs to the
961    /// NvFBC session; then the capture session; then the CUDA context both were sharing.
962    fn drop(&mut self) {
963        self.encoder.release_external_input();
964        self.nvfbc.stop();
965        self.encoder.pop_context();
966    }
967}
968
969/// Frames between framebuffer-size polls, matching the XShm path's geometry cadence.
970const GEOMETRY_POLL_FRAMES: i32 = 30;
971
972/// Run the zero-copy X11 capture until `controls.stop` is set, or report that it cannot start.
973///
974/// `None` means NvFBC cannot serve this session and nothing was delivered, which is the caller's
975/// signal to run the XShm path instead. Anything else means the capture ran — every frame going
976/// from the driver's video-memory buffer into NVENC without a copy — and carries how it ended.
977///
978/// Each iteration:
979///
980/// 1. **Paces** to the live target frame rate, which is re-read every tick, so a rate change
981///    applies immediately and the loop never runs ahead of the session's contract.
982/// 2. **Applies the cross-thread controls** on the thread that owns the sessions: a requested key
983///    frame, a rate change, live tunables, a cursor toggle, or a region change (the last two are
984///    session parameters, so they restart the capture session in place).
985/// 3. **Grabs** the next frame, which returns immediately when one is already waiting and
986///    otherwise waits for one until the frame deadline.
987/// 4. **Follows the geometry**: a framebuffer resize restarts the capture session at the new size
988///    and reconfigures NVENC in place.
989/// 5. **Encodes** through the same send / quality / key-frame policy every full-frame encoder
990///    obeys, with the driver's own "this frame is new" report standing in for the damage signal
991///    and a hash of the new frame's bands on the GPU (`NvencEncoder::band_hashes`) saying how
992///    much of it changed, so a blinking caret stays a small change while directly rendered GL,
993///    which the X server may not report, reads as the motion it is; and hands the bitstream to
994///    the recording sink and the delivery callback.
995pub 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    // Capture and encode are the same thread here, so the re-entrant-stop guard watches it as
1006    // both.
1007    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    // No capture session runs: a rebuild the driver refused, or one a lost session or handle
1023    // asks for, which the geometry step below creates against the root as it is now.
1024    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            // An explicit live size pins the region; `<= 0` keeps following the framebuffer.
1084            gpu.request.auto_adjust_screen_capture_size = nw <= 0 || nh <= 0;
1085        }
1086        // The root watch says when the root changes size; without one the driver is asked
1087        // every GEOMETRY_POLL_FRAMES frames.
1088        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            // A resize can shrink the root before the poll above sees it, and a box resolved
1104            // against the size it had is one the driver refuses.
1105            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                        // A refusal counts as a failed grab: the root or the region may yet
1126                        // settle, and the next pass resolves the box again.
1127                        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        // A still screen leaves the grab waiting for damage until its timeout: to this frame's
1140        // deadline, or a whole frame's wait would come on top of the pacing and the encode.
1141        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        // Without Turbo the extent decides whether a new frame is sent, so it is hashed first.
1201        // Turbo sends every frame and only its cleanup reads the extent, so there it reads the
1202        // frame before's (`lagged`), hashed after that frame's delivery: beside a GPU-bound GL
1203        // client the hash waits for the GPU, half a millisecond a frame on the path to the
1204        // client. As on the XShm path, a new frame coded whole at a cleanup's quality is hashed
1205        // first instead, and one that moved is coded as motion is; a held band codes the rest of
1206        // its frame at the coarsest quantizer, so a change there costs one coarse frame. Only the
1207        // cleanup reads the extent, so without one nothing is hashed.
1208        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        // A capped constant quantizer keeps the block ledger of what it coded bound to its peak,
1263        // and codes it again once the peak lets go (`plan_refinement`); a tick that codes nothing
1264        // drains its peak. The capture places no change, so the ledger reads one as the whole
1265        // frame's, Turbo's the frame before's: a frame the released peak codes at the session's
1266        // quantizer codes the coarse picture under it again too.
1267        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                    // Its own count: a grab that keeps succeeding must not clear it.
1362                    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        // An unserved request stays armed: on an infinite GOP a key frame lost to an encode error
1382        // or a skipped tick would never come back on its own.
1383        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
1405/// Restart the capture session at a new region, size, or cursor mode, and bring NVENC to the same
1406/// geometry. The device pointer the driver returns afterwards is a new one, which the encoder's
1407/// registration follows on the next frame.
1408fn restart_session(
1409    gpu: &mut GpuCapture,
1410    region: NVFBC_BOX,
1411    size: NVFBC_SIZE,
1412    with_cursor: bool,
1413) -> Result<(), NvfbcError> {
1414    // The registration addresses the buffer the outgoing session owns, so it goes before the
1415    // session does.
1416    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
1433/// Replace the client handle, for the one failure the API offers no other way back from: the X
1434/// server the handle was opened on is gone. The capture session on it is created again by the
1435/// loop's geometry step, against the root the new handle reports.
1436///
1437/// The encoder, its CUDA context, and the frames already delivered all survive; only the driver
1438/// objects are rebuilt, in the order that keeps the encoder from holding a registration of memory
1439/// the outgoing session owns.
1440fn 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
1448/// The row stride of a captured frame: the driver's own, derived from the buffer size it
1449/// reports, or the packed width when that size is not a whole number of rows.
1450fn 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    /// Every parameter struct carries its own `size_of` inside the version word the driver
1468    /// validates, so a layout that drifts from the SDK's is rejected at the first call with no
1469    /// other symptom. These are the sizes `NvFBC.h` (NvFBC 1.8) defines.
1470    /// A probe that never answers costs the bounded wait and names itself; one that answers
1471    /// hands its value straight through.
1472    #[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    /// The gate is answered by the X server rather than by the library, so it agrees with the
1498    /// extension list that server publishes, and it answers at all: a question that could hang
1499    /// would be worth no more than the call it stands in front of.
1500    #[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    /// The guard reads which libxcb libX11 calls from libX11's own GOT after the failed open
1520    /// resolves it, and with the host's libX11 and no other copy of libxcb that is the host's, so
1521    /// NvFBC is not declined for it.
1522    #[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    /// The version words the driver compares against are the SDK's `NVFBC_*_VER` constants: the
1561    /// struct's size, its revision at bit 16, and the API version's low byte at bit 24.
1562    #[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    /// A capture covering the whole framebuffer asks for the whole tracked region, so the session
1589    /// keeps following it; anything smaller or offset is a box.
1590    #[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, &region_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    /// Both dimensions are even, because every video codec requires it, and a region is clamped
1620    /// into the framebuffer rather than running past its edge.
1621    ///
1622    /// An odd screen therefore captures an even box of it, never the whole screen delivered at
1623    /// the even size: the driver would scale into the difference, which is a conversion pass and
1624    /// the end of direct capture.
1625    #[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    /// What the capture does after a failed driver call follows from the status alone: a session
1652    /// the driver invalidated is created again, a lost X server needs a whole new handle, and
1653    /// nothing else is recoverable. The status is read from the error rather than matched in its
1654    /// text, where the driver's own message could carry the same digits.
1655    #[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    /// The stride comes from the driver's own byte count when that is a whole number of rows,
1678    /// and falls back to the packed width when it is not.
1679    #[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    /// Thread CPU time, for the per-frame CPU cost of the path independent of GPU waiting.
1699    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    /// A session that follows the screen carries no dimensions of its own, which is how Selkies
1709    /// runs a full-screen capture, and the encoder has to be built at the size the capture
1710    /// resolves rather than at the zero it was asked with. Ignored by default; run with
1711    /// `DISPLAY` on an NVIDIA X server.
1712    #[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    /// On a real NVIDIA X server: the driver reports whether it can capture this screen, and at
1736    /// what size. Ignored by default; run with `DISPLAY` pointing at an NVIDIA X server:
1737    /// `cargo test gpu_nvfbc -- --ignored --nocapture --test-threads=1`.
1738    #[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    /// The luma of the next frame `gpu` captures, waiting up to two seconds for a new one, as
1764    /// encoded and decoded.
1765    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    /// Whether two captures of the same X screen hold NvFBC sessions at once, which is what a
1783    /// second Selkies display on X11 needs to be zero-copy as well: each on its own thread, as
1784    /// the displays run them (a handle's contexts belong to the thread that made it, and two on
1785    /// one thread capture stale frames and break each other), each shows a repaint, and the
1786    /// first keeps capturing once the second closes. A driver that allows only one leaves the
1787    /// second capture on the XShm path, a correct outcome but a slower one, so the answer is
1788    /// worth knowing rather than assuming. Ignored by default.
1789    #[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    /// The whole zero-copy path against a real X server and GPU: the driver composites the root
1859    /// into video memory, NVENC encodes that buffer in place, and the decoded picture is the
1860    /// color that was painted.
1861    ///
1862    /// The zero-copy property is asserted rather than assumed, three ways: the driver reports it
1863    /// needed no post-processing pass (the native BGRA request means no conversion copy), the
1864    /// grabs hand back no more buffers than the encoder keeps registered (so each registration is
1865    /// made once and reused instead of per frame), and repainting the root changes the decoded
1866    /// color, which it could not if the encoder were reading a stale copy. Ignored by default.
1867    #[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        // The session's first frame may predate the paint: the paint is made again once it runs.
1881        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                // A repaint is one frame; ask until the driver has produced it.
1892                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    /// On an NVIDIA X server RandR can resize (a virtual screen larger than 1920x1080): the root
1970    /// shrinks under a running capture of a region narrower than it, which the driver cannot carry
1971    /// across the resize the way it does a whole-screen capture, and the region follows only
1972    /// after, as Selkies resizes (the root first, then the region it realized a little narrower).
1973    /// The capture rebuilds inside the root as it is and keeps delivering, where a rebuild against
1974    /// the size the root had was refused and ended it. Ignored by default.
1975    #[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        // Frames come on damage, so the root is repainted while they are counted.
2005        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        // A session can take over a second to open; the NvFBC path declining ends the thread.
2017        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            // Frames grabbed before the region follows meet the old box in the new root.
2030            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    /// On a real NVIDIA X server: a still screen in Turbo runs at the frame rate asked for. Each
2052    /// pass's grab waits for damage only to that frame's deadline, where a whole frame's wait on
2053    /// top of the pacing and the encode held it near 50 at 60. Ignored by default.
2054    #[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        // A session can take over a second to open; the NvFBC path declining ends the thread.
2076        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    /// On a real NVIDIA X server: a capped CRF session its peak bound while a page scrolled codes
2097    /// the still page again once a higher peak lets it go, without a key frame, sending the still
2098    /// screen's frames for it, to the uncapped session's quality
2099    /// (`capped_crf_codes_again_what_it_coded_bound`). Ignored by default.
2100    #[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    /// `gpu_nvfbc_capped_crf_codes_again_what_it_coded_bound` with the paint-over armed: the
2125    /// paint-over the binding peak spends is owed to the still page, which the refresh codes at
2126    /// it once the peak lets go, so it ends where the uncapped session's paint-over leaves it.
2127    /// Ignored by default.
2128    #[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    /// What the zero-copy path costs on a real GPU, per codec the device serves.
2152    ///
2153    /// Two numbers, because they answer different questions. The steady-state pass grabs without
2154    /// waiting, so it measures what capture and encode themselves cost with nothing else in the
2155    /// way — the ceiling the path imposes on frame rate. The moving-screen pass repaints the root
2156    /// between frames and times only the grab and encode, so the encoder is doing real work on
2157    /// changed content. In both, the host-CPU figure is the one that matters: nothing on the host
2158    /// touches a pixel, so it stays a small fraction of the wall time. Ignored by default.
2159    #[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    /// The same screen, same codec, and same GPU through the general XShm path, for the comparison
2225    /// the zero-copy path exists to win: the X server blits the root into a shared-memory surface
2226    /// and the encoder uploads that surface across PCIe, where NvFBC hands the encoder a buffer
2227    /// the driver already composited in video memory.
2228    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}