Skip to main content

pixelflux/x11/
dri3.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//! DRI3 zero-copy X11 capture: the X server blits the screen into GPU buffers this process
8//! allocated, and the hardware encoder reads them where they lie.
9//!
10//! It serves a server whose screen lives on the GPU (XLibre's Xvfb with glamor, an Xorg on a
11//! DRM driver), where the XShm path in [`super`] would read the screen back into host memory
12//! for the hardware session to upload again. A small pool of dmabufs is allocated through GBM
13//! on the render node the server draws with, each is wrapped in an X pixmap
14//! (`PixmapFromBuffers`), and every frame is one `CopyArea` from the root window into the next
15//! pixmap, which glamor runs as a GPU-to-GPU blit. The encoder then imports the dmabuf in place,
16//! through the same [`FrameEncoder::encode_dmabuf`] the Wayland zero-copy path uses, so a frame
17//! is never touched by the CPU between the screen and the bitstream.
18//!
19//! Completion is waited for with a one-pixel `GetImage` of the destination pixmap: the server
20//! answers only after the blit it ordered before has landed, so the encoder never reads a
21//! half-copied frame, and no fence object or implicit-sync assumption is involved.
22//!
23//! Where the server offers SELKIES-SEMAPHORE (the images' Xvfb) and the encoder can wait on a GPU
24//! semaphore (NVENC), the server signals one the encoder made after each blit instead, and the
25//! encode is queued at once behind a wait on it. Under a GPU-bound client each GPU job that has
26//! to wait for the CPU to submit it waits a draw of that client for its turn, so the GetImage
27//! cost two draws a frame where the semaphore costs one: an uncapped 72 fps client streamed at
28//! 36 fps with the GetImage. A checked Signal request is the round trip the wait follows, which
29//! answers once the server has queued the signal, not once the GPU ran it: a wait queued before
30//! the server's signal lost its turn as well.
31//!
32//! Nothing here reads pixels, so what stands in for the XShm path's per-stripe content hashing is
33//! the Damage extension on the root window: the server's report is both what says a frame is worth
34//! encoding and what ends the wait for one, within the pacing [`crate::pace`] keeps, so a change is
35//! published as it lands rather than on the next tick. Its region says how much changed, so a
36//! blinking caret stays a small change and a still screen is cleaned up around it. The XFixes
37//! cursor is composited by the server through Render, on the GPU, so the `capture_cursor` overlay
38//! costs no readback either; the server keeps its own software cursor out of a copy from the
39//! screen, so the cursor is never drawn twice.
40//!
41//! Everything that can be checked is checked before a frame is delivered, and the path is
42//! declined otherwise: a codec no hardware engine serves, software encoding, a server without
43//! DRI3 1.2, Damage, or Render, a server drawing on a GPU other than the encode node's, a buffer
44//! the server will not import, or a first frame the encoder cannot read. A watermark is not among
45//! them: it is composited by the server through Render, as the cursor is. The capture then runs the XShm path with nothing half-built.
46
47use std::ffi::c_void;
48use std::fs::File;
49use std::io::IoSlice;
50use std::os::fd::OwnedFd;
51use std::os::unix::fs::MetadataExt;
52use std::sync::Arc;
53use std::sync::atomic::Ordering;
54use std::time::{Duration, Instant};
55
56use gbm::{BufferObject, Device as RawGbmDevice, Format as GbmFormat};
57use smithay::backend::allocator::dmabuf::Dmabuf;
58use smithay::backend::allocator::gbm::GbmDevice;
59use smithay::backend::egl::EGLDisplay;
60use x11rb::connection::{Connection, RequestConnection};
61use x11rb::protocol::dri3::ConnectionExt as Dri3Ext;
62use x11rb::protocol::render::{self, ConnectionExt as RenderExt};
63use x11rb::protocol::xfixes::ConnectionExt as XfixesExt;
64use x11rb::protocol::xproto::{self, ConnectionExt as XprotoExt, ImageFormat, ImageOrder};
65use x11rb::rust_connection::RustConnection;
66
67use super::{
68    Controls, RootDamage, captured_damage, clamp_offset, cursor_image_origin, require_32bpp,
69    resolve_dims, wait_for_frame,
70};
71use crate::RustCaptureSettings;
72use crate::encoders::software::{EncodedStripe, FrameTiming, StripeState};
73use crate::encoders::{self, FrameEncoder, FrameSource};
74use crate::pace::{FramePace, TickTrigger};
75use crate::pipeline::{Damage, EncoderQuality, decide_hw_fullframe};
76use crate::recording_sink::RecordingSink;
77
78/// GPU buffers the screen is blitted into, used round-robin so an encoder still reading the
79/// previous frame never has the next blitted over it.
80const POOL_N: usize = 3;
81
82/// Frames between framebuffer-size polls, matching the XShm path's geometry cadence.
83const GEOMETRY_POLL_FRAMES: i32 = 30;
84
85/// Consecutive X request failures before the capture reports itself dead: the server is gone
86/// rather than momentarily late, and the Python watchdog restarts the display.
87const MAX_X_FAILURES: u32 = 5;
88
89/// One dmabuf the server blits into: the GBM allocation, the encoder-side description of it,
90/// and the pixmap and Render picture the server knows it as.
91struct GpuBuffer {
92    _bo: BufferObject<()>,
93    dmabuf: Dmabuf,
94    pixmap: xproto::Pixmap,
95    picture: render::Picture,
96}
97
98/// An ARGB image the server composites over each frame through Render: the XFixes cursor,
99/// re-uploaded whenever it changes, or the watermark, uploaded once.
100struct Sprite {
101    pixmap: xproto::Pixmap,
102    picture: render::Picture,
103    gc: xproto::Gcontext,
104    width: u16,
105    height: u16,
106}
107
108/// The XFixes cursor image uploaded once per cursor change as an ARGB pixmap the server
109/// composites over each frame.
110struct CursorSprite {
111    pixmap: xproto::Pixmap,
112    picture: render::Picture,
113    gc: xproto::Gcontext,
114    width: u16,
115    height: u16,
116    serial: u32,
117}
118
119/// The X side of the capture: the private connection and the server objects the blit needs.
120struct XScreen {
121    conn: RustConnection,
122    root: xproto::Window,
123    depth: u8,
124    byte_order: ImageOrder,
125    gc: xproto::Gcontext,
126    /// The server's report that the root changed, which both ends a frame's wait early and
127    /// says whether to encode one at all: nothing here reads pixels, so there is no content
128    /// hash to stand in for it.
129    damage: RootDamage,
130    /// The Render format of the screen's depth, for the destination pictures.
131    rgb_format: render::Pictformat,
132    /// The Render format of a premultiplied ARGB cursor image.
133    argb_format: render::Pictformat,
134    /// What the server accepts for buffers of the screen's depth; empty leaves the driver's
135    /// choice and the server learns the layout from the modifier the allocation reports.
136    modifiers: Vec<u64>,
137    device: String,
138    /// SELKIES-SEMAPHORE's major opcode, where the server offers it.
139    semaphore_opcode: Option<u8>,
140}
141
142/// The images' Xvfb extension for GPU semaphores shared with the screen's renderer.
143const SEMAPHORE_EXTENSION: &str = "SELKIES-SEMAPHORE";
144
145/// A running zero-copy capture, on the thread that owns every context in it.
146///
147/// Field order is drop order: the encoder releases its imports before the buffers they
148/// describe go, and both go before the GBM device and EGL display they were made on.
149struct GpuCapture {
150    /// `None` only while a rebuild is replacing the session; a rebuild that fails ends the
151    /// capture before another frame is encoded.
152    encoder: Option<FrameEncoder>,
153    buffers: Vec<GpuBuffer>,
154    cursor: Option<CursorSprite>,
155    /// The watermark image and where it sits, and the pixmap the server composites it from.
156    /// Held rather than blended into host pixels, which is what keeps this path zero-copy.
157    overlay: crate::encoders::overlay::OverlayState,
158    watermark: Option<Sprite>,
159    x: XScreen,
160    gbm: RawGbmDevice<File>,
161    _egl: Option<EGLDisplay>,
162    egl_display: *const c_void,
163    next: usize,
164    /// The live session: its geometry is what is captured and encoded, and its rate and quality
165    /// fields follow the cross-thread controls.
166    settings: RustCaptureSettings,
167    /// The geometry that was asked for, which a region change updates and every region is
168    /// resolved from, kept apart from `settings` so a capture following the root keeps following.
169    request: RustCaptureSettings,
170    cap_x: i16,
171    cap_y: i16,
172    root_w: u16,
173    root_h: u16,
174    /// The server's id for the encoder's blit semaphore, which it signals after each blit for
175    /// the encode to wait on; `None` waits for each blit with a GetImage.
176    blit_semaphore: Option<u32>,
177}
178
179/// Take `chosen` out of the modifiers the next allocation may pick. A choice the list never
180/// named came from the driver itself, so the whole list goes and the driver picks again
181/// unconstrained.
182fn prune_modifier(modifiers: &mut Vec<u64>, chosen: u64) {
183    let before = modifiers.len();
184    modifiers.retain(|&m| m != chosen);
185    if modifiers.len() == before {
186        modifiers.clear();
187    }
188}
189
190/// Whether `modifier` is one of Intel's compressed (CCS) layouts. The plane count does not tell:
191/// DG2 and later keep the compression state outside the buffer, so theirs are single-plane, and
192/// the media driver reads such a buffer as if it were uncompressed. Intel's uncompressed
193/// layouts are the few named here; every other one it defines is a CCS variant.
194fn intel_compressed(modifier: u64) -> bool {
195    const INTEL: u64 = 0x01;
196    const X_TILED: u64 = 1;
197    const Y_TILED: u64 = 2;
198    const YF_TILED: u64 = 3;
199    const TILE4: u64 = 9;
200    let layout = modifier & 0x00ff_ffff_ffff_ffff;
201    modifier >> 56 == INTEL && !matches!(layout, X_TILED | Y_TILED | YF_TILED | TILE4)
202}
203
204/// Why the DRI3 path was not taken, for the one line that says so.
205fn declined(reason: &str) -> Option<GpuCapture> {
206    println!("[X11] Zero-copy capture (DRI3) unavailable: {reason}; capturing through XShm.");
207    crate::report::capture_declined("DRI3", reason);
208    None
209}
210
211fn x_err(what: &str, e: impl std::fmt::Display) -> String {
212    format!("{what}: {e}")
213}
214
215/// The DRM device a file descriptor names, as the kernel numbers it.
216fn device_of_fd(fd: &OwnedFd) -> Result<u64, String> {
217    let file = File::from(
218        fd.try_clone()
219            .map_err(|e| x_err("dup of the DRI3 device", e))?,
220    );
221    file.metadata()
222        .map(|m| m.rdev())
223        .map_err(|e| x_err("fstat of the DRI3 device", e))
224}
225
226/// Whether two DRM nodes belong to one GPU: an Xorg whose glamor runs on the modesetting
227/// driver hands DRI3 clients its primary node, where the encoder opens the render node.
228fn same_gpu(a: u64, b: u64) -> bool {
229    let gpu = |dev: u64| {
230        std::fs::canonicalize(format!(
231            "/sys/dev/char/{}:{}/device",
232            libc::major(dev),
233            libc::minor(dev)
234        ))
235        .ok()
236    };
237    a == b || gpu(a).is_some_and(|g| gpu(b) == Some(g))
238}
239
240impl XScreen {
241    /// Connect and negotiate everything the blit needs, or say what the server lacks.
242    fn open(node: i32) -> Result<(Self, OwnedFd), String> {
243        let (conn, screen_num) =
244            x11rb::connect(None).map_err(|e| x_err("X11 connect failed", e))?;
245        require_32bpp(&conn, screen_num)?;
246        let screen = &conn.setup().roots[screen_num];
247        let root = screen.root;
248        let depth = screen.root_depth;
249        let byte_order = conn.setup().image_byte_order;
250
251        let dri3 = conn
252            .dri3_query_version(1, 2)
253            .map_err(|e| x_err("DRI3 query", e))?
254            .reply()
255            .map_err(|e| x_err("the server has no DRI3", e))?;
256        if dri3.major_version < 1 || (dri3.major_version == 1 && dri3.minor_version < 2) {
257            return Err(format!(
258                "the server's DRI3 is {}.{}, and buffers with modifiers need 1.2",
259                dri3.major_version, dri3.minor_version
260            ));
261        }
262        conn.xfixes_query_version(5, 0)
263            .map_err(|e| x_err("XFixes query", e))?
264            .reply()
265            .map_err(|e| x_err("the server has no XFixes", e))?;
266        let formats = conn
267            .render_query_pict_formats()
268            .map_err(|e| x_err("Render query", e))?
269            .reply()
270            .map_err(|e| x_err("the server has no Render extension", e))?
271            .formats;
272        let direct =
273            |f: &render::Pictforminfo, d: u8| f.type_ == render::PictType::DIRECT && f.depth == d;
274        let rgb_format = formats
275            .iter()
276            .find(|f| direct(f, depth) && f.direct.red_shift == 16 && f.direct.blue_shift == 0)
277            .map(|f| f.id)
278            .ok_or_else(|| format!("no Render format for the depth-{depth} screen"))?;
279        let argb_format = formats
280            .iter()
281            .find(|f| direct(f, 32) && f.direct.alpha_mask == 0xff && f.direct.red_shift == 16)
282            .map(|f| f.id)
283            .ok_or("no premultiplied ARGB32 Render format for the cursor")?;
284
285        let opened = conn
286            .dri3_open(root, 0)
287            .map_err(|e| x_err("DRI3 open", e))?
288            .reply()
289            .map_err(|e| x_err("the server refused DRI3 open", e))?;
290        let device_fd = opened.device_fd;
291        let encode_path = format!("/dev/dri/renderD{}", 128 + node);
292        let encode_dev = std::fs::metadata(&encode_path)
293            .map(|m| m.rdev())
294            .map_err(|e| x_err(&format!("encode node {encode_path}"), e))?;
295        let server_dev = device_of_fd(&device_fd)?;
296        if !same_gpu(server_dev, encode_dev) {
297            return Err(format!(
298                "the X server draws on DRM device {}:{}, not on the GPU of encode node {encode_path}",
299                libc::major(server_dev),
300                libc::minor(server_dev)
301            ));
302        }
303
304        let modifiers_reply = conn
305            .dri3_get_supported_modifiers(root, depth, 32)
306            .map_err(|e| x_err("DRI3 modifiers", e))?
307            .reply()
308            .map_err(|e| x_err("the server listed no buffer modifiers", e))?;
309        let mut modifiers = if modifiers_reply.window_modifiers.is_empty() {
310            modifiers_reply.screen_modifiers
311        } else {
312            modifiers_reply.window_modifiers
313        };
314        modifiers.retain(|&m| !intel_compressed(m));
315
316        let gc = conn.generate_id().map_err(|e| x_err("GC id", e))?;
317        conn.create_gc(
318            gc,
319            root,
320            &xproto::CreateGCAux::new()
321                .subwindow_mode(xproto::SubwindowMode::INCLUDE_INFERIORS)
322                .graphics_exposures(0),
323        )
324        .map_err(|e| x_err("CreateGC", e))?
325        .check()
326        .map_err(|e| x_err("CreateGC", e))?;
327        let damage = RootDamage::create(&conn, root).ok_or("the server has no Damage extension")?;
328        let semaphore_opcode = conn
329            .extension_information(SEMAPHORE_EXTENSION)
330            .ok()
331            .flatten()
332            .map(|e| e.major_opcode);
333
334        Ok((
335            Self {
336                conn,
337                root,
338                depth,
339                byte_order,
340                gc,
341                damage,
342                rgb_format,
343                argb_format,
344                modifiers,
345                device: encode_path,
346                semaphore_opcode,
347            },
348            device_fd,
349        ))
350    }
351
352    /// One SELKIES-SEMAPHORE request: its minor opcode and CARD32 fields, and an fd if it takes one.
353    fn semaphore_request(
354        &self,
355        minor: u8,
356        fields: &[u32],
357        fds: Vec<OwnedFd>,
358    ) -> Result<x11rb::cookie::VoidCookie<'_, RustConnection>, String> {
359        let opcode = self
360            .semaphore_opcode
361            .ok_or("the server has no SELKIES-SEMAPHORE")?;
362        let mut req = vec![opcode, minor];
363        req.extend_from_slice(&(1 + fields.len() as u16).to_ne_bytes());
364        for f in fields {
365            req.extend_from_slice(&f.to_ne_bytes());
366        }
367        self.conn
368            .send_request_without_reply(&[IoSlice::new(&req)], fds)
369            .map_err(|e| x_err("SELKIES-SEMAPHORE", e))
370    }
371
372    /// Hand the server a semaphore to signal after the blits it is asked to (`signal_semaphore`),
373    /// returning its id there.
374    fn import_semaphore(&self, fd: OwnedFd) -> Result<u32, String> {
375        let id = self
376            .conn
377            .generate_id()
378            .map_err(|e| x_err("semaphore id", e))?;
379        self.semaphore_request(1, &[id, self.root], vec![fd])?
380            .check()
381            .map_err(|e| format!("the server did not take the semaphore: {e}"))?;
382        Ok(id)
383    }
384
385    /// Have the server signal the semaphore once the rendering requested before has landed.
386    /// The check's round trip answers once the server has queued the signal, which is what the
387    /// encoder's wait has to come after; an error means no signal is coming.
388    fn signal_semaphore(&self, id: u32) -> Result<(), String> {
389        self.semaphore_request(2, &[id], Vec::new())?
390            .check()
391            .map_err(|e| format!("the server did not signal the semaphore: {e}"))
392    }
393
394    fn free_semaphore(&self, id: u32) {
395        let _ = self.semaphore_request(3, &[id], Vec::new());
396    }
397
398    /// Wait for the blit into `pixmap` to land: the reply to a read of one of its pixels comes
399    /// back only after the server's GPU work ordered before it has completed.
400    fn await_blit(&self, pixmap: xproto::Pixmap) -> Result<(), String> {
401        self.conn
402            .get_image(ImageFormat::Z_PIXMAP, pixmap, 0, 0, 1, 1, !0u32)
403            .map_err(|e| x_err("blit sync", e))?
404            .reply()
405            .map_err(|e| x_err("blit sync reply", e))?;
406        Ok(())
407    }
408
409    /// Hand the server an ARGB image as a pixmap it can composite from. `argb` is one
410    /// premultiplied 0xAARRGGBB word per pixel, which is what Render's OVER expects.
411    fn upload_sprite(&self, argb: &[u32], width: u16, height: u16) -> Result<Sprite, String> {
412        let conn = &self.conn;
413        let pixmap = conn
414            .generate_id()
415            .map_err(|e| x_err("sprite pixmap id", e))?;
416        conn.create_pixmap(32, pixmap, self.root, width, height)
417            .map_err(|e| x_err("sprite CreatePixmap", e))?;
418        let gc = conn.generate_id().map_err(|e| x_err("sprite GC id", e))?;
419        conn.create_gc(
420            gc,
421            pixmap,
422            &xproto::CreateGCAux::new().graphics_exposures(0),
423        )
424        .map_err(|e| x_err("sprite CreateGC", e))?;
425        let picture = conn
426            .generate_id()
427            .map_err(|e| x_err("sprite picture id", e))?;
428        conn.render_create_picture(
429            picture,
430            pixmap,
431            self.argb_format,
432            &render::CreatePictureAux::new(),
433        )
434        .map_err(|e| x_err("sprite CreatePicture", e))?;
435        let mut data = Vec::with_capacity(argb.len() * 4);
436        for px in argb {
437            let bytes = if self.byte_order == ImageOrder::LSB_FIRST {
438                px.to_le_bytes()
439            } else {
440                px.to_be_bytes()
441            };
442            data.extend_from_slice(&bytes);
443        }
444        conn.put_image(
445            ImageFormat::Z_PIXMAP,
446            pixmap,
447            gc,
448            width,
449            height,
450            0,
451            0,
452            0,
453            32,
454            &data,
455        )
456        .map_err(|e| x_err("sprite PutImage", e))?;
457        Ok(Sprite {
458            pixmap,
459            picture,
460            gc,
461            width,
462            height,
463        })
464    }
465
466    fn free_sprite(&self, s: &Sprite) {
467        let _ = self.conn.render_free_picture(s.picture);
468        let _ = self.conn.free_gc(s.gc);
469        let _ = self.conn.free_pixmap(s.pixmap);
470    }
471
472    fn free_buffer(&self, b: &GpuBuffer) {
473        let _ = self.conn.render_free_picture(b.picture);
474        let _ = self.conn.free_pixmap(b.pixmap);
475    }
476
477    fn free_cursor(&self, c: &CursorSprite) {
478        let _ = self.conn.render_free_picture(c.picture);
479        let _ = self.conn.free_gc(c.gc);
480        let _ = self.conn.free_pixmap(c.pixmap);
481    }
482}
483
484impl GpuCapture {
485    /// One buffer of the pool: a single-plane allocation the server and the encoder both
486    /// describe with one fd, stride, and offset. A modifier that spreads the buffer over
487    /// auxiliary planes (a compressed layout) is dropped from the server's list and the
488    /// allocation retried, down to the driver's own choice.
489    fn alloc_bo(&mut self, w: u16, h: u16) -> Result<BufferObject<()>, String> {
490        loop {
491            let bo = if self.x.modifiers.is_empty() {
492                crate::alloc_render_target(&self.gbm, w as u32, h as u32, GbmFormat::Argb8888)
493            } else {
494                // The entry point without the flags argument, which implies exactly the
495                // rendering use this asks for: the one that takes flags arrived in Mesa 21.1
496                // and a wheel referencing it does not load at all against an older libgbm,
497                // since Python resolves an extension's symbols eagerly.
498                self.gbm.create_buffer_object_with_modifiers::<()>(
499                    w as u32,
500                    h as u32,
501                    GbmFormat::Argb8888,
502                    self.x.modifiers.iter().map(|&m| gbm::Modifier::from(m)),
503                )
504            };
505            let bo = match bo {
506                Ok(bo) => bo,
507                Err(e) if !self.x.modifiers.is_empty() => {
508                    crate::log::debug!(
509                        "[X11] DRI3 capture: allocation with the server's modifiers failed ({e:?}); trying without"
510                    );
511                    self.x.modifiers.clear();
512                    continue;
513                }
514                Err(e) => return Err(format!("GBM allocation {w}x{h}: {e:?}")),
515            };
516            if bo.plane_count() <= 1 {
517                return Ok(bo);
518            }
519            let chosen: u64 = bo.modifier().into();
520            if self.x.modifiers.is_empty() {
521                return Err(format!(
522                    "the driver lays {w}x{h} out over {} planes even unconstrained",
523                    bo.plane_count()
524                ));
525            }
526            crate::log::debug!(
527                "[X11] DRI3 capture: modifier {chosen:#x} lays the buffer out in {} planes; trying another",
528                bo.plane_count()
529            );
530            drop(bo);
531            prune_modifier(&mut self.x.modifiers, chosen);
532        }
533    }
534
535    /// Allocate the buffer pool at `w` x `h` and hand each buffer to the server as a pixmap.
536    fn alloc_buffers(&mut self, w: u16, h: u16) -> Result<(), String> {
537        let mut fresh = Vec::with_capacity(POOL_N);
538        for _ in 0..POOL_N {
539            let bo = self.alloc_bo(w, h)?;
540            let dmabuf = crate::create_dmabuf_from_bo(&bo);
541            let server_fd = bo
542                .fd()
543                .map_err(|e| format!("dmabuf fd for the server: {e:?}"))?;
544            let modifier: u64 = bo.modifier().into();
545            let pixmap = self
546                .x
547                .conn
548                .generate_id()
549                .map_err(|e| x_err("pixmap id", e))?;
550            self.x
551                .conn
552                .dri3_pixmap_from_buffers(
553                    pixmap,
554                    self.x.root,
555                    w,
556                    h,
557                    bo.stride(),
558                    0,
559                    0,
560                    0,
561                    0,
562                    0,
563                    0,
564                    0,
565                    self.x.depth,
566                    32,
567                    modifier,
568                    vec![server_fd],
569                )
570                .map_err(|e| x_err("PixmapFromBuffers", e))?
571                .check()
572                .map_err(|e| {
573                    format!("the server did not import the buffer (modifier {modifier:#x}): {e}")
574                })?;
575            let picture = self
576                .x
577                .conn
578                .generate_id()
579                .map_err(|e| x_err("picture id", e))?;
580            self.x
581                .conn
582                .render_create_picture(
583                    picture,
584                    pixmap,
585                    self.x.rgb_format,
586                    &render::CreatePictureAux::new(),
587                )
588                .map_err(|e| x_err("CreatePicture", e))?
589                .check()
590                .map_err(|e| x_err("CreatePicture", e))?;
591            fresh.push(GpuBuffer {
592                _bo: bo,
593                dmabuf,
594                pixmap,
595                picture,
596            });
597        }
598        self.free_buffers();
599        self.buffers = fresh;
600        self.next = 0;
601        Ok(())
602    }
603
604    fn free_buffers(&mut self) {
605        for b in self.buffers.drain(..) {
606            self.x.free_buffer(&b);
607        }
608    }
609
610    /// Bring the pool and the encoder to a new capture size. NVENC follows in place; a session
611    /// that cannot is rebuilt through the same ladder that chose it.
612    fn reshape(&mut self, w: u16, h: u16) -> Result<(), String> {
613        self.alloc_buffers(w, h)?;
614        self.settings.width = w as i32;
615        self.settings.height = h as i32;
616        let in_place = match self.encoder.as_mut() {
617            Some(FrameEncoder::Nvenc(enc)) => {
618                enc.reconfigure_resolution(&self.settings).map(|_| ())
619            }
620            Some(FrameEncoder::Vaapi(_)) => {
621                Err("a VA-API session is rebuilt at a new size".to_string())
622            }
623            Some(FrameEncoder::Vpx(_) | FrameEncoder::Hevc(_) | FrameEncoder::Av1(_)) => {
624                Err("a software session takes host frames".to_string())
625            }
626            // Neither a Tegra nor a V4L2 M2M session reaches this path: both take host frames,
627            // so `open` below declines the zero-copy capture before one is built. The arms exist
628            // because the variants do, saying what would happen rather than panicking.
629            #[cfg(target_arch = "aarch64")]
630            Some(FrameEncoder::Tegra(_)) => Err("a Tegra session takes host frames".to_string()),
631            Some(FrameEncoder::V4l2m2m(_)) => {
632                Err("a V4L2 M2M session takes host frames".to_string())
633            }
634            None => Err("no session to reconfigure".to_string()),
635        };
636        if let Err(e) = in_place {
637            crate::log::debug!("[X11] DRI3 capture: {e}");
638            self.rebuild_encoder()?;
639        }
640        Ok(())
641    }
642
643    fn enc(&mut self) -> &mut FrameEncoder {
644        self.encoder
645            .as_mut()
646            .expect("the DRI3 capture always carries an encoder between frames")
647    }
648
649    /// The encoder's name for the logs.
650    fn backend(&self) -> &'static str {
651        self.encoder
652            .as_ref()
653            .map(|e| e.backend_name())
654            .unwrap_or("none")
655    }
656
657    /// Replace the encoder with the startup construction, releasing the old session first so
658    /// the two never hold device memory at once.
659    fn rebuild_encoder(&mut self) -> Result<(), String> {
660        self.detach_semaphore();
661        drop(self.encoder.take());
662        let source = FrameSource::Dmabuf {
663            egl_display: self.egl_display,
664        };
665        let mut settings = self.settings.clone();
666        match encoders::select_frame_encoder(&mut settings, source, None, "X11") {
667            Some(enc) if enc.is_hardware() => {
668                self.encoder = Some(enc);
669                self.settings = settings;
670                self.attach_semaphore();
671                Ok(())
672            }
673            Some(_) => Err("no hardware encoder is left for the zero-copy path".to_string()),
674            None => Err("no encoder could be rebuilt for the zero-copy path".to_string()),
675        }
676    }
677
678    /// Give the server the encoder's blit semaphore, where it offers SELKIES-SEMAPHORE and the
679    /// encoder can wait on one; otherwise each blit is waited for with a GetImage.
680    fn attach_semaphore(&mut self) {
681        self.detach_semaphore();
682        if self.x.semaphore_opcode.is_none() {
683            return;
684        }
685        let Some(fd) = self.enc().blit_semaphore_fd() else {
686            return;
687        };
688        match fd.and_then(|fd| self.x.import_semaphore(fd)) {
689            Ok(id) => {
690                self.blit_semaphore = Some(id);
691                println!(
692                    "[X11] DRI3 capture: the encoder waits for each blit on the GPU, behind a semaphore the server signals ({SEMAPHORE_EXTENSION})."
693                );
694            }
695            Err(e) => crate::log::debug!(
696                "[X11] DRI3 capture: no blit semaphore ({e}); each blit is waited for with a GetImage"
697            ),
698        }
699    }
700
701    fn detach_semaphore(&mut self) {
702        if let Some(id) = self.blit_semaphore.take() {
703            self.x.free_semaphore(id);
704        }
705    }
706
707    /// Blit the capture region into the next pool buffer, composite the cursor if asked, and
708    /// either have the server signal the blit semaphore after it or wait for it to land. Returns
709    /// the buffer to encode and whether its encode has to wait on the semaphore.
710    fn grab(&mut self, with_cursor: bool) -> Result<(usize, bool), String> {
711        let idx = self.next;
712        self.next = (self.next + 1) % self.buffers.len();
713        let (w, h) = (self.settings.width as u16, self.settings.height as u16);
714        let dst = self.buffers[idx].pixmap;
715        self.x
716            .conn
717            .copy_area(
718                self.x.root,
719                dst,
720                self.x.gc,
721                self.cap_x,
722                self.cap_y,
723                0,
724                0,
725                w,
726                h,
727            )
728            .map_err(|e| x_err("CopyArea", e))?;
729        if with_cursor {
730            self.composite_cursor(idx)?;
731        } else if let Some(c) = self.cursor.take() {
732            self.x.free_cursor(&c);
733        }
734        self.composite_watermark(idx)?;
735        if let Some(id) = self.blit_semaphore {
736            match self.x.signal_semaphore(id) {
737                Ok(()) => return Ok((idx, true)),
738                // No signal is coming, so this frame waits for its blit the other way, and so
739                // does every later one.
740                Err(e) => {
741                    eprintln!(
742                        "[X11] DRI3 capture: {e}; waiting for each blit with a GetImage from now on."
743                    );
744                    self.detach_semaphore();
745                }
746            }
747        }
748        self.x.await_blit(dst)?;
749        Ok((idx, false))
750    }
751
752    /// Draw the watermark over the freshly blitted frame, on the GPU. The image is uploaded
753    /// on its first frame and composited by the server after that, so the CPU never sees the
754    /// pixels it is drawn on; an animated location only moves where it is composited.
755    fn composite_watermark(&mut self, idx: usize) -> Result<(), String> {
756        let Some((pixels, w, h)) = self.overlay.sprite() else {
757            return Ok(());
758        };
759        if self.watermark.is_none() {
760            // `image` decodes to straight alpha and Render's OVER wants it premultiplied.
761            let argb: Vec<u32> = pixels
762                .as_chunks::<4>()
763                .0
764                .iter()
765                .map(|p| {
766                    let a = p[3] as u32;
767                    let pm = |c: u8| (c as u32 * a / 255) & 0xff;
768                    (a << 24) | (pm(p[0]) << 16) | (pm(p[1]) << 8) | pm(p[2])
769                })
770                .collect();
771            self.watermark = Some(self.x.upload_sprite(&argb, w as u16, h as u16)?);
772        }
773        let (fw, fh) = (self.settings.width, self.settings.height);
774        self.overlay
775            .update_position(fw, fh, self.request.watermark_location_enum);
776        let (x, y) = self.overlay.position();
777        let sprite = self.watermark.as_ref().unwrap();
778        if x >= fw || y >= fh || x + sprite.width as i32 <= 0 || y + sprite.height as i32 <= 0 {
779            return Ok(());
780        }
781        self.x
782            .conn
783            .render_composite(
784                render::PictOp::OVER,
785                sprite.picture,
786                x11rb::NONE,
787                self.buffers[idx].picture,
788                0,
789                0,
790                0,
791                0,
792                x.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
793                y.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
794                sprite.width,
795                sprite.height,
796            )
797            .map_err(|e| x_err("watermark Composite", e))?;
798        Ok(())
799    }
800
801    /// Draw the XFixes cursor over the freshly blitted frame, uploading its image only when the
802    /// cursor changed. The server paints it through Render, on the GPU.
803    fn composite_cursor(&mut self, idx: usize) -> Result<(), String> {
804        let Some(c) = self
805            .x
806            .conn
807            .xfixes_get_cursor_image()
808            .ok()
809            .and_then(|c| c.reply().ok())
810        else {
811            return Ok(());
812        };
813        if c.width == 0 || c.height == 0 {
814            return Ok(());
815        }
816        let stale = self.cursor.as_ref().is_none_or(|s| {
817            s.serial != c.cursor_serial || s.width != c.width || s.height != c.height
818        });
819        if stale {
820            if let Some(old) = self.cursor.take() {
821                self.x.free_cursor(&old);
822            }
823            let conn = &self.x.conn;
824            let pixmap = conn
825                .generate_id()
826                .map_err(|e| x_err("cursor pixmap id", e))?;
827            conn.create_pixmap(32, pixmap, self.x.root, c.width, c.height)
828                .map_err(|e| x_err("cursor CreatePixmap", e))?;
829            let gc = conn.generate_id().map_err(|e| x_err("cursor GC id", e))?;
830            conn.create_gc(
831                gc,
832                pixmap,
833                &xproto::CreateGCAux::new().graphics_exposures(0),
834            )
835            .map_err(|e| x_err("cursor CreateGC", e))?;
836            let picture = conn
837                .generate_id()
838                .map_err(|e| x_err("cursor picture id", e))?;
839            conn.render_create_picture(
840                picture,
841                pixmap,
842                self.x.argb_format,
843                &render::CreatePictureAux::new(),
844            )
845            .map_err(|e| x_err("cursor CreatePicture", e))?;
846            let mut data = Vec::with_capacity(c.cursor_image.len() * 4);
847            for px in &c.cursor_image {
848                let bytes = if self.x.byte_order == ImageOrder::LSB_FIRST {
849                    px.to_le_bytes()
850                } else {
851                    px.to_be_bytes()
852                };
853                data.extend_from_slice(&bytes);
854            }
855            conn.put_image(
856                ImageFormat::Z_PIXMAP,
857                pixmap,
858                gc,
859                c.width,
860                c.height,
861                0,
862                0,
863                0,
864                32,
865                &data,
866            )
867            .map_err(|e| x_err("cursor PutImage", e))?;
868            self.cursor = Some(CursorSprite {
869                pixmap,
870                picture,
871                gc,
872                width: c.width,
873                height: c.height,
874                serial: c.cursor_serial,
875            });
876        }
877        let sprite = self.cursor.as_ref().unwrap();
878        let (img_x, img_y) = cursor_image_origin(
879            c.x,
880            c.y,
881            c.xhot,
882            c.yhot,
883            self.cap_x as i32,
884            self.cap_y as i32,
885        );
886        let (fw, fh) = (self.settings.width, self.settings.height);
887        if img_x >= fw || img_y >= fh || img_x + c.width as i32 <= 0 || img_y + c.height as i32 <= 0
888        {
889            return Ok(());
890        }
891        self.x
892            .conn
893            .render_composite(
894                render::PictOp::OVER,
895                sprite.picture,
896                x11rb::NONE,
897                self.buffers[idx].picture,
898                0,
899                0,
900                0,
901                0,
902                img_x.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
903                img_y.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
904                sprite.width,
905                sprite.height,
906            )
907            .map_err(|e| x_err("cursor Composite", e))?;
908        Ok(())
909    }
910}
911
912impl Drop for GpuCapture {
913    fn drop(&mut self) {
914        self.detach_semaphore();
915        if let Some(c) = self.cursor.take() {
916            self.x.free_cursor(&c);
917        }
918        if let Some(w) = self.watermark.take() {
919            self.x.free_sprite(&w);
920        }
921        self.free_buffers();
922        let _ = self.x.conn.free_gc(self.x.gc);
923        let _ = self.x.conn.flush();
924    }
925}
926
927/// Build the zero-copy capture for these settings, or report why it cannot serve them.
928///
929/// Everything fallible happens here, before a single frame is delivered, so the caller can fall
930/// back to the XShm path with nothing half-built. The server is asked first, over a connection
931/// of our own, because whether it draws on the GPU at all, and on which device, is its answer to
932/// give; the encoder is built on the device the server named.
933fn open(settings: &RustCaptureSettings) -> Option<GpuCapture> {
934    if !settings.codec.is_video() {
935        return declined("the codec is JPEG");
936    }
937    if settings.use_cpu || settings.encode_node_index == -1 {
938        return declined("software encoding was requested");
939    }
940    let node = settings.encode_node_index.max(0);
941    let driver = crate::get_gpu_driver(node);
942    if driver.is_empty() {
943        return declined(&format!("encode node {node} has no readable render node"));
944    }
945    let (x, device_fd) = match XScreen::open(node) {
946        Ok(v) => v,
947        Err(e) => return declined(&e),
948    };
949    crate::report::zero_copy_available(true);
950
951    let alloc_file = File::from(device_fd);
952    let egl_file = match alloc_file.try_clone() {
953        Ok(f) => f,
954        Err(e) => return declined(&format!("dup of the DRI3 device: {e}")),
955    };
956    let gbm = match RawGbmDevice::new(alloc_file) {
957        Ok(d) => d,
958        Err(e) => return declined(&format!("no GBM device on {}: {e:?}", x.device)),
959    };
960    // NVENC imports through an EGL display on the same device; VA-API needs none.
961    let wants_egl = crate::driver_selects_nvenc(&driver);
962    let egl = GbmDevice::new(egl_file)
963        .ok()
964        .and_then(|dev| unsafe { EGLDisplay::new(dev) }.ok());
965    if wants_egl && egl.is_none() {
966        return declined(&format!(
967            "no EGL display on {} for the NVENC import",
968            x.device
969        ));
970    }
971    let egl_display = egl
972        .as_ref()
973        .map(|e| e.get_display_handle().handle)
974        .unwrap_or(std::ptr::null());
975
976    let geo = match x
977        .conn
978        .get_geometry(x.root)
979        .ok()
980        .and_then(|c| c.reply().ok())
981    {
982        Some(g) => g,
983        None => return declined("the root geometry could not be read"),
984    };
985    let mut watermark = crate::encoders::overlay::OverlayState::default();
986    if !settings.watermark_path.is_empty() {
987        watermark.load_watermark(&settings.watermark_path, 1.0);
988    }
989    let request = settings.clone();
990    let mut live = settings.clone();
991    let (w, h) = resolve_dims(geo.width, geo.height, &request);
992    live.width = w as i32;
993    live.height = h as i32;
994
995    let mut gpu = GpuCapture {
996        encoder: None,
997        buffers: Vec::new(),
998        cursor: None,
999        overlay: watermark,
1000        watermark: None,
1001        cap_x: clamp_offset(request.capture_x, geo.width),
1002        cap_y: clamp_offset(request.capture_y, geo.height),
1003        root_w: geo.width,
1004        root_h: geo.height,
1005        blit_semaphore: None,
1006        x,
1007        gbm,
1008        _egl: egl,
1009        egl_display,
1010        next: 0,
1011        settings: live,
1012        request,
1013    };
1014    if let Err(e) = gpu.alloc_buffers(w, h) {
1015        return declined(&e);
1016    }
1017    let mut live = gpu.settings.clone();
1018    gpu.encoder = match encoders::select_frame_encoder(
1019        &mut live,
1020        FrameSource::Dmabuf { egl_display },
1021        None,
1022        "X11",
1023    ) {
1024        Some(enc) if enc.is_hardware() => Some(enc),
1025        Some(_) => return declined("the session encodes in software"),
1026        None => return declined("no hardware encoder opened for this session"),
1027    };
1028    gpu.settings = live;
1029    gpu.attach_semaphore();
1030    println!(
1031        "[X11] Zero-copy capture (DRI3): {}x{} blitted by the X server into {} GPU buffers on {}, encoded in place on {}.",
1032        gpu.settings.width,
1033        gpu.settings.height,
1034        POOL_N,
1035        gpu.x.device,
1036        gpu.backend()
1037    );
1038    crate::report::capture("DRI3", true);
1039    crate::log_stream_settings("X11", &gpu.settings, 1, gpu.encoder.as_ref());
1040    Some(gpu)
1041}
1042
1043/// Run the zero-copy X11 capture until `controls.stop` is set, or report that it cannot start.
1044///
1045/// `None` means DRI3 cannot serve this session and nothing was delivered, which is the caller's
1046/// signal to run the XShm path instead. That includes a first frame the encoder could not read,
1047/// since a buffer the server imported is not yet proof the encoder can. Anything else means the
1048/// capture ran, every frame going from the server's blit into the encoder without a copy, and
1049/// carries how it ended.
1050///
1051/// Each iteration paces to the live target rate, applies the cross-thread controls on the thread
1052/// that owns the sessions, follows the root geometry, asks the server whether anything was drawn,
1053/// and only then blits, waits, and encodes through the same send / quality / key-frame policy every
1054/// full-frame encoder obeys.
1055pub fn run_capture<F>(
1056    settings: RustCaptureSettings,
1057    controls: Arc<Controls>,
1058    encode_tid_tx: std::sync::mpsc::Sender<std::thread::ThreadId>,
1059    on_frame: &mut F,
1060) -> Option<Result<(), String>>
1061where
1062    F: FnMut(Vec<EncodedStripe>),
1063{
1064    let mut gpu = open(&settings)?;
1065    controls
1066        .codec
1067        .store(gpu.enc().codec().id(), Ordering::Relaxed);
1068
1069    let recording_sink = RecordingSink::try_bind(&settings.recording_socket, settings.target_fps);
1070    let mut state = StripeState::default();
1071    let mut frame_counter: u16 = 0;
1072    let mut pending_force_idr = true;
1073    let mut pace = FramePace::default();
1074    let mut geometry_check = GEOMETRY_POLL_FRAMES;
1075    let mut x_failures = 0u32;
1076    let mut encode_errors = 0u32;
1077    let mut encoder_rebuilt = false;
1078    // Capture and encode share this thread, so it is what the re-entrant-stop guard watches;
1079    // it is named only once the path is committed, because a decline on the first frame hands
1080    // the session to XShm, whose own encode thread must then be the one on the channel.
1081    let mut delivered_any = false;
1082    let mut last_log = Instant::now();
1083    let mut sent_frames: u64 = 0;
1084    let mut damaged_frames: u64 = 0;
1085    let mut first_frame = true;
1086    let mut tid_sent = false;
1087
1088    while !controls.stop.load(Ordering::Relaxed) {
1089        let fps = f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
1090        let frame_dur = Duration::from_secs_f64(1.0 / fps.max(1.0));
1091        let trigger = wait_for_frame(&gpu.x.conn, Some(&gpu.x.damage), &pace, frame_dur);
1092        let woke = Instant::now();
1093        // The report is spent where it is read, so a change racing this frame wakes the next one
1094        // rather than being cleared along with what the blit captured.
1095        let is_dirty = trigger == TickTrigger::Damage || first_frame;
1096        // Where the root changed, None where the server cannot say or the frame is the first.
1097        let changed = gpu
1098            .x
1099            .damage
1100            .take(&gpu.x.conn)
1101            .filter(|rects| !first_frame && !rects.is_empty());
1102        first_frame = false;
1103        damaged_frames += is_dirty as u64;
1104        if controls.stop.load(Ordering::Relaxed) {
1105            break;
1106        }
1107
1108        for report in std::mem::take(&mut *controls.reference_reports.lock().unwrap()) {
1109            if !gpu.enc().take_report(report) {
1110                pending_force_idr = true;
1111            }
1112        }
1113        if controls.force_idr.swap(false, Ordering::Relaxed)
1114            || recording_sink
1115                .as_ref()
1116                .is_some_and(|s| s.should_force_idr())
1117        {
1118            pending_force_idr = true;
1119        }
1120        if controls.rate_dirty.swap(false, Ordering::Acquire) {
1121            gpu.settings.video_bitrate_kbps = controls.bitrate_kbps.load(Ordering::Relaxed);
1122            gpu.settings.video_vbv_multiplier =
1123                controls.vbv_mult_milli.load(Ordering::Relaxed) as f64 / 1000.0;
1124            gpu.settings.target_fps = fps;
1125            let live = gpu.settings.clone();
1126            if let Err(e) = gpu.enc().reconfigure_rate(&live) {
1127                eprintln!(
1128                    "[X11] DRI3 capture: rate reconfigure failed ({e}); rebuilding the encoder."
1129                );
1130                if let Err(e) = gpu.rebuild_encoder() {
1131                    return Some(Err(format!("DRI3 capture ended: {e}")));
1132                }
1133                state = StripeState::default();
1134                pending_force_idr = true;
1135            }
1136        }
1137        if controls.tunables_dirty.swap(false, Ordering::Acquire)
1138            && let Some(t) = controls.tunables.lock().unwrap().take()
1139        {
1140            t.apply_to(&mut gpu.settings);
1141        }
1142
1143        let want_cursor = controls.capture_cursor.load(Ordering::Relaxed);
1144        let mut recheck_geometry = controls.region_dirty.swap(false, Ordering::Acquire);
1145        if recheck_geometry {
1146            let (nx, ny, nw, nh) = *controls.region.lock().unwrap();
1147            gpu.request.capture_x = nx;
1148            gpu.request.capture_y = ny;
1149            gpu.request.width = nw;
1150            gpu.request.height = nh;
1151            gpu.request.auto_adjust_screen_capture_size = nw <= 0 || nh <= 0;
1152        }
1153        geometry_check -= 1;
1154        if geometry_check <= 0 {
1155            geometry_check = GEOMETRY_POLL_FRAMES;
1156            recheck_geometry = true;
1157        }
1158        if recheck_geometry {
1159            match gpu
1160                .x
1161                .conn
1162                .get_geometry(gpu.x.root)
1163                .ok()
1164                .and_then(|c| c.reply().ok())
1165            {
1166                Some(g) => {
1167                    gpu.root_w = g.width;
1168                    gpu.root_h = g.height;
1169                }
1170                None => {
1171                    x_failures += 1;
1172                    if x_failures > MAX_X_FAILURES {
1173                        return Some(Err(
1174                            "DRI3 capture ended: the X server stopped answering".to_string()
1175                        ));
1176                    }
1177                    continue;
1178                }
1179            }
1180            gpu.cap_x = clamp_offset(gpu.request.capture_x, gpu.root_w);
1181            gpu.cap_y = clamp_offset(gpu.request.capture_y, gpu.root_h);
1182            let (w, h) = resolve_dims(gpu.root_w, gpu.root_h, &gpu.request);
1183            if w as i32 != gpu.settings.width || h as i32 != gpu.settings.height {
1184                if let Err(e) = gpu.reshape(w, h) {
1185                    return Some(Err(format!(
1186                        "DRI3 capture could not follow the new geometry: {e}"
1187                    )));
1188                }
1189                state = StripeState::default();
1190                pending_force_idr = true;
1191            }
1192        }
1193
1194        let quality = EncoderQuality::of(gpu.enc());
1195        let decision = decide_hw_fullframe(
1196            &mut state,
1197            &gpu.settings,
1198            frame_counter,
1199            match changed {
1200                _ if !is_dirty => Damage::None,
1201                Some(rects) => captured_damage(
1202                    &rects,
1203                    gpu.cap_x.into(),
1204                    gpu.cap_y.into(),
1205                    gpu.settings.width,
1206                    gpu.settings.height,
1207                ),
1208                None => Damage::Unknown,
1209            },
1210            false,
1211            pending_force_idr,
1212            quality,
1213        );
1214        // A tick that publishes nothing is no frame: counted as one, it would hold a change
1215        // landing just after it back by a pull's worth of budget or a whole period.
1216        if decision.send {
1217            pace.ticked(trigger, frame_dur, woke, false);
1218        } else {
1219            pace.defer(woke + frame_dur);
1220        }
1221        let mut delivered = false;
1222        if decision.send {
1223            // The frame is the screen as the CopyArea finds it, so it is stamped as that is sent:
1224            // a later reading would leave out the wait for the blit, which is most of a frame's
1225            // latency under a GPU-bound client.
1226            let grabbed_ns = crate::wayland::host::now_ns();
1227            let (idx, after_blit) = match gpu.grab(want_cursor) {
1228                Ok(i) => {
1229                    x_failures = 0;
1230                    i
1231                }
1232                Err(e) => {
1233                    x_failures += 1;
1234                    if x_failures > MAX_X_FAILURES {
1235                        return Some(Err(format!("DRI3 capture ended: {e}")));
1236                    }
1237                    frame_counter = frame_counter.wrapping_add(1);
1238                    continue;
1239                }
1240            };
1241            let dmabuf = gpu.buffers[idx].dmabuf.clone();
1242            decision.prepare(gpu.enc());
1243            let encode_start_ns = crate::wayland::host::now_ns();
1244            let result = if after_blit {
1245                gpu.enc().encode_dmabuf_after_blit(
1246                    &dmabuf,
1247                    frame_counter as u64,
1248                    decision.target_qp,
1249                    decision.force_idr,
1250                )
1251            } else {
1252                gpu.enc().encode_dmabuf(
1253                    &dmabuf,
1254                    frame_counter as u64,
1255                    decision.target_qp,
1256                    decision.force_idr,
1257                )
1258            };
1259            match result {
1260                Ok(data) if !data.is_empty() => {
1261                    encode_errors = 0;
1262                    let stripes = vec![EncodedStripe {
1263                        data: Arc::new(data),
1264                        codec: gpu.settings.codec,
1265                        stripe_y_start: 0,
1266                        stripe_height: gpu.settings.height,
1267                        frame_id: frame_counter as i32,
1268                        timing: FrameTiming {
1269                            capture_ns: grabbed_ns,
1270                            encode_start_ns,
1271                            encode_end_ns: crate::wayland::host::now_ns(),
1272                        },
1273                        reference: gpu.enc().last_reference(),
1274                    }];
1275                    if let Some(sink) = &recording_sink {
1276                        sink.write_frame(&stripes, gpu.settings.width, gpu.settings.height);
1277                    }
1278                    sent_frames += 1;
1279                    delivered = true;
1280                    delivered_any = true;
1281                    on_frame(stripes);
1282                }
1283                Ok(_) => {
1284                    encode_errors = 0;
1285                    delivered_any = true;
1286                }
1287                Err(e) => {
1288                    if !delivered_any {
1289                        // Nothing has reached a client, so this is still the time to decline.
1290                        drop(gpu);
1291                        declined(&format!(
1292                            "the encoder could not read the server's buffer: {e}"
1293                        ));
1294                        return None;
1295                    }
1296                    encode_errors += 1;
1297                    if encode_errors % crate::HW_ERROR_RECOVERY_THRESHOLD == 1 {
1298                        eprintln!("[X11] hardware encode error on the DRI3 path: {e}");
1299                    }
1300                    if encode_errors >= crate::HW_ERROR_RECOVERY_THRESHOLD {
1301                        if encoder_rebuilt {
1302                            return Some(Err(
1303                                "the encoder failed repeatedly on the DRI3 path".to_string()
1304                            ));
1305                        }
1306                        eprintln!(
1307                            "[X11] rebuilding the encoder after repeated errors on the DRI3 path."
1308                        );
1309                        if let Err(e) = gpu.rebuild_encoder() {
1310                            return Some(Err(format!("DRI3 capture ended: {e}")));
1311                        }
1312                        encoder_rebuilt = true;
1313                        encode_errors = 0;
1314                        state = StripeState::default();
1315                        pending_force_idr = true;
1316                    }
1317                }
1318            }
1319        }
1320        if delivered_any && !tid_sent {
1321            let _ = encode_tid_tx.send(std::thread::current().id());
1322            tid_sent = true;
1323        }
1324        pending_force_idr = (pending_force_idr || decision.force_idr) && !delivered;
1325        frame_counter = frame_counter.wrapping_add(1);
1326
1327        let elapsed = last_log.elapsed().as_secs_f64();
1328        if elapsed >= 1.0 {
1329            crate::log::debug!(
1330                "[X11] DRI3 {}x{} Encoder: {} EncFPS: {:.2} Damaged/s: {:.2}",
1331                gpu.settings.width,
1332                gpu.settings.height,
1333                gpu.backend(),
1334                sent_frames as f64 / elapsed,
1335                damaged_frames as f64 / elapsed
1336            );
1337            sent_frames = 0;
1338            damaged_frames = 0;
1339            last_log = Instant::now();
1340        }
1341    }
1342    Some(Ok(()))
1343}
1344
1345#[cfg(test)]
1346mod modifier_tests {
1347    use super::{intel_compressed, prune_modifier};
1348
1349    const CCS_CC: u64 = 0x0100_0000_0000_0008;
1350    const CCS: u64 = 0x0100_0000_0000_0006;
1351    const Y_TILED: u64 = 0x0100_0000_0000_0002;
1352
1353    /// The list Xvfb offers on an Arc card keeps its linear and tiled layouts and loses both
1354    /// compressed ones, the single-plane one included.
1355    #[test]
1356    fn the_single_plane_compressed_layouts_are_not_offered() {
1357        const X_TILED: u64 = 0x0100_0000_0000_0001;
1358        const TILE4: u64 = 0x0100_0000_0000_0009;
1359        const DG2_RC_CCS: u64 = 0x0100_0000_0000_000a;
1360        const DG2_RC_CCS_CC: u64 = 0x0100_0000_0000_000c;
1361        let mut mods = vec![0, X_TILED, TILE4, DG2_RC_CCS, DG2_RC_CCS_CC];
1362        mods.retain(|&m| !intel_compressed(m));
1363        assert_eq!(mods, vec![0, X_TILED, TILE4]);
1364        assert!(intel_compressed(CCS) && intel_compressed(CCS_CC));
1365        assert!(!intel_compressed(Y_TILED) && !intel_compressed(0x00ff_ffff_ffff_ffff));
1366    }
1367
1368    /// A compressed layout the server offered is dropped and the rest stays offered, so the
1369    /// retry walks the server's list down to a single-plane layout.
1370    #[test]
1371    fn a_rejected_modifier_leaves_the_others() {
1372        let mut mods = vec![CCS_CC, CCS, Y_TILED];
1373        prune_modifier(&mut mods, CCS_CC);
1374        assert_eq!(mods, vec![CCS, Y_TILED]);
1375        prune_modifier(&mut mods, CCS);
1376        assert_eq!(mods, vec![Y_TILED]);
1377    }
1378
1379    /// A layout the list never named came from the driver's own choice, so the list is spent
1380    /// and the next allocation is unconstrained rather than retried against the same list.
1381    #[test]
1382    fn a_choice_outside_the_list_spends_it() {
1383        let mut mods = vec![CCS, Y_TILED];
1384        prune_modifier(&mut mods, 0x00ff_ffff_ffff_ffff);
1385        assert!(mods.is_empty());
1386    }
1387}
1388
1389#[cfg(test)]
1390mod gpu_tests {
1391    use super::super::gpu_test_support::{decoded_mean, paint_root, painted_ycbcr, settings};
1392    use super::*;
1393    use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, VIDEO_HEADER_LEN, parse_video_type};
1394    use crate::webcam::decode::{Codec as DecCodec, Decoder, VideoDecoder};
1395
1396    /// Encode the buffer `grab` returned, behind the blit semaphore where the server signals it,
1397    /// as the capture does.
1398    fn encode_grabbed(
1399        gpu: &mut GpuCapture,
1400        (idx, after_blit): (usize, bool),
1401        i: u64,
1402        key: bool,
1403    ) -> Vec<u8> {
1404        let dmabuf = gpu.buffers[idx].dmabuf.clone();
1405        if after_blit {
1406            gpu.enc().encode_dmabuf_after_blit(&dmabuf, i, 25, key)
1407        } else {
1408            gpu.enc().encode_dmabuf(&dmabuf, i, 25, key)
1409        }
1410        .expect("encode in place")
1411    }
1412
1413    /// The primary node of the encode node's GPU is the same GPU to the DRI3 check, as the
1414    /// node an Xorg on the modesetting driver hands its DRI3 clients. Ignored by default.
1415    #[test]
1416    #[ignore]
1417    fn gpu_dri3_counts_the_primary_node_of_the_encode_gpu_as_it() {
1418        let node = settings(crate::encoders::codec::Codec::H264)
1419            .encode_node_index
1420            .max(0);
1421        let render = format!("/dev/dri/renderD{}", 128 + node);
1422        let render_dev = std::fs::metadata(&render).expect("encode node").rdev();
1423        let primary = std::fs::read_dir(format!("/sys/class/drm/renderD{}/device/drm", 128 + node))
1424            .expect("the encode node's GPU in sysfs")
1425            .filter_map(|e| e.ok()?.file_name().into_string().ok())
1426            .find(|name| name.starts_with("card"))
1427            .expect("a primary node on the encode node's GPU");
1428        let primary_dev = std::fs::metadata(format!("/dev/dri/{primary}"))
1429            .expect("primary node")
1430            .rdev();
1431        assert_ne!(
1432            primary_dev, render_dev,
1433            "{primary} and {render} are distinct nodes"
1434        );
1435        assert!(
1436            same_gpu(primary_dev, render_dev),
1437            "{primary} is the GPU of {render}"
1438        );
1439    }
1440
1441    /// The watermark is composited by the server, so it reaches the frame without the CPU
1442    /// touching a pixel, and a translucent one blends as the readback paths blend it: Render's
1443    /// OVER takes premultiplied alpha, and a straight-alpha upload would darken it instead.
1444    /// Ignored by default; same server and encoder as the check below.
1445    #[test]
1446    #[ignore]
1447    fn gpu_dri3_composites_a_watermark_without_a_readback() {
1448        const SCREEN: (u8, u8, u8) = (0x20, 0x40, 0x80);
1449        const MARK: (u8, u8, u8) = (0xff, 0x00, 0x00);
1450        const ALPHA: u8 = 128;
1451        if !paint_root(SCREEN) {
1452            println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1453            return;
1454        }
1455        let path = std::env::temp_dir().join("pixelflux-dri3-watermark.png");
1456        image::RgbaImage::from_pixel(96, 96, image::Rgba([MARK.0, MARK.1, MARK.2, ALPHA]))
1457            .save(&path)
1458            .expect("write the watermark");
1459        let mut s = settings(crate::encoders::codec::Codec::H264);
1460        s.watermark_path = path.to_string_lossy().into_owned();
1461        let Some(mut gpu) = open(&s) else {
1462            println!(
1463                "the DRI3 path declined a watermarked session on this host; nothing to capture"
1464            );
1465            return;
1466        };
1467        let grabbed = gpu.grab(false).expect("blit and composite");
1468        let pkt = encode_grabbed(&mut gpu, grabbed, 0, true);
1469        let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1470        assert!(
1471            dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
1472            "no picture"
1473        );
1474        let v = dec.frame().expect("decoded frame");
1475        // Straight-alpha source over the painted screen, which is what both paths must produce.
1476        let blend =
1477            |m: u8, bg: u8| (m as f64 * ALPHA as f64 + bg as f64 * (255.0 - ALPHA as f64)) / 255.0;
1478        let want = crate::encoders::chroma_siting::ycbcr(
1479            [
1480                blend(MARK.0, SCREEN.0),
1481                blend(MARK.1, SCREEN.1),
1482                blend(MARK.2, SCREEN.2),
1483            ],
1484            crate::encoders::chroma_siting::BT709,
1485        );
1486        let (mx, my) = (48usize, 48usize);
1487        let got = [
1488            v.y[my * v.y_stride + mx] as f64,
1489            v.u[(my / 2) * v.uv_stride + mx / 2] as f64,
1490            v.v[(my / 2) * v.uv_stride + mx / 2] as f64,
1491        ];
1492        for i in 0..3 {
1493            assert!(
1494                (got[i] - want[i]).abs() <= 10.0,
1495                "watermark plane {i}: captured {:.1}, blended {:.1}",
1496                got[i],
1497                want[i]
1498            );
1499        }
1500        let _ = std::fs::remove_file(&path);
1501        println!("the server composited the watermark at ({mx},{my}) to Y/Cb/Cr {got:?}");
1502    }
1503
1504    /// On a DRI3 server whose screen lives on the GPU: the server imports the pool, a blit of a
1505    /// painted root decodes to the painted color, and a repaint reaches the next frame, which it
1506    /// could not if the encoder read a stale buffer. Ignored by default; run with `DISPLAY` on
1507    /// such a server (XLibre's Xvfb with `-glamor -dri`, an Xorg on a DRM driver) and a
1508    /// hardware encoder on the same device:
1509    /// `cargo test gpu_dri3 -- --ignored --nocapture --test-threads=1`.
1510    #[test]
1511    #[ignore]
1512    fn gpu_dri3_zero_copy_encodes_the_painted_root() {
1513        const FIRST: (u8, u8, u8) = (0x20, 0x40, 0xc0);
1514        const SECOND: (u8, u8, u8) = (0xd0, 0x50, 0x18);
1515        if !paint_root(FIRST) {
1516            println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1517            return;
1518        }
1519        let Some(mut gpu) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1520            println!("the DRI3 path declined this session on this host; nothing to capture");
1521            return;
1522        };
1523        let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1524
1525        let encode = |gpu: &mut GpuCapture, i: u64, key: bool| -> Vec<u8> {
1526            let grabbed = gpu.grab(false).expect("blit into the pool");
1527            encode_grabbed(gpu, grabbed, i, key)
1528        };
1529
1530        let pkt = encode(&mut gpu, 0, true);
1531        assert_eq!(
1532            parse_video_type(pkt[1]),
1533            Some((crate::encoders::codec::Codec::H264, FRAME_KEY))
1534        );
1535        let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1536        let want = painted_ycbcr(FIRST);
1537        for i in 0..3 {
1538            assert!(
1539                (mean[i] - want[i]).abs() <= 8.0,
1540                "plane {i}: captured {:.1}, painted {:.1}",
1541                mean[i],
1542                want[i]
1543            );
1544        }
1545
1546        for i in 1..4u64 {
1547            let pkt = encode(&mut gpu, i, false);
1548            assert_eq!(
1549                parse_video_type(pkt[1]),
1550                Some((crate::encoders::codec::Codec::H264, FRAME_DELTA))
1551            );
1552            let _ = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1553        }
1554
1555        assert!(
1556            paint_root(SECOND),
1557            "the root was painted once, so a repaint must work"
1558        );
1559        let pkt = encode(&mut gpu, 4, false);
1560        let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1561        let want = painted_ycbcr(SECOND);
1562        for i in 0..3 {
1563            assert!(
1564                (mean[i] - want[i]).abs() <= 8.0,
1565                "after repaint, plane {i}: captured {:.1}, painted {:.1}",
1566                mean[i],
1567                want[i]
1568            );
1569        }
1570        println!(
1571            "captured {}x{} through {} server-imported buffers on {} and encoded in place on {}",
1572            gpu.settings.width,
1573            gpu.settings.height,
1574            POOL_N,
1575            gpu.x.device,
1576            gpu.backend()
1577        );
1578    }
1579}