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