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 pointer is stable for as long as the geometry is, so it is
20//!    registered and mapped once (`NvencEncoder::encode_cuda_pitch`) and every later frame reuses
21//!    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::protocol::xproto::ConnectionExt as XprotoExt;
58
59use super::Controls;
60use crate::RustCaptureSettings;
61use crate::encoders::nvenc::NvencEncoder;
62use crate::encoders::software::{EncodedStripe, FrameTiming, StripeState};
63use crate::pipeline::{
64    DAMAGE_BAND_ROWS, Damage, EncoderQuality, decide_hw_fullframe, hashed_damage, periodic_idr_due,
65};
66use crate::recording_sink::RecordingSink;
67use nvcodec_sys::cuda::CUdeviceptr;
68
69/// NvFBC API version this module speaks: minor in the low byte, major above it.
70const NVFBC_VERSION: u32 = 8 | (1 << 8);
71
72/// The version word an NvFBC parameter struct carries: its own size, its revision, and the
73/// API version, packed as the SDK's `NVFBC_STRUCT_VERSION` macro packs them (the API version
74/// lands at bit 24, where only its low byte survives the 32-bit result).
75const fn struct_ver(size: usize, rev: u32) -> u32 {
76    (size as u32) | (rev << 16) | (NVFBC_VERSION << 24)
77}
78
79type NVFBCSTATUS = u32;
80const NVFBC_SUCCESS: NVFBCSTATUS = 0;
81/// The X server is gone; the handle cannot be reused, only destroyed and recreated.
82const NVFBC_ERR_X: NVFBCSTATUS = 10;
83/// A modeset invalidated the session; recreating it resumes capture.
84const NVFBC_ERR_MUST_RECREATE: NVFBCSTATUS = 16;
85
86type NVFBC_BOOL = u32;
87const NVFBC_FALSE: NVFBC_BOOL = 0;
88const NVFBC_TRUE: NVFBC_BOOL = 1;
89
90type NVFBC_SESSION_HANDLE = u64;
91
92/// Capture into a CUDA buffer in video memory, the only capture type this module uses.
93const NVFBC_CAPTURE_SHARED_CUDA: u32 = 1;
94/// Track the entire X screen (the framebuffer), rather than one RandR output.
95const NVFBC_TRACKING_SCREEN: u32 = 2;
96/// The driver's native buffer format: B,G,R,A in memory, 32 bpp, needing no conversion pass.
97const NVFBC_BUFFER_FORMAT_BGRA: u32 = 5;
98/// Return whatever is in the capture buffer without waiting for the driver to render.
99const NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT: u32 = 1 << 0;
100/// Take a frame that is already waiting without blocking, otherwise wait for one.
101const NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY: u32 = 1 << 2;
102
103const NVFBC_OUTPUT_MAX: usize = 5;
104const NVFBC_OUTPUT_NAME_LEN: usize = 128;
105
106/// A region of the tracked framebuffer, in its own coordinates.
107#[repr(C)]
108#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
109struct NVFBC_BOX {
110    x: u32,
111    y: u32,
112    w: u32,
113    h: u32,
114}
115
116/// A frame's pixel dimensions.
117#[repr(C)]
118#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
119struct NVFBC_SIZE {
120    w: u32,
121    h: u32,
122}
123
124/// What the driver reports about a captured frame: its geometry and byte size, whether it is
125/// one the client has not seen, how many the driver generated meanwhile, and whether it needed
126/// a conversion pass or came straight from a fullscreen application.
127#[repr(C)]
128#[derive(Clone, Copy, Default)]
129struct NVFBC_FRAME_GRAB_INFO {
130    dwWidth: u32,
131    dwHeight: u32,
132    dwByteSize: u32,
133    dwCurrentFrame: u32,
134    bIsNewFrame: NVFBC_BOOL,
135    ulTimestampUs: u64,
136    dwMissedFrames: u32,
137    bRequiredPostProcessing: NVFBC_BOOL,
138    bDirectCapture: NVFBC_BOOL,
139}
140
141#[repr(C)]
142#[derive(Clone, Copy, Default)]
143struct NVFBC_CREATE_HANDLE_PARAMS {
144    dwVersion: u32,
145    privateData: *const c_void,
146    privateDataSize: u32,
147    bExternallyManagedContext: NVFBC_BOOL,
148    glxCtx: *mut c_void,
149    glxFBConfig: *mut c_void,
150}
151
152/// The only parameter block of the destroy / context calls: a version word.
153#[repr(C)]
154#[derive(Clone, Copy, Default)]
155struct NVFBC_VERSION_ONLY_PARAMS {
156    dwVersion: u32,
157}
158
159/// One RandR output of the tracked X screen, as `NvFBCGetStatus` reports it.
160#[repr(C)]
161#[derive(Clone, Copy)]
162struct NVFBC_RANDR_OUTPUT_INFO {
163    dwId: u32,
164    name: [c_char; NVFBC_OUTPUT_NAME_LEN],
165    trackedBox: NVFBC_BOX,
166}
167
168impl Default for NVFBC_RANDR_OUTPUT_INFO {
169    fn default() -> Self {
170        Self {
171            dwId: 0,
172            name: [0; NVFBC_OUTPUT_NAME_LEN],
173            trackedBox: NVFBC_BOX::default(),
174        }
175    }
176}
177
178/// What the driver says about capture on this X server: whether it is possible at all, whether
179/// a session can be created right now, the framebuffer size, and the connected outputs.
180#[repr(C)]
181#[derive(Clone, Copy)]
182struct NVFBC_GET_STATUS_PARAMS {
183    dwVersion: u32,
184    bIsCapturePossible: NVFBC_BOOL,
185    bCurrentlyCapturing: NVFBC_BOOL,
186    bCanCreateNow: NVFBC_BOOL,
187    screenSize: NVFBC_SIZE,
188    bXRandRAvailable: NVFBC_BOOL,
189    outputs: [NVFBC_RANDR_OUTPUT_INFO; NVFBC_OUTPUT_MAX],
190    dwOutputNum: u32,
191    dwNvFBCVersion: u32,
192    bInModeset: NVFBC_BOOL,
193}
194
195impl Default for NVFBC_GET_STATUS_PARAMS {
196    fn default() -> Self {
197        Self {
198            dwVersion: 0,
199            bIsCapturePossible: 0,
200            bCurrentlyCapturing: 0,
201            bCanCreateNow: 0,
202            screenSize: NVFBC_SIZE::default(),
203            bXRandRAvailable: 0,
204            outputs: [NVFBC_RANDR_OUTPUT_INFO::default(); NVFBC_OUTPUT_MAX],
205            dwOutputNum: 0,
206            dwNvFBCVersion: 0,
207            bInModeset: 0,
208        }
209    }
210}
211
212#[repr(C)]
213#[derive(Clone, Copy, Default)]
214struct NVFBC_CREATE_CAPTURE_SESSION_PARAMS {
215    dwVersion: u32,
216    eCaptureType: u32,
217    eTrackingType: u32,
218    dwOutputId: u32,
219    captureBox: NVFBC_BOX,
220    frameSize: NVFBC_SIZE,
221    bWithCursor: NVFBC_BOOL,
222    bDisableAutoModesetRecovery: NVFBC_BOOL,
223    bRoundFrameSize: NVFBC_BOOL,
224    dwSamplingRateMs: u32,
225    bPushModel: NVFBC_BOOL,
226    bAllowDirectCapture: NVFBC_BOOL,
227}
228
229#[repr(C)]
230#[derive(Clone, Copy, Default)]
231struct NVFBC_TOCUDA_SETUP_PARAMS {
232    dwVersion: u32,
233    eBufferFormat: u32,
234}
235
236#[repr(C)]
237#[derive(Clone, Copy, Default)]
238struct NVFBC_TOCUDA_GRAB_FRAME_PARAMS {
239    dwVersion: u32,
240    dwFlags: u32,
241    pCUDADeviceBuffer: *mut c_void,
242    pFrameGrabInfo: *mut NVFBC_FRAME_GRAB_INFO,
243    dwTimeoutMs: u32,
244}
245
246/// The driver's entry points, as `NvFBCCreateInstance` fills them. The retired slots are the
247/// deprecated hardware-encoder interface and are never called.
248#[repr(C)]
249#[derive(Clone, Copy)]
250struct NVFBC_API_FUNCTION_LIST {
251    dwVersion: u32,
252    nvFBCGetLastErrorStr: Option<unsafe extern "C" fn(NVFBC_SESSION_HANDLE) -> *const c_char>,
253    nvFBCCreateHandle: Option<
254        unsafe extern "C" fn(
255            *mut NVFBC_SESSION_HANDLE,
256            *mut NVFBC_CREATE_HANDLE_PARAMS,
257        ) -> NVFBCSTATUS,
258    >,
259    nvFBCDestroyHandle: Option<
260        unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
261    >,
262    nvFBCGetStatus: Option<
263        unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_GET_STATUS_PARAMS) -> NVFBCSTATUS,
264    >,
265    nvFBCCreateCaptureSession: Option<
266        unsafe extern "C" fn(
267            NVFBC_SESSION_HANDLE,
268            *mut NVFBC_CREATE_CAPTURE_SESSION_PARAMS,
269        ) -> NVFBCSTATUS,
270    >,
271    nvFBCDestroyCaptureSession: Option<
272        unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
273    >,
274    nvFBCToSysSetUp: *mut c_void,
275    nvFBCToSysGrabFrame: *mut c_void,
276    nvFBCToCudaSetUp: Option<
277        unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_TOCUDA_SETUP_PARAMS) -> NVFBCSTATUS,
278    >,
279    nvFBCToCudaGrabFrame: Option<
280        unsafe extern "C" fn(
281            NVFBC_SESSION_HANDLE,
282            *mut NVFBC_TOCUDA_GRAB_FRAME_PARAMS,
283        ) -> NVFBCSTATUS,
284    >,
285    pad1: *mut c_void,
286    pad2: *mut c_void,
287    pad3: *mut c_void,
288    nvFBCBindContext: Option<
289        unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
290    >,
291    nvFBCReleaseContext: Option<
292        unsafe extern "C" fn(NVFBC_SESSION_HANDLE, *mut NVFBC_VERSION_ONLY_PARAMS) -> NVFBCSTATUS,
293    >,
294    pad4: *mut c_void,
295    pad5: *mut c_void,
296    pad6: *mut c_void,
297    pad7: *mut c_void,
298    nvFBCToGLSetUp: *mut c_void,
299    nvFBCToGLGrabFrame: *mut c_void,
300}
301
302impl Default for NVFBC_API_FUNCTION_LIST {
303    fn default() -> Self {
304        unsafe { std::mem::zeroed() }
305    }
306}
307
308type NvFBCCreateInstanceFn = unsafe extern "C" fn(*mut NVFBC_API_FUNCTION_LIST) -> NVFBCSTATUS;
309
310/// Runtime name of the NvFBC library. The NVIDIA container toolkit injects it under the `video`
311/// driver capability, alongside `libnvidia-encode.so.1`.
312const NVFBC_LIBRARY: &str = "libnvidia-fbc.so.1";
313
314/// A driver call that failed: the status the driver returned, and its own account of it.
315///
316/// The status is kept because what to do next follows from it and nothing else: a session
317/// invalidated by a modeset is rebuilt, a lost X server needs a whole new handle, and anything
318/// else ends the capture.
319#[derive(Debug)]
320struct NvfbcError {
321    status: NVFBCSTATUS,
322    message: String,
323}
324
325impl std::fmt::Display for NvfbcError {
326    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327        if self.message.is_empty() {
328            write!(f, "({})", self.status)
329        } else {
330            write!(f, "{} ({})", self.message, self.status)
331        }
332    }
333}
334
335/// One frame the driver composited into video memory.
336struct GrabbedFrame {
337    device_ptr: CUdeviceptr,
338    width: u32,
339    height: u32,
340    byte_size: u32,
341    /// Whether the driver rendered this frame since the previous grab. A static screen reports
342    /// `false` and costs nothing to skip.
343    is_new: bool,
344    /// Whether a fullscreen application presented straight into the capture buffer, bypassing
345    /// the X server.
346    direct: bool,
347    /// Whether the driver had to run a conversion pass, which would mean an extra frame copy;
348    /// the native BGRA request is chosen so that it does not.
349    post_processed: bool,
350}
351
352/// A live NvFBC capture: the loaded library, the driver's function table, the client handle, and
353/// the capture session bound to it.
354///
355/// The handle owns an OpenGL and a CUDA context that the driver binds to the thread that created
356/// it, and the captured device pointer belongs to whichever CUDA context was current at setup —
357/// which is why the whole object is built, used, and dropped on one thread, with NVENC's own
358/// primary context pushed current. Sharing that context is what makes the capture zero-copy: the
359/// pointer the driver hands back is one NVENC can register directly.
360struct NvfbcSession {
361    _lib: Library,
362    funcs: NVFBC_API_FUNCTION_LIST,
363    handle: NVFBC_SESSION_HANDLE,
364    capture_open: bool,
365    /// The region of the framebuffer being captured, and the frame size it is delivered at.
366    region: NVFBC_BOX,
367    size: NVFBC_SIZE,
368    with_cursor: bool,
369}
370
371impl NvfbcSession {
372    /// Load `libnvidia-fbc.so.1` and fill the driver's function table, erroring when the library
373    /// is absent (no NVIDIA driver, or a container without the `video` capability) or too old to
374    /// answer for this API version.
375    fn load() -> Result<(Library, NVFBC_API_FUNCTION_LIST), String> {
376        unsafe {
377            let lib = Library::new(NVFBC_LIBRARY)
378                .map_err(|e| format!("could not load {NVFBC_LIBRARY}: {e}"))?;
379            let create: libloading::Symbol<NvFBCCreateInstanceFn> = lib
380                .get(b"NvFBCCreateInstance\0")
381                .map_err(|e| format!("missing symbol NvFBCCreateInstance: {e}"))?;
382            let mut funcs = NVFBC_API_FUNCTION_LIST {
383                dwVersion: NVFBC_VERSION,
384                ..Default::default()
385            };
386            let st = create(&mut funcs);
387            if st != NVFBC_SUCCESS {
388                return Err(format!("NvFBCCreateInstance failed ({st})"));
389            }
390            if funcs.nvFBCCreateHandle.is_none()
391                || funcs.nvFBCGetStatus.is_none()
392                || funcs.nvFBCCreateCaptureSession.is_none()
393                || funcs.nvFBCToCudaSetUp.is_none()
394                || funcs.nvFBCToCudaGrabFrame.is_none()
395            {
396                return Err("the driver's NvFBC table has no CUDA capture entry points".into());
397            }
398            Ok((lib, funcs))
399        }
400    }
401
402    /// The driver's message for the last failure on this handle, for a log line that says what
403    /// went wrong rather than only that something did.
404    fn last_error(&self) -> String {
405        match self.funcs.nvFBCGetLastErrorStr {
406            Some(f) => unsafe {
407                let p = f(self.handle);
408                if p.is_null() {
409                    String::new()
410                } else {
411                    CStr::from_ptr(p).to_string_lossy().into_owned()
412                }
413            },
414            None => String::new(),
415        }
416    }
417
418    /// Turn a driver status into an error carrying both the code and the driver's own message.
419    fn fail(&self, what: &str, status: NVFBCSTATUS) -> NvfbcError {
420        let detail = self.last_error();
421        let message = if detail.is_empty() {
422            format!("{what} failed")
423        } else {
424            format!("{what} failed: {detail}")
425        };
426        NvfbcError { status, message }
427    }
428
429    /// Open a client handle on the X server named by the environment, letting NvFBC create and
430    /// manage the OpenGL context it captures through.
431    ///
432    /// The server is asked for [`NVIDIA_GLX`] first, over a connection of our own: it is what the
433    /// library looks up here too, and where it is missing the library's own lookup is the call
434    /// that never returns.
435    ///
436    /// The handle is bound to the calling thread, and every later call has to come from it.
437    fn open() -> Result<Self, String> {
438        if !nvidia_driven_x_server()? {
439            return Err(format!("the X server offers no {NVIDIA_GLX}"));
440        }
441        let (lib, funcs) = Self::load()?;
442        let mut handle: NVFBC_SESSION_HANDLE = 0;
443        let mut params = NVFBC_CREATE_HANDLE_PARAMS {
444            dwVersion: struct_ver(std::mem::size_of::<NVFBC_CREATE_HANDLE_PARAMS>(), 2),
445            bExternallyManagedContext: NVFBC_FALSE,
446            ..Default::default()
447        };
448        let st = unsafe { (funcs.nvFBCCreateHandle.unwrap())(&mut handle, &mut params) };
449        if st != NVFBC_SUCCESS {
450            return Err(format!("NvFBCCreateHandle failed ({st})"));
451        }
452        Ok(Self {
453            _lib: lib,
454            funcs,
455            handle,
456            capture_open: false,
457            region: NVFBC_BOX::default(),
458            size: NVFBC_SIZE::default(),
459            with_cursor: false,
460        })
461    }
462
463    /// Ask the driver whether capture is possible here and how large the framebuffer is.
464    fn status(&self) -> Result<NVFBC_GET_STATUS_PARAMS, NvfbcError> {
465        let mut params = NVFBC_GET_STATUS_PARAMS {
466            dwVersion: struct_ver(std::mem::size_of::<NVFBC_GET_STATUS_PARAMS>(), 2),
467            ..Default::default()
468        };
469        let st = unsafe { (self.funcs.nvFBCGetStatus.unwrap())(self.handle, &mut params) };
470        if st != NVFBC_SUCCESS {
471            return Err(self.fail("NvFBCGetStatus", st));
472        }
473        Ok(params)
474    }
475
476    /// Start capturing `region` of the framebuffer into video memory, delivered at `size`.
477    ///
478    /// The session is programmed for latency rather than for throughput smoothing: push model, so
479    /// the driver generates a frame when an application damages the screen instead of polling on
480    /// a timer; direct capture allowed, so a fullscreen unoccluded application presents into the
481    /// capture buffer without going through the X server (which the driver takes up only when the
482    /// cursor is not composited); and the native BGRA format, which needs no conversion pass and
483    /// so no extra copy. Modeset recovery is left to the driver, which rebuilds the session and
484    /// resumes on its own.
485    fn start(
486        &mut self,
487        region: NVFBC_BOX,
488        size: NVFBC_SIZE,
489        with_cursor: bool,
490    ) -> Result<(), NvfbcError> {
491        self.stop();
492        let mut params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS {
493            dwVersion: struct_ver(
494                std::mem::size_of::<NVFBC_CREATE_CAPTURE_SESSION_PARAMS>(),
495                6,
496            ),
497            eCaptureType: NVFBC_CAPTURE_SHARED_CUDA,
498            eTrackingType: NVFBC_TRACKING_SCREEN,
499            dwOutputId: 0,
500            captureBox: region,
501            frameSize: size,
502            bWithCursor: if with_cursor { NVFBC_TRUE } else { NVFBC_FALSE },
503            bDisableAutoModesetRecovery: NVFBC_FALSE,
504            bRoundFrameSize: NVFBC_FALSE,
505            dwSamplingRateMs: 0,
506            bPushModel: NVFBC_TRUE,
507            bAllowDirectCapture: NVFBC_TRUE,
508        };
509        let st =
510            unsafe { (self.funcs.nvFBCCreateCaptureSession.unwrap())(self.handle, &mut params) };
511        if st != NVFBC_SUCCESS {
512            return Err(self.fail("NvFBCCreateCaptureSession", st));
513        }
514        self.capture_open = true;
515        let mut setup = NVFBC_TOCUDA_SETUP_PARAMS {
516            dwVersion: struct_ver(std::mem::size_of::<NVFBC_TOCUDA_SETUP_PARAMS>(), 1),
517            eBufferFormat: NVFBC_BUFFER_FORMAT_BGRA,
518        };
519        let st = unsafe { (self.funcs.nvFBCToCudaSetUp.unwrap())(self.handle, &mut setup) };
520        if st != NVFBC_SUCCESS {
521            let msg = self.fail("NvFBCToCudaSetUp", st);
522            self.stop();
523            return Err(msg);
524        }
525        self.region = region;
526        self.size = size;
527        self.with_cursor = with_cursor;
528        Ok(())
529    }
530
531    /// End the capture session, leaving the handle open for another `start`.
532    fn stop(&mut self) {
533        if !self.capture_open {
534            return;
535        }
536        let mut params = NVFBC_VERSION_ONLY_PARAMS {
537            dwVersion: struct_ver(std::mem::size_of::<NVFBC_VERSION_ONLY_PARAMS>(), 1),
538        };
539        if let Some(f) = self.funcs.nvFBCDestroyCaptureSession {
540            unsafe { f(self.handle, &mut params) };
541        }
542        self.capture_open = false;
543    }
544
545    /// Take the next frame, waiting at most `timeout` for the driver to produce one.
546    ///
547    /// A frame the client has not seen yet is returned without waiting at all; otherwise the call
548    /// blocks until one is rendered or the timeout expires, and then returns the frame already in
549    /// the buffer with `is_new` false. Nothing is copied either way: the device pointer addresses
550    /// the driver's own capture buffer, which stays valid until the next grab.
551    fn grab(&mut self, timeout: Duration) -> Result<GrabbedFrame, NvfbcError> {
552        self.grab_with(NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY, timeout)
553    }
554
555    /// The grab above, with the driver's wait behavior named explicitly.
556    fn grab_with(&mut self, flags: u32, timeout: Duration) -> Result<GrabbedFrame, NvfbcError> {
557        let mut device_ptr: CUdeviceptr = 0;
558        let mut info = NVFBC_FRAME_GRAB_INFO::default();
559        let mut params = NVFBC_TOCUDA_GRAB_FRAME_PARAMS {
560            dwVersion: struct_ver(std::mem::size_of::<NVFBC_TOCUDA_GRAB_FRAME_PARAMS>(), 2),
561            dwFlags: flags,
562            pCUDADeviceBuffer: &mut device_ptr as *mut CUdeviceptr as *mut c_void,
563            pFrameGrabInfo: &mut info,
564            dwTimeoutMs: timeout.as_millis().clamp(1, u32::MAX as u128) as u32,
565        };
566        let st = unsafe { (self.funcs.nvFBCToCudaGrabFrame.unwrap())(self.handle, &mut params) };
567        if st != NVFBC_SUCCESS {
568            return Err(self.fail("NvFBCToCudaGrabFrame", st));
569        }
570        Ok(GrabbedFrame {
571            device_ptr,
572            width: info.dwWidth,
573            height: info.dwHeight,
574            byte_size: info.dwByteSize,
575            is_new: info.bIsNewFrame == NVFBC_TRUE,
576            direct: info.bDirectCapture == NVFBC_TRUE,
577            post_processed: info.bRequiredPostProcessing == NVFBC_TRUE,
578        })
579    }
580}
581
582impl Drop for NvfbcSession {
583    fn drop(&mut self) {
584        self.stop();
585        let mut params = NVFBC_VERSION_ONLY_PARAMS {
586            dwVersion: struct_ver(std::mem::size_of::<NVFBC_VERSION_ONLY_PARAMS>(), 1),
587        };
588        if let Some(f) = self.funcs.nvFBCDestroyHandle {
589            unsafe { f(self.handle, &mut params) };
590        }
591    }
592}
593
594/// What a failed driver call leaves the capture able to do.
595#[derive(Clone, Copy, PartialEq, Eq)]
596enum Recovery {
597    /// The capture session is gone but the client handle is live: create the session again.
598    Session,
599    /// The X server the handle talks to is gone. The API has no way back from this other than a
600    /// new handle, so the old one is destroyed (which may leak the X, GLX, and GL resources it can
601    /// no longer reach) and the capture rebuilt around a fresh one.
602    Handle,
603    /// Nothing this path can do; the caller stops.
604    None,
605}
606
607fn recovery_for(err: &NvfbcError) -> Recovery {
608    match err.status {
609        NVFBC_ERR_MUST_RECREATE => Recovery::Session,
610        NVFBC_ERR_X => Recovery::Handle,
611        _ => Recovery::None,
612    }
613}
614
615/// The capture region and delivered frame size for a set of settings against a framebuffer of
616/// `screen`, in the terms the XShm path resolves them: the origin clamped into the screen, the
617/// size following the root when auto-adjust is on and clamped to what is left of it otherwise,
618/// and both dimensions even because every video codec needs them to be.
619///
620/// A zero-sized box means the whole tracked region, so a capture that covers the screen asks for
621/// it that way and keeps following a resize.
622fn resolve_region(screen: NVFBC_SIZE, s: &RustCaptureSettings) -> (NVFBC_BOX, NVFBC_SIZE) {
623    let (w, h) = super::resolve_dims(
624        screen.w.min(u16::MAX as u32) as u16,
625        screen.h.min(u16::MAX as u32) as u16,
626        s,
627    );
628    let x = super::clamp_offset(s.capture_x, screen.w.min(u16::MAX as u32) as u16).max(0) as u32;
629    let y = super::clamp_offset(s.capture_y, screen.h.min(u16::MAX as u32) as u16).max(0) as u32;
630    let size = NVFBC_SIZE {
631        w: w as u32,
632        h: h as u32,
633    };
634    // An empty box is the whole tracked region, which is what keeps a whole-screen capture
635    // following a resize. It may only stand in for a frame that *is* the screen: a frame size
636    // differing from the captured region makes the driver scale into it, which is both a
637    // conversion pass and the end of direct capture.
638    let full = x == 0 && y == 0 && size.w == screen.w && size.h == screen.h;
639    let region = if full {
640        NVFBC_BOX::default()
641    } else {
642        NVFBC_BOX {
643            x,
644            y,
645            w: size.w,
646            h: size.h,
647        }
648    };
649    (region, size)
650}
651
652/// A running zero-copy capture: the NvFBC session and the NVENC session it feeds, on the thread
653/// that owns both contexts.
654struct GpuCapture {
655    nvfbc: NvfbcSession,
656    encoder: NvencEncoder,
657    /// The live session: its geometry is what is actually being captured and encoded, and its
658    /// rate and quality fields follow the cross-thread controls.
659    settings: RustCaptureSettings,
660    /// The geometry that was asked for, which a region change updates and every region is
661    /// resolved from. Kept apart from `settings` because a capture that follows the framebuffer
662    /// would otherwise take its own current size for the request and stop following.
663    request: RustCaptureSettings,
664    screen: NVFBC_SIZE,
665}
666
667/// How long the driver may take to answer whether it can capture this X server at all.
668const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
669
670/// Run `f` on a thread of its own and wait at most `timeout` for its answer. A call into a
671/// driver library that never returns then costs a bounded wait and a leaked thread instead of
672/// the caller, whose name `what` gives the refusal.
673fn answered_within<T: Send + 'static>(
674    timeout: Duration,
675    what: &str,
676    f: impl FnOnce() -> Result<T, String> + Send + 'static,
677) -> Result<T, String> {
678    let (tx, rx) = std::sync::mpsc::channel();
679    std::thread::Builder::new()
680        .name(format!("pf-{}-probe", what.to_lowercase()))
681        .spawn(move || {
682            let _ = tx.send(f());
683        })
684        .map_err(|e| format!("{what} probe thread: {e}"))?;
685    match rx.recv_timeout(timeout) {
686        Ok(answer) => answer,
687        Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(format!(
688            "{what} did not answer within {} s",
689            timeout.as_secs()
690        )),
691        Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
692            Err(format!("{what} ended without an answer"))
693        }
694    }
695}
696
697/// The extension the NVIDIA driver's GLX registers, and the one `libnvidia-fbc` resolves on its
698/// own X connection before it will do anything: NvFBC captures the screen through a GLX context
699/// only that driver provides. An X server without it — an Xvfb, a Mesa-driven Xorg — has nothing
700/// for the library to attach to, and the lookup there is not merely futile: it can block inside
701/// the library and never return, which costs the capture thread and then the display's stop path.
702const NVIDIA_GLX: &str = "NV-GLX";
703
704/// Whether the X server this session would capture is driven by the NVIDIA driver. Asked over a
705/// connection of our own, so it costs one round trip and is answered by the X server rather than
706/// by the library whose answer cannot be relied on to come.
707fn nvidia_driven_x_server() -> Result<bool, String> {
708    let (conn, _) = x11rb::connect(None).map_err(|e| format!("X11 connect failed: {e}"))?;
709    let cookie = conn
710        .query_extension(NVIDIA_GLX.as_bytes())
711        .map_err(|e| format!("{NVIDIA_GLX} query failed: {e}"))?;
712    let reply = cookie
713        .reply()
714        .map_err(|e| format!("{NVIDIA_GLX} query failed: {e}"))?;
715    Ok(reply.present)
716}
717
718/// The libxcb this process's libX11 calls, when that is another copy than the host's
719/// `libxcb.so.1`, which is the one the NVIDIA library calls. The manylinux policy leaves libX11 to
720/// the host but lets a wheel bundle libxcb, and a wheel whose libraries load libX11 first binds the
721/// host's libX11 to its own copy (a pcmflux wheel whose libpulse carries libxcb does). NvFBC opens
722/// its display through libX11 and then calls the host's libxcb on that connection, whose structure
723/// is laid out by the other build: a mutex read at a shifted offset makes glibc abort the process,
724/// and a flag read at one leaves the library waiting forever.
725///
726/// Opening a display whose name carries no display number fails inside `xcb_parse_display`
727/// before anything connects, which resolves libX11's lazily bound libxcb imports through
728/// libX11's own scope, so the GOT slot then names the copy every later call of libX11 reaches.
729fn foreign_libxcb() -> Option<String> {
730    unsafe {
731        let x11 = libc::dlopen(c"libX11.so.6".as_ptr(), libc::RTLD_LAZY | libc::RTLD_NOLOAD);
732        if x11.is_null() {
733            return None;
734        }
735        let open = libc::dlsym(x11, c"XOpenDisplay".as_ptr());
736        if !open.is_null() {
737            let open: unsafe extern "C" fn(*const c_char) -> *mut c_void =
738                std::mem::transmute(open);
739            open(c"pixelflux".as_ptr());
740        }
741        libc::dlclose(x11);
742        let bound = crate::nvgpufilter::bound_import("libX11.so", b"xcb_parse_display")?;
743        let xcb = libc::dlopen(c"libxcb.so.1".as_ptr(), libc::RTLD_LAZY | libc::RTLD_NOLOAD);
744        if xcb.is_null() {
745            return None;
746        }
747        let host = libc::dlsym(xcb, c"xcb_parse_display".as_ptr());
748        libc::dlclose(xcb);
749        let object = |addr: *const c_void| {
750            let mut info: libc::Dl_info = std::mem::zeroed();
751            (libc::dladdr(addr, &mut info) != 0
752                && !info.dli_sname.is_null()
753                && !info.dli_fname.is_null())
754            .then(|| {
755                (
756                    info.dli_fbase,
757                    CStr::from_ptr(info.dli_sname),
758                    CStr::from_ptr(info.dli_fname),
759                )
760            })
761        };
762        let (bound_base, bound_name, bound_path) = object(bound as *const c_void)?;
763        let (host_base, _, host_path) = object(host)?;
764        (bound_name.to_bytes() == b"xcb_parse_display" && bound_base != host_base).then(|| {
765            format!(
766                "libX11 calls {}, not the {} the NVIDIA library calls on the same connection",
767                bound_path.to_string_lossy(),
768                host_path.to_string_lossy()
769            )
770        })
771    }
772}
773
774/// Why the NvFBC path was not taken, for the one line that says so.
775fn declined(reason: &str) -> Option<GpuCapture> {
776    println!("[X11] Zero-copy capture (NvFBC) unavailable: {reason}.");
777    crate::report::capture_declined("NvFBC", reason);
778    None
779}
780
781/// Build the zero-copy capture for these settings, or report why it cannot serve them.
782///
783/// Everything fallible happens here, before a single frame is delivered, so the caller can fall
784/// back to the XShm path with nothing half-built. The order matters: NVENC is opened first
785/// because it owns the CUDA context, and NvFBC is then set up with that context current, which is
786/// what makes the device pointer it hands back one NVENC can register in place.
787///
788/// The path is declined, rather than failed, when it cannot be zero-copy: a codec no NVENC engine
789/// serves, software encoding requested, a device that is not NVIDIA, a watermark (which is
790/// composited into host pixels and would mean reading the frame back), a libX11 bound to another
791/// libxcb than the driver's, an X server the NVIDIA driver does not drive, or a driver that offers
792/// no NvFBC on this X server.
793fn open(settings: &RustCaptureSettings) -> Option<GpuCapture> {
794    if !settings.codec.is_video() {
795        return declined("the codec is JPEG");
796    }
797    if settings.use_cpu || settings.encode_node_index == -1 {
798        return declined("software encoding was requested");
799    }
800    if !settings.watermark_path.is_empty() {
801        return declined("a watermark is composited into the frame");
802    }
803    let node = settings.encode_node_index.max(0);
804    let driver = crate::get_gpu_driver(node);
805    if !crate::driver_selects_nvenc(&driver) {
806        return declined(&format!("the encode node's driver is {driver}"));
807    }
808    if crate::encoders::ten_bit_is_softwares(settings) {
809        return declined(&format!(
810            "NVENC encodes no 10-bit {}, which the software encoder does",
811            settings.codec.display()
812        ));
813    }
814    if let Some(reason) = foreign_libxcb() {
815        return declined(&reason);
816    }
817    // The driver is asked before an encoder is built, so a host without NvFBC never pays for an
818    // NVENC session the XShm path would immediately build again. It is asked on a thread of its
819    // own with a bound: a library that never answers (one at odds with the X server it finds)
820    // would otherwise hold the display's video with nothing said.
821    let probed = answered_within(PROBE_TIMEOUT, "NvFBC", || {
822        let nvfbc = NvfbcSession::open()?;
823        let status = nvfbc.status().map_err(|e| e.to_string())?;
824        if status.bIsCapturePossible != NVFBC_TRUE {
825            return Err("the driver reports capture is not possible on this X server".into());
826        }
827        if status.bInModeset == NVFBC_TRUE {
828            return Err("the X server is in a modeset".into());
829        }
830        Ok(status.screenSize)
831    });
832    let screen = match probed {
833        Ok(v) => v,
834        Err(e) => return declined(&e),
835    };
836    crate::report::zero_copy_available(true);
837    let mut nvfbc = match NvfbcSession::open() {
838        Ok(v) => v,
839        Err(e) => return declined(&e),
840    };
841
842    // The region is resolved before the encoder is built, because the encoder is built at the
843    // size it will be fed: a session that follows the screen carries no dimensions of its own,
844    // and NVENC opens no buffer at zero.
845    let (region, size) = resolve_region(screen, settings);
846    let request = settings.clone();
847    let mut settings = settings.clone();
848    settings.width = size.w as i32;
849    settings.height = size.h as i32;
850
851    let mut encoder = match NvencEncoder::new(&settings, std::ptr::null()) {
852        Ok(enc) => enc,
853        Err(e) => {
854            return declined(&format!(
855                "NVENC {} did not open: {e}",
856                settings.codec.display()
857            ));
858        }
859    };
860    // The capture session is created with the encoder's context current, so the device pointer it
861    // hands back is one the encoder can register.
862    if !encoder.push_context() {
863        return declined("the encoder's CUDA context could not be made current");
864    }
865    if let Err(e) = nvfbc.start(region, size, settings.capture_cursor) {
866        encoder.pop_context();
867        return declined(&e.to_string());
868    }
869
870    settings.width = nvfbc.size.w as i32;
871    settings.height = nvfbc.size.h as i32;
872    if (settings.width != encoder.width() as i32 || settings.height != encoder.height() as i32)
873        && let Err(e) = encoder.reconfigure_resolution(&settings)
874    {
875        nvfbc.stop();
876        encoder.pop_context();
877        return declined(&format!("NVENC could not follow the captured size: {e}"));
878    }
879    println!(
880        "[X11] Zero-copy capture (NvFBC): {}x{} composited into video memory, encoded in place on NVENC.",
881        settings.width, settings.height
882    );
883    crate::report::capture("NvFBC", true);
884    crate::report::hardware_encoder(
885        encoder.device_name(),
886        crate::encoders::driver_name(&driver),
887        node,
888    );
889    crate::log_stream_settings_of(
890        "X11",
891        &settings,
892        1,
893        Some(("NVENC", true)),
894        None,
895        true,
896        encoder.is_fullcolor(),
897        false,
898    );
899    crate::report::bit_depth(encoder.bit_depth());
900    Some(GpuCapture {
901        nvfbc,
902        encoder,
903        settings,
904        request,
905        screen,
906    })
907}
908
909impl Drop for GpuCapture {
910    /// Tear down in the one order that keeps the driver's bookkeeping honest: the encoder's
911    /// registration of the capture buffer goes first, because the memory behind it belongs to the
912    /// NvFBC session; then the capture session; then the CUDA context both were sharing.
913    fn drop(&mut self) {
914        self.encoder.release_external_input();
915        self.nvfbc.stop();
916        self.encoder.pop_context();
917    }
918}
919
920/// Frames between framebuffer-size polls, matching the XShm path's geometry cadence.
921const GEOMETRY_POLL_FRAMES: i32 = 30;
922
923/// Run the zero-copy X11 capture until `controls.stop` is set, or report that it cannot start.
924///
925/// `None` means NvFBC cannot serve this session and nothing was delivered, which is the caller's
926/// signal to run the XShm path instead. Anything else means the capture ran — every frame going
927/// from the driver's video-memory buffer into NVENC without a copy — and carries how it ended.
928///
929/// Each iteration:
930///
931/// 1. **Paces** to the live target frame rate, which is re-read every tick, so a rate change
932///    applies immediately and the loop never runs ahead of the session's contract.
933/// 2. **Applies the cross-thread controls** on the thread that owns the sessions: a requested key
934///    frame, a rate change, live tunables, a cursor toggle, or a region change (the last two are
935///    session parameters, so they restart the capture session in place).
936/// 3. **Grabs** the next frame, which returns immediately when one is already waiting and
937///    otherwise waits for one until the frame deadline.
938/// 4. **Follows the geometry**: a framebuffer resize restarts the capture session at the new size
939///    and reconfigures NVENC in place.
940/// 5. **Encodes** through the same send / quality / key-frame policy every full-frame encoder
941///    obeys, with the driver's own "this frame is new" report standing in for the damage signal
942///    and a hash of the new frame's bands on the GPU (`NvencEncoder::band_hashes`) saying how
943///    much of it changed, so a blinking caret stays a small change while directly rendered GL,
944///    which the X server may not report, reads as the motion it is; and hands the bitstream to
945///    the recording sink and the delivery callback.
946pub fn run_capture<F>(
947    settings: RustCaptureSettings,
948    controls: Arc<Controls>,
949    encode_tid_tx: std::sync::mpsc::Sender<std::thread::ThreadId>,
950    on_frame: &mut F,
951) -> Option<Result<(), String>>
952where
953    F: FnMut(Vec<EncodedStripe>),
954{
955    let mut gpu = open(&settings)?;
956    // Capture and encode are the same thread here, so the re-entrant-stop guard watches it as
957    // both.
958    let _ = encode_tid_tx.send(std::thread::current().id());
959    controls
960        .codec
961        .store(gpu.encoder.codec().id(), Ordering::Relaxed);
962
963    let recording_sink = RecordingSink::try_bind(&settings.recording_socket, settings.target_fps);
964    let mut state = StripeState::default();
965    let mut hashes: Vec<u64> = Vec::new();
966    let mut lagged = Damage::None;
967    let mut frame_counter: u16 = 0;
968    let mut pending_force_idr = false;
969    let mut next_frame = Instant::now();
970    let mut geometry_check = GEOMETRY_POLL_FRAMES;
971    let mut grab_errors = 0u32;
972    let mut encode_errors = 0u32;
973    let mut last_log = Instant::now();
974    let mut sent_frames: u64 = 0;
975    let mut new_frames: u64 = 0;
976    let mut direct_frames: u64 = 0;
977
978    while !controls.stop.load(Ordering::Relaxed) {
979        let fps = f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
980        let frame_dur = Duration::from_secs_f64(1.0 / fps.max(1.0));
981        let now = Instant::now();
982        if now < next_frame {
983            std::thread::sleep(next_frame - now);
984        }
985        next_frame += frame_dur;
986        let now = Instant::now();
987        if next_frame < now {
988            next_frame = now;
989        }
990        if controls.stop.load(Ordering::Relaxed) {
991            break;
992        }
993
994        for report in std::mem::take(&mut *controls.reference_reports.lock().unwrap()) {
995            if !gpu.encoder.take_report(report) {
996                pending_force_idr = true;
997            }
998        }
999        if controls.force_idr.swap(false, Ordering::Relaxed)
1000            || recording_sink
1001                .as_ref()
1002                .is_some_and(|s| s.should_force_idr())
1003        {
1004            pending_force_idr = true;
1005        }
1006        if controls.rate_dirty.swap(false, Ordering::Acquire) {
1007            gpu.settings.video_bitrate_kbps = controls.bitrate_kbps.load(Ordering::Relaxed);
1008            gpu.settings.video_vbv_multiplier =
1009                controls.vbv_mult_milli.load(Ordering::Relaxed) as f64 / 1000.0;
1010            gpu.settings.target_fps = fps;
1011            gpu.encoder.reconfigure_rate(&gpu.settings);
1012        }
1013        if controls.tunables_dirty.swap(false, Ordering::Acquire)
1014            && let Some(t) = controls.tunables.lock().unwrap().take()
1015        {
1016            t.apply_to(&mut gpu.settings);
1017        }
1018
1019        let want_cursor = controls.capture_cursor.load(Ordering::Relaxed);
1020        let mut recheck_geometry = controls.region_dirty.swap(false, Ordering::Acquire);
1021        if recheck_geometry {
1022            let (nx, ny, nw, nh) = *controls.region.lock().unwrap();
1023            gpu.request.capture_x = nx;
1024            gpu.request.capture_y = ny;
1025            gpu.request.width = nw;
1026            gpu.request.height = nh;
1027            // An explicit live size pins the region; `<= 0` keeps following the framebuffer.
1028            gpu.request.auto_adjust_screen_capture_size = nw <= 0 || nh <= 0;
1029        }
1030        geometry_check -= 1;
1031        if geometry_check <= 0 {
1032            geometry_check = GEOMETRY_POLL_FRAMES;
1033            if let Ok(status) = gpu.nvfbc.status() {
1034                recheck_geometry |= status.screenSize != gpu.screen;
1035                gpu.screen = status.screenSize;
1036            }
1037        }
1038        if recheck_geometry || want_cursor != gpu.nvfbc.with_cursor {
1039            let (region, size) = resolve_region(gpu.screen, &gpu.request);
1040            if region != gpu.nvfbc.region
1041                || size != gpu.nvfbc.size
1042                || want_cursor != gpu.nvfbc.with_cursor
1043            {
1044                match restart_session(&mut gpu, region, size, want_cursor) {
1045                    Ok(()) => {
1046                        state = StripeState::default();
1047                        hashes.clear();
1048                        lagged = Damage::None;
1049                        pending_force_idr = true;
1050                    }
1051                    Err(e) => {
1052                        return Some(Err(format!("NvFBC could not follow the new geometry: {e}")));
1053                    }
1054                }
1055            }
1056        }
1057
1058        let frame = match gpu.nvfbc.grab(frame_dur) {
1059            Ok(f) => {
1060                grab_errors = 0;
1061                f
1062            }
1063            Err(e) => {
1064                grab_errors += 1;
1065                let recovery = recovery_for(&e);
1066                if recovery == Recovery::None || grab_errors > 5 {
1067                    return Some(Err(format!("NvFBC capture ended: {e}")));
1068                }
1069                let (region, size) = resolve_region(gpu.screen, &gpu.request);
1070                let rebuilt = if recovery == Recovery::Handle {
1071                    eprintln!("[X11] NvFBC lost the X server ({e}); rebuilding the client handle.");
1072                    rebuild_handle(&mut gpu, region, size, want_cursor)
1073                } else {
1074                    eprintln!("[X11] NvFBC grab failed ({e}); rebuilding the capture session.");
1075                    restart_session(&mut gpu, region, size, want_cursor).map_err(|e| e.to_string())
1076                };
1077                if let Err(e) = rebuilt {
1078                    return Some(Err(format!("NvFBC capture could not be rebuilt: {e}")));
1079                }
1080                state = StripeState::default();
1081                hashes.clear();
1082                lagged = Damage::None;
1083                pending_force_idr = true;
1084                continue;
1085            }
1086        };
1087        let grabbed_ns = crate::wayland::host::now_ns();
1088        if frame.device_ptr == 0 || frame.width == 0 || frame.height == 0 {
1089            continue;
1090        }
1091        new_frames += frame.is_new as u64;
1092        direct_frames += frame.direct as u64;
1093
1094        if frame.width != gpu.encoder.width() || frame.height != gpu.encoder.height() {
1095            gpu.settings.width = frame.width as i32;
1096            gpu.settings.height = frame.height as i32;
1097            if let Err(e) = gpu.encoder.reconfigure_resolution(&gpu.settings) {
1098                return Some(Err(format!(
1099                    "NVENC could not follow the captured size: {e}"
1100                )));
1101            }
1102            state = StripeState::default();
1103            hashes.clear();
1104            lagged = Damage::None;
1105            pending_force_idr = true;
1106        }
1107
1108        let pitch = frame_pitch(frame.byte_size, frame.width, frame.height);
1109        let hash = |encoder: &mut NvencEncoder, hashes: &mut Vec<u64>| {
1110            encoder
1111                .band_hashes(
1112                    frame.device_ptr,
1113                    pitch,
1114                    frame.width,
1115                    frame.height,
1116                    DAMAGE_BAND_ROWS as u32,
1117                )
1118                .map_or(Damage::Unknown, |h| hashed_damage(hashes, h))
1119        };
1120        // Without Turbo the extent decides whether a new frame is sent, so it is hashed first.
1121        // Turbo sends every frame and only its cleanup reads the extent, so there it reads the
1122        // frame before's (`lagged`), hashed after that frame's delivery: beside a GPU-bound GL
1123        // client the hash waits for the GPU, half a millisecond a frame on the path to the
1124        // client. As on the XShm path, a new frame coded whole at a cleanup's quality is hashed
1125        // first instead, and one that moved is coded as motion is; a held band codes the rest of
1126        // its frame at the coarsest quantizer, so a change there costs one coarse frame. Only the
1127        // paint-over's cleanup reads the extent, so without it nothing is hashed.
1128        let lags = gpu.settings.video_streaming_mode && gpu.settings.use_paint_over_quality;
1129        let damage = if lags {
1130            std::mem::replace(&mut lagged, Damage::None)
1131        } else if !frame.is_new {
1132            Damage::None
1133        } else if gpu.settings.video_streaming_mode || !gpu.settings.use_paint_over_quality {
1134            hashes.clear();
1135            Damage::Unknown
1136        } else {
1137            hash(&mut gpu.encoder, &mut hashes)
1138        };
1139        let mut decision = decide_hw_fullframe(
1140            &mut state,
1141            &gpu.settings,
1142            frame_counter,
1143            damage,
1144            false,
1145            pending_force_idr,
1146            EncoderQuality {
1147                last: gpu.encoder.last_quality(),
1148                bytes: gpu.encoder.last_size(),
1149                holds: true,
1150                reopens: false,
1151                keys: true,
1152                band: gpu.encoder.band_size(),
1153                measures: false,
1154                psnr: None,
1155            },
1156        );
1157        let normal = gpu.settings.video_crf as u32;
1158        let mut extent = None;
1159        if lags && frame.is_new && decision.send && decision.cleanup_quality(normal).is_some() {
1160            let d = hash(&mut gpu.encoder, &mut hashes);
1161            if d.is_motion() {
1162                decision.hold_qp = None;
1163                decision.target_qp = normal;
1164                decision.force_idr =
1165                    pending_force_idr || periodic_idr_due(&gpu.settings, frame_counter);
1166            }
1167            extent = Some(d);
1168        }
1169        let mut delivered = false;
1170        if decision.send {
1171            let encode_start_ns = crate::wayland::host::now_ns();
1172            if let Some(q) = decision.hold_qp {
1173                gpu.encoder.hold_quantizer(q, decision.hold_band);
1174            }
1175            match gpu.encoder.encode_cuda_pitch(
1176                frame.device_ptr,
1177                pitch,
1178                false,
1179                frame_counter as u64,
1180                decision.target_qp,
1181                decision.force_idr,
1182            ) {
1183                Ok(data) if !data.is_empty() => {
1184                    encode_errors = 0;
1185                    let stripes = vec![EncodedStripe {
1186                        data: Arc::new(data),
1187                        codec: gpu.settings.codec,
1188                        stripe_y_start: 0,
1189                        stripe_height: gpu.settings.height,
1190                        frame_id: frame_counter as i32,
1191                        timing: FrameTiming {
1192                            capture_ns: grabbed_ns,
1193                            encode_start_ns,
1194                            encode_end_ns: crate::wayland::host::now_ns(),
1195                        },
1196                        reference: gpu.encoder.last_reference(),
1197                    }];
1198                    if let Some(sink) = &recording_sink {
1199                        sink.write_frame(&stripes, gpu.settings.width, gpu.settings.height);
1200                    }
1201                    sent_frames += 1;
1202                    delivered = true;
1203                    on_frame(stripes);
1204                }
1205                Ok(_) => {}
1206                Err(e) => {
1207                    // Its own count: a grab that keeps succeeding must not clear it.
1208                    encode_errors += 1;
1209                    if encode_errors % crate::HW_ERROR_RECOVERY_THRESHOLD == 1 {
1210                        eprintln!("[X11] NVENC encode error on the zero-copy path: {e}");
1211                    }
1212                    if encode_errors >= crate::HW_ERROR_RECOVERY_THRESHOLD {
1213                        return Some(Err(
1214                            "NVENC failed repeatedly on the zero-copy path".to_string()
1215                        ));
1216                    }
1217                }
1218            }
1219        }
1220        if lags {
1221            lagged = match extent {
1222                Some(d) => d,
1223                None if frame.is_new => hash(&mut gpu.encoder, &mut hashes),
1224                None => Damage::None,
1225            };
1226        }
1227        // An unserved request stays armed: on an infinite GOP a key frame lost to an encode error
1228        // or a skipped tick would never come back on its own.
1229        pending_force_idr = (pending_force_idr || decision.force_idr) && !delivered;
1230        frame_counter = frame_counter.wrapping_add(1);
1231
1232        let elapsed = last_log.elapsed().as_secs_f64();
1233        if elapsed >= 1.0 {
1234            crate::log::debug!(
1235                "[X11] NvFBC {}x{} Encoder: NVENC EncFPS: {:.2} NewFrames/s: {:.2} Direct: {}",
1236                gpu.settings.width,
1237                gpu.settings.height,
1238                sent_frames as f64 / elapsed,
1239                new_frames as f64 / elapsed,
1240                direct_frames > 0
1241            );
1242            sent_frames = 0;
1243            new_frames = 0;
1244            direct_frames = 0;
1245            last_log = Instant::now();
1246        }
1247    }
1248    Some(Ok(()))
1249}
1250
1251/// Restart the capture session at a new region, size, or cursor mode, and bring NVENC to the same
1252/// geometry. The device pointer the driver returns afterwards is a new one, which the encoder's
1253/// registration follows on the next frame.
1254fn restart_session(
1255    gpu: &mut GpuCapture,
1256    region: NVFBC_BOX,
1257    size: NVFBC_SIZE,
1258    with_cursor: bool,
1259) -> Result<(), NvfbcError> {
1260    // The registration addresses the buffer the outgoing session owns, so it goes before the
1261    // session does.
1262    gpu.encoder.release_external_input();
1263    gpu.nvfbc.start(region, size, with_cursor)?;
1264    gpu.settings.width = size.w as i32;
1265    gpu.settings.height = size.h as i32;
1266    if gpu.settings.width != gpu.encoder.width() as i32
1267        || gpu.settings.height != gpu.encoder.height() as i32
1268    {
1269        gpu.encoder
1270            .reconfigure_resolution(&gpu.settings)
1271            .map_err(|e| NvfbcError {
1272                status: NVFBC_SUCCESS,
1273                message: e,
1274            })?;
1275    }
1276    Ok(())
1277}
1278
1279/// Replace the client handle and the capture session on it, for the one failure the API offers no
1280/// other way back from: the X server the handle was opened on is gone.
1281///
1282/// The encoder, its CUDA context, and the frames already delivered all survive; only the driver
1283/// objects are rebuilt, in the order that keeps the encoder from holding a registration of memory
1284/// the outgoing session owns.
1285fn rebuild_handle(
1286    gpu: &mut GpuCapture,
1287    region: NVFBC_BOX,
1288    size: NVFBC_SIZE,
1289    with_cursor: bool,
1290) -> Result<(), String> {
1291    gpu.encoder.release_external_input();
1292    gpu.nvfbc.stop();
1293    gpu.nvfbc = NvfbcSession::open()?;
1294    gpu.screen = gpu.nvfbc.status().map_err(|e| e.to_string())?.screenSize;
1295    let (region, size) = if region == NVFBC_BOX::default() {
1296        resolve_region(gpu.screen, &gpu.request)
1297    } else {
1298        (region, size)
1299    };
1300    restart_session(gpu, region, size, with_cursor).map_err(|e| e.to_string())
1301}
1302
1303/// The row stride of a captured frame: the driver's own, derived from the buffer size it
1304/// reports, or the packed width when that size is not a whole number of rows.
1305fn frame_pitch(byte_size: u32, width: u32, height: u32) -> usize {
1306    let packed = width as usize * 4;
1307    if height == 0 {
1308        return packed;
1309    }
1310    let rows = byte_size as usize / height as usize;
1311    if rows >= packed && rows.is_multiple_of(4) && rows * height as usize == byte_size as usize {
1312        rows
1313    } else {
1314        packed
1315    }
1316}
1317
1318#[cfg(test)]
1319mod abi_tests {
1320    use super::*;
1321
1322    /// Every parameter struct carries its own `size_of` inside the version word the driver
1323    /// validates, so a layout that drifts from the SDK's is rejected at the first call with no
1324    /// other symptom. These are the sizes `NvFBC.h` (NvFBC 1.8) defines.
1325    /// A probe that never answers costs the bounded wait and names itself; one that answers
1326    /// hands its value straight through.
1327    #[test]
1328    fn probe_answers_are_bounded() {
1329        let stuck = answered_within(
1330            Duration::from_millis(50),
1331            "Stuck",
1332            || -> Result<u32, String> {
1333                std::thread::sleep(Duration::from_millis(400));
1334                Ok(1)
1335            },
1336        );
1337        assert_eq!(stuck, Err("Stuck did not answer within 0 s".to_string()));
1338        assert_eq!(
1339            answered_within(Duration::from_secs(1), "Quick", || Ok(7u32)),
1340            Ok(7)
1341        );
1342        assert_eq!(
1343            answered_within(
1344                Duration::from_secs(1),
1345                "Refused",
1346                || -> Result<u32, String> { Err("no".into()) }
1347            ),
1348            Err("no".to_string())
1349        );
1350    }
1351
1352    /// The gate is answered by the X server rather than by the library, so it agrees with the
1353    /// extension list that server publishes, and it answers at all: a question that could hang
1354    /// would be worth no more than the call it stands in front of.
1355    #[test]
1356    fn the_nvidia_gate_is_answered_by_the_server() {
1357        let answered = answered_within(Duration::from_secs(5), "Gate", || {
1358            Ok(nvidia_driven_x_server())
1359        })
1360        .expect("the NV-GLX gate must answer");
1361        let Ok(present) = answered else { return };
1362        let (conn, _) = x11rb::connect(None).expect("a server that answered once answers again");
1363        let listed = conn
1364            .list_extensions()
1365            .expect("the server takes the request")
1366            .reply()
1367            .expect("the server lists its extensions")
1368            .names
1369            .iter()
1370            .any(|name| name.name == NVIDIA_GLX.as_bytes());
1371        assert_eq!(present, listed);
1372    }
1373
1374    /// The guard reads which libxcb libX11 calls from libX11's own GOT after the failed open
1375    /// resolves it, and with the host's libX11 and no other copy of libxcb that is the host's, so
1376    /// NvFBC is not declined for it.
1377    #[test]
1378    fn the_host_libx11_calls_the_host_libxcb() {
1379        let x11 = unsafe { libc::dlopen(c"libX11.so.6".as_ptr(), libc::RTLD_LAZY) };
1380        assert!(!x11.is_null(), "libX11.so.6 does not load here");
1381        assert_eq!(foreign_libxcb(), None);
1382        let bound = crate::nvgpufilter::bound_import("libX11.so", b"xcb_parse_display")
1383            .expect("libX11 imports xcb_parse_display");
1384        let mut info: libc::Dl_info = unsafe { std::mem::zeroed() };
1385        assert_ne!(
1386            unsafe { libc::dladdr(bound as *const c_void, &mut info) },
1387            0
1388        );
1389        assert!(!info.dli_sname.is_null(), "the slot names no symbol");
1390        assert_eq!(
1391            unsafe { CStr::from_ptr(info.dli_sname) }.to_bytes(),
1392            b"xcb_parse_display"
1393        );
1394        unsafe { libc::dlclose(x11) };
1395    }
1396
1397    #[test]
1398    fn struct_layouts_match_the_sdk() {
1399        assert_eq!(std::mem::size_of::<NVFBC_BOX>(), 16);
1400        assert_eq!(std::mem::size_of::<NVFBC_SIZE>(), 8);
1401        assert_eq!(std::mem::size_of::<NVFBC_FRAME_GRAB_INFO>(), 48);
1402        assert_eq!(std::mem::size_of::<NVFBC_CREATE_HANDLE_PARAMS>(), 40);
1403        assert_eq!(std::mem::size_of::<NVFBC_VERSION_ONLY_PARAMS>(), 4);
1404        assert_eq!(std::mem::size_of::<NVFBC_RANDR_OUTPUT_INFO>(), 148);
1405        assert_eq!(std::mem::size_of::<NVFBC_GET_STATUS_PARAMS>(), 780);
1406        assert_eq!(
1407            std::mem::size_of::<NVFBC_CREATE_CAPTURE_SESSION_PARAMS>(),
1408            64
1409        );
1410        assert_eq!(std::mem::size_of::<NVFBC_TOCUDA_SETUP_PARAMS>(), 8);
1411        assert_eq!(std::mem::size_of::<NVFBC_TOCUDA_GRAB_FRAME_PARAMS>(), 32);
1412        assert_eq!(std::mem::size_of::<NVFBC_API_FUNCTION_LIST>(), 176);
1413    }
1414
1415    /// The version words the driver compares against are the SDK's `NVFBC_*_VER` constants: the
1416    /// struct's size, its revision at bit 16, and the API version's low byte at bit 24.
1417    #[test]
1418    fn version_words_match_the_sdk() {
1419        assert_eq!(NVFBC_VERSION, 0x108);
1420        assert_eq!(struct_ver(40, 2), 0x0802_0028);
1421        assert_eq!(struct_ver(4, 1), 0x0801_0004);
1422        assert_eq!(struct_ver(780, 2), 0x0802_030C);
1423        assert_eq!(struct_ver(64, 6), 0x0806_0040);
1424        assert_eq!(struct_ver(8, 1), 0x0801_0008);
1425        assert_eq!(struct_ver(32, 2), 0x0802_0020);
1426    }
1427}
1428
1429#[cfg(test)]
1430mod region_tests {
1431    use super::*;
1432    use crate::encoders::codec::Codec;
1433
1434    fn settings(w: i32, h: i32) -> RustCaptureSettings {
1435        RustCaptureSettings {
1436            width: w,
1437            height: h,
1438            codec: Codec::H264,
1439            ..Default::default()
1440        }
1441    }
1442
1443    /// A capture covering the whole framebuffer asks for the whole tracked region, so the session
1444    /// keeps following it; anything smaller or offset is a box.
1445    #[test]
1446    fn whole_screen_is_an_empty_box() {
1447        let screen = NVFBC_SIZE { w: 1920, h: 1080 };
1448        let mut s = settings(1920, 1080);
1449        let (region, size) = resolve_region(screen, &s);
1450        assert_eq!(region, NVFBC_BOX::default());
1451        assert_eq!(size, NVFBC_SIZE { w: 1920, h: 1080 });
1452
1453        s.auto_adjust_screen_capture_size = true;
1454        let (region, size) = resolve_region(screen, &s);
1455        assert_eq!(region, NVFBC_BOX::default());
1456        assert_eq!(size, NVFBC_SIZE { w: 1920, h: 1080 });
1457
1458        let mut region_only = settings(1280, 720);
1459        region_only.capture_x = 100;
1460        region_only.capture_y = 50;
1461        let (region, size) = resolve_region(screen, &region_only);
1462        assert_eq!(
1463            region,
1464            NVFBC_BOX {
1465                x: 100,
1466                y: 50,
1467                w: 1280,
1468                h: 720
1469            }
1470        );
1471        assert_eq!(size, NVFBC_SIZE { w: 1280, h: 720 });
1472    }
1473
1474    /// Both dimensions are even, because every video codec requires it, and a region is clamped
1475    /// into the framebuffer rather than running past its edge.
1476    ///
1477    /// An odd screen therefore captures an even box of it, never the whole screen delivered at
1478    /// the even size: the driver would scale into the difference, which is a conversion pass and
1479    /// the end of direct capture.
1480    #[test]
1481    fn region_is_even_and_inside_the_screen() {
1482        let screen = NVFBC_SIZE { w: 1919, h: 1081 };
1483        let (region, size) = resolve_region(screen, &settings(0, 0));
1484        assert_eq!(size, NVFBC_SIZE { w: 1918, h: 1080 });
1485        assert_eq!(
1486            region,
1487            NVFBC_BOX {
1488                x: 0,
1489                y: 0,
1490                w: 1918,
1491                h: 1080
1492            }
1493        );
1494
1495        let mut s = settings(1920, 1080);
1496        s.capture_x = 1000;
1497        s.capture_y = 800;
1498        let (region, size) = resolve_region(NVFBC_SIZE { w: 1920, h: 1080 }, &s);
1499        assert_eq!(region.x, 1000);
1500        assert_eq!(region.y, 800);
1501        assert_eq!(size, NVFBC_SIZE { w: 920, h: 280 });
1502        assert_eq!(region.w, size.w);
1503        assert_eq!(region.h, size.h);
1504    }
1505
1506    /// What the capture does after a failed driver call follows from the status alone: a session
1507    /// the driver invalidated is created again, a lost X server needs a whole new handle, and
1508    /// nothing else is recoverable. The status is read from the error rather than matched in its
1509    /// text, where the driver's own message could carry the same digits.
1510    #[test]
1511    fn recovery_follows_the_status() {
1512        let session = |status| NvfbcError {
1513            status,
1514            message: String::new(),
1515        };
1516        assert!(matches!(
1517            recovery_for(&session(NVFBC_ERR_MUST_RECREATE)),
1518            Recovery::Session
1519        ));
1520        assert!(matches!(
1521            recovery_for(&session(NVFBC_ERR_X)),
1522            Recovery::Handle
1523        ));
1524        assert!(matches!(recovery_for(&session(1)), Recovery::None));
1525        let misleading = NvfbcError {
1526            status: 1,
1527            message: "the display (16) went away (10)".into(),
1528        };
1529        assert!(matches!(recovery_for(&misleading), Recovery::None));
1530    }
1531
1532    /// The stride comes from the driver's own byte count when that is a whole number of rows,
1533    /// and falls back to the packed width when it is not.
1534    #[test]
1535    fn pitch_follows_the_reported_byte_size() {
1536        assert_eq!(frame_pitch(1920 * 1080 * 4, 1920, 1080), 1920 * 4);
1537        assert_eq!(frame_pitch(2048 * 4 * 1080, 1920, 1080), 2048 * 4);
1538        assert_eq!(frame_pitch(12345, 1920, 1080), 1920 * 4);
1539        assert_eq!(frame_pitch(0, 1920, 0), 1920 * 4);
1540    }
1541}
1542
1543#[cfg(test)]
1544mod gpu_tests {
1545    use super::super::gpu_test_support::{decoded_mean, paint_root, painted_ycbcr, settings};
1546    use super::*;
1547    use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, VIDEO_HEADER_LEN, parse_video_type};
1548    use crate::webcam::decode::{Codec as DecCodec, VideoDecoder};
1549
1550    /// Thread CPU time, for the per-frame CPU cost of the path independent of GPU waiting.
1551    fn thread_cpu() -> Duration {
1552        let mut ts = libc::timespec {
1553            tv_sec: 0,
1554            tv_nsec: 0,
1555        };
1556        unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) };
1557        Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
1558    }
1559
1560    /// A session that follows the screen carries no dimensions of its own, which is how Selkies
1561    /// runs a full-screen capture, and the encoder has to be built at the size the capture
1562    /// resolves rather than at the zero it was asked with. Ignored by default; run with
1563    /// `DISPLAY` on an NVIDIA X server.
1564    #[test]
1565    #[ignore]
1566    fn gpu_nvfbc_opens_a_session_that_follows_the_screen() {
1567        let mut s = settings(crate::encoders::codec::Codec::H264);
1568        s.width = 0;
1569        s.height = 0;
1570        s.auto_adjust_screen_capture_size = true;
1571        let Some(gpu) = open(&s) else {
1572            println!("the NvFBC path declined this host; nothing to open");
1573            return;
1574        };
1575        assert!(
1576            gpu.settings.width > 0 && gpu.settings.height > 0,
1577            "a session following the screen opened at {}x{}",
1578            gpu.settings.width,
1579            gpu.settings.height
1580        );
1581        println!(
1582            "a session following the screen opened at {}x{}",
1583            gpu.settings.width, gpu.settings.height
1584        );
1585    }
1586
1587    /// On a real NVIDIA X server: the driver reports whether it can capture this screen, and at
1588    /// what size. Ignored by default; run with `DISPLAY` pointing at an NVIDIA X server:
1589    /// `cargo test gpu_nvfbc -- --ignored --nocapture --test-threads=1`.
1590    #[test]
1591    #[ignore]
1592    fn gpu_nvfbc_status_reports_capture() {
1593        let Ok(nvfbc) = NvfbcSession::open() else {
1594            println!("NvFBC handed out no capture handle on this host; nothing to report");
1595            return;
1596        };
1597        let status = nvfbc.status().expect("NvFBC status");
1598        println!(
1599            "capture possible={} can create now={} screen={}x{} outputs={} nvfbc version={} in modeset={}",
1600            status.bIsCapturePossible,
1601            status.bCanCreateNow,
1602            status.screenSize.w,
1603            status.screenSize.h,
1604            status.dwOutputNum,
1605            status.dwNvFBCVersion,
1606            status.bInModeset
1607        );
1608        assert_eq!(
1609            status.bIsCapturePossible, NVFBC_TRUE,
1610            "this GPU offers no NvFBC"
1611        );
1612        assert!(status.screenSize.w > 0 && status.screenSize.h > 0);
1613    }
1614
1615    /// The luma of the next frame `gpu` captures, waiting up to two seconds for a new one, as
1616    /// encoded and decoded.
1617    fn captured_luma(gpu: &mut GpuCapture, dec: &mut VideoDecoder, n: &mut u64) -> f64 {
1618        let mut frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1619        for _ in 0..10 {
1620            if frame.is_new {
1621                break;
1622            }
1623            frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1624        }
1625        let pitch = frame_pitch(frame.byte_size, frame.width, frame.height);
1626        let pkt = gpu
1627            .encoder
1628            .encode_cuda_pitch(frame.device_ptr, pitch, false, *n, 25, *n == 0)
1629            .expect("encode in place");
1630        *n += 1;
1631        decoded_mean(dec, &pkt[VIDEO_HEADER_LEN..])[0]
1632    }
1633
1634    /// Whether two captures of the same X screen hold NvFBC sessions at once, which is what a
1635    /// second Selkies display on X11 needs to be zero-copy as well: each on its own thread, as
1636    /// the displays run them (a handle's contexts belong to the thread that made it, and two on
1637    /// one thread capture stale frames and break each other), each shows a repaint, and the
1638    /// first keeps capturing once the second closes. A driver that allows only one leaves the
1639    /// second capture on the XShm path, a correct outcome but a slower one, so the answer is
1640    /// worth knowing rather than assuming. Ignored by default.
1641    #[test]
1642    #[ignore]
1643    fn gpu_nvfbc_two_sessions_on_one_screen() {
1644        const FIRST: (u8, u8, u8) = (0x20, 0x40, 0xc0);
1645        const SECOND: (u8, u8, u8) = (0xd0, 0x50, 0x18);
1646        const THIRD: (u8, u8, u8) = (0x30, 0xc0, 0x30);
1647        if !paint_root(FIRST) {
1648            println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1649            return;
1650        }
1651        let Some(mut first) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1652            println!("the NvFBC path declined the first capture on this host; nothing to compare");
1653            return;
1654        };
1655        let (go, wait) = std::sync::mpsc::channel::<()>();
1656        let (tell, seen) = std::sync::mpsc::channel::<Option<f64>>();
1657        let second = std::thread::spawn(move || {
1658            let Some(mut gpu) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1659                let _ = tell.send(None);
1660                return;
1661            };
1662            let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1663            let mut n = 0;
1664            let _ = tell.send(Some(captured_luma(&mut gpu, &mut dec, &mut n)));
1665            while wait.recv().is_ok() {
1666                let _ = tell.send(Some(captured_luma(&mut gpu, &mut dec, &mut n)));
1667            }
1668        });
1669        let opened = seen.recv().expect("the second capture's thread").is_some();
1670        println!(
1671            "a second concurrent NvFBC capture of the same screen: {}",
1672            if opened {
1673                "opens"
1674            } else {
1675                "is declined, and falls back to XShm"
1676            }
1677        );
1678        let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1679        let mut n = 0;
1680        captured_luma(&mut first, &mut dec, &mut n);
1681        assert!(paint_root(SECOND));
1682        let want = painted_ycbcr(SECOND)[0];
1683        let got = captured_luma(&mut first, &mut dec, &mut n);
1684        assert!(
1685            (got - want).abs() <= 8.0,
1686            "the first captured {got:.1} beside the second, painted {want:.1}"
1687        );
1688        if opened {
1689            go.send(()).expect("the second capture's thread");
1690            let got = seen
1691                .recv()
1692                .expect("the second capture's thread")
1693                .expect("a capture");
1694            assert!(
1695                (got - want).abs() <= 8.0,
1696                "the second captured {got:.1}, painted {want:.1}"
1697            );
1698        }
1699        drop(go);
1700        second.join().expect("the second capture's thread");
1701        assert!(paint_root(THIRD));
1702        let want = painted_ycbcr(THIRD)[0];
1703        let got = captured_luma(&mut first, &mut dec, &mut n);
1704        assert!(
1705            (got - want).abs() <= 8.0,
1706            "the first captured {got:.1} once the second closed, painted {want:.1}"
1707        );
1708    }
1709
1710    /// The whole zero-copy path against a real X server and GPU: the driver composites the root
1711    /// into video memory, NVENC encodes that buffer in place, and the decoded picture is the
1712    /// color that was painted.
1713    ///
1714    /// The zero-copy property is asserted rather than assumed, three ways: the driver reports it
1715    /// needed no post-processing pass (the native BGRA request means no conversion copy), every
1716    /// grab hands back the *same* device pointer (so the encoder's registration is made once and
1717    /// reused instead of per frame), and repainting the root changes the decoded color, which it
1718    /// could not if the encoder were reading a stale copy. Ignored by default.
1719    #[test]
1720    #[ignore]
1721    fn gpu_nvfbc_zero_copy_encodes_the_painted_root() {
1722        const FIRST: (u8, u8, u8) = (0x20, 0x40, 0xc0);
1723        const SECOND: (u8, u8, u8) = (0xd0, 0x50, 0x18);
1724        if !paint_root(FIRST) {
1725            println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1726            return;
1727        }
1728        let Some(mut gpu) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1729            println!("the NvFBC path declined this session on this host; nothing to capture");
1730            return;
1731        };
1732        let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1733        let mut pointers = Vec::new();
1734
1735        let encode =
1736            |gpu: &mut GpuCapture, i: u64, key: bool, pointers: &mut Vec<CUdeviceptr>| -> Vec<u8> {
1737                // A repaint is one frame; ask until the driver has produced it.
1738                let mut frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1739                for _ in 0..30 {
1740                    if frame.is_new {
1741                        break;
1742                    }
1743                    frame = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1744                }
1745                assert_eq!(frame.width, gpu.encoder.width());
1746                assert_eq!(frame.height, gpu.encoder.height());
1747                assert!(
1748                    !frame.post_processed,
1749                    "the native format needed a conversion pass, which is an extra frame copy"
1750                );
1751                pointers.push(frame.device_ptr);
1752                let pitch = frame_pitch(frame.byte_size, frame.width, frame.height);
1753                gpu.encoder
1754                    .encode_cuda_pitch(frame.device_ptr, pitch, false, i, 25, key)
1755                    .expect("encode in place")
1756            };
1757
1758        let pkt = encode(&mut gpu, 0, true, &mut pointers);
1759        assert_eq!(
1760            parse_video_type(pkt[1]),
1761            Some((crate::encoders::codec::Codec::H264, FRAME_KEY))
1762        );
1763        let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1764        let want = painted_ycbcr(FIRST);
1765        for i in 0..3 {
1766            assert!(
1767                (mean[i] - want[i]).abs() <= 8.0,
1768                "plane {i}: captured {:.1}, painted {:.1}",
1769                mean[i],
1770                want[i]
1771            );
1772        }
1773
1774        for i in 1..4u64 {
1775            let pkt = encode(&mut gpu, i, false, &mut pointers);
1776            assert_eq!(
1777                parse_video_type(pkt[1]),
1778                Some((crate::encoders::codec::Codec::H264, FRAME_DELTA))
1779            );
1780            let _ = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1781        }
1782
1783        assert!(
1784            paint_root(SECOND),
1785            "the root was painted once, so a repaint must work"
1786        );
1787        let pkt = encode(&mut gpu, 4, false, &mut pointers);
1788        let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1789        let want = painted_ycbcr(SECOND);
1790        for i in 0..3 {
1791            assert!(
1792                (mean[i] - want[i]).abs() <= 8.0,
1793                "after repaint, plane {i}: captured {:.1}, painted {:.1}",
1794                mean[i],
1795                want[i]
1796            );
1797        }
1798
1799        assert!(
1800            pointers.windows(2).all(|w| w[0] == w[1]),
1801            "the capture buffer moved between frames, so the registration cannot be reused: {pointers:?}"
1802        );
1803        println!(
1804            "captured {}x{} into one device pointer {:#x} across {} frames, no post-processing pass",
1805            gpu.encoder.width(),
1806            gpu.encoder.height(),
1807            pointers[0],
1808            pointers.len()
1809        );
1810    }
1811
1812    /// What the zero-copy path costs on a real GPU, per codec the device serves.
1813    ///
1814    /// Two numbers, because they answer different questions. The steady-state pass grabs without
1815    /// waiting, so it measures what capture and encode themselves cost with nothing else in the
1816    /// way — the ceiling the path imposes on frame rate. The moving-screen pass repaints the root
1817    /// between frames and times only the grab and encode, so the encoder is doing real work on
1818    /// changed content. In both, the host-CPU figure is the one that matters: nothing on the host
1819    /// touches a pixel, so it stays a small fraction of the wall time. Ignored by default.
1820    #[test]
1821    #[ignore]
1822    fn gpu_nvfbc_throughput() {
1823        let n: usize = std::env::var("NVFBC_BENCH_FRAMES")
1824            .ok()
1825            .and_then(|v| v.parse().ok())
1826            .unwrap_or(240);
1827        for codec in [
1828            crate::encoders::codec::Codec::H264,
1829            crate::encoders::codec::Codec::H265,
1830            crate::encoders::codec::Codec::Av1,
1831        ] {
1832            let mut s = settings(codec);
1833            s.auto_adjust_screen_capture_size = true;
1834            let Some(mut gpu) = open(&s) else {
1835                println!("{}: declined", codec.display());
1836                continue;
1837            };
1838            let (w, h) = (gpu.encoder.width(), gpu.encoder.height());
1839            let run = |label: &str, repaint: bool, gpu: &mut GpuCapture| {
1840                let f = gpu.nvfbc.grab(Duration::from_millis(200)).expect("grab");
1841                let pitch = frame_pitch(f.byte_size, f.width, f.height);
1842                gpu.encoder
1843                    .encode_cuda_pitch(f.device_ptr, pitch, false, 0, 25, true)
1844                    .expect("warm-up");
1845                let (mut wall, mut cpu, mut bytes, mut news) =
1846                    (Duration::ZERO, Duration::ZERO, 0usize, 0usize);
1847                for i in 0..n {
1848                    if repaint {
1849                        let v = (i * 9) as u8;
1850                        let _ = std::process::Command::new("xsetroot")
1851                            .args(["-solid", &format!("#{:02x}{:02x}{:02x}", v, 0x40, 0x80)])
1852                            .output();
1853                    }
1854                    let t = Instant::now();
1855                    let c = thread_cpu();
1856                    let f = gpu
1857                        .nvfbc
1858                        .grab_with(NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, Duration::from_millis(50))
1859                        .expect("grab");
1860                    let pitch = frame_pitch(f.byte_size, f.width, f.height);
1861                    let pkt = gpu
1862                        .encoder
1863                        .encode_cuda_pitch(f.device_ptr, pitch, false, 1 + i as u64, 25, false)
1864                        .expect("encode");
1865                    wall += t.elapsed();
1866                    cpu += thread_cpu() - c;
1867                    bytes += pkt.len();
1868                    news += f.is_new as usize;
1869                }
1870                println!(
1871                    "{} {w}x{h} {label}: {:.0} fps ({:.2} ms/frame wall, {:.2} ms/frame host CPU), {news}/{n} new, {:.1} KiB/frame",
1872                    codec.display(),
1873                    n as f64 / wall.as_secs_f64(),
1874                    wall.as_secs_f64() * 1000.0 / n as f64,
1875                    cpu.as_secs_f64() * 1000.0 / n as f64,
1876                    bytes as f64 / n as f64 / 1024.0
1877                );
1878            };
1879            run("steady state", false, &mut gpu);
1880            run("moving screen", true, &mut gpu);
1881            shm_upload_baseline(codec, w, h, n);
1882        }
1883    }
1884
1885    /// The same screen, same codec, and same GPU through the general XShm path, for the comparison
1886    /// the zero-copy path exists to win: the X server blits the root into a shared-memory surface
1887    /// and the encoder uploads that surface across PCIe, where NvFBC hands the encoder a buffer
1888    /// the driver already composited in video memory.
1889    fn shm_upload_baseline(codec: crate::encoders::codec::Codec, w: u32, h: u32, n: usize) {
1890        use x11rb::connection::Connection;
1891        use x11rb::protocol::shm::ConnectionExt as ShmExt;
1892        let Ok((conn, screen)) = x11rb::connect(None) else {
1893            return;
1894        };
1895        if conn
1896            .shm_query_version()
1897            .ok()
1898            .and_then(|c| c.reply().ok())
1899            .is_none()
1900        {
1901            return;
1902        }
1903        let root = conn.setup().roots[screen].root;
1904        let Ok(mut surface) = crate::x11::ShmSurface::create(&conn, w as u16, h as u16) else {
1905            return;
1906        };
1907        let mut s = settings(codec);
1908        s.width = w as i32;
1909        s.height = h as i32;
1910        let mut enc = match NvencEncoder::new(&s, std::ptr::null()) {
1911            Ok(e) => e,
1912            Err(_) => {
1913                surface.destroy(&conn);
1914                return;
1915            }
1916        };
1917        let stride = w as usize * 4;
1918        let (mut wall, mut cpu) = (Duration::ZERO, Duration::ZERO);
1919        for i in 0..n {
1920            let t = Instant::now();
1921            let c = thread_cpu();
1922            if crate::x11::grab_frame(&conn, root, &surface, 0, 0, w as u16, h as u16).is_err() {
1923                break;
1924            }
1925            let _ = enc
1926                .encode_cpu_packed(surface.as_slice(), stride, false, i as u64, 25, i == 0)
1927                .expect("encode uploaded frame");
1928            wall += t.elapsed();
1929            cpu += thread_cpu() - c;
1930        }
1931        surface.destroy(&conn);
1932        println!(
1933            "{} {w}x{h} XShm grab + upload: {:.0} fps ({:.2} ms/frame wall, {:.2} ms/frame host CPU)",
1934            codec.display(),
1935            n as f64 / wall.as_secs_f64(),
1936            wall.as_secs_f64() * 1000.0 / n as f64,
1937            cpu.as_secs_f64() * 1000.0 / n as f64
1938        );
1939    }
1940}