Skip to main content

pixelflux/x11/
mod.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//! X11 host capture: grab the root window into host memory as BGRA, composite the XFixes hardware
8//! cursor and the watermark on the CPU, and feed each frame to [`pipeline::X11Pipeline`], which owns
9//! damage/stripe/encode. The grab goes through a shared-memory segment (XShm via x11rb) rather than
10//! a plain `GetImage` for one reason: a full-screen frame is far too large to copy through the X
11//! protocol socket every tick, so XShm has the server write the pixels straight into memory this
12//! process already has mapped.
13//!
14//! This is the general path, and it works against any X server. Two zero-copy paths come first
15//! where they can serve the session: [`dri3`] on any server whose screen lives on the GPU has the
16//! server blit the root into dmabufs the hardware encoder reads in place, and [`nvfbc`] on an
17//! NVIDIA X server has the driver composite the screen straight into video memory and registers
18//! that buffer with NVENC in place. `run_capture` tries them in that order and falls back here
19//! otherwise. Here a server that shares its framebuffer (`framebuffer`, the images' Xvfb with
20//! the screen in system memory) is read in place, each frame copying only the tiles its surface
21//! misses, with no request a frame, and XShm with XDamage serves every other server.
22//!
23//! `run_capture` splits the work across two threads because grabbing the next frame and encoding
24//! the previous one have no reason to wait on each other: the caller's thread grabs frames and owns
25//! the x11rb connection and the pool of shm surfaces, while a spawned encode thread owns the
26//! [`pipeline::X11Pipeline`] (and thus the encoder) and runs the delivery callback, so the two overlap for
27//! throughput. Frames hand off through a bounded `FramePool` that carries only a raw pointer and
28//! geometry — never an X object, none of which is safe to share — so nothing X-related ever crosses
29//! the thread boundary and the encoder never has to touch X. Multi-instance safety for the encoders
30//! is handled inside them (e.g. the libx264 open/close lock); each capture owns its own private xcb
31//! connection, so there is no shared X state to serialize here.
32
33use std::cell::Cell;
34use std::os::unix::io::AsRawFd;
35use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering};
36use std::sync::mpsc::Sender;
37use std::sync::{Arc, Condvar, Mutex};
38use std::thread;
39use std::time::{Duration, Instant};
40
41use x11rb::connection::Connection;
42use x11rb::protocol::Event;
43use x11rb::protocol::damage::{ConnectionExt as DamageExt, Damage, ReportLevel};
44use x11rb::protocol::shm::ConnectionExt as ShmExt;
45use x11rb::protocol::xfixes::{ConnectionExt as XfixesExt, Region};
46use x11rb::protocol::xproto::{
47    Atom, AtomEnum, ConnectionExt as XprotoExt, ImageFormat, PropMode, Rectangle, Window,
48};
49use x11rb::rust_connection::RustConnection;
50use x11rb::wrapper::ConnectionExt as WrapperExt;
51
52use crate::pace::{FramePace, TickTrigger};
53
54use crate::CaptureSettings;
55use crate::encoders::overlay::blend_pixel_premultiplied;
56use crate::encoders::software::{EncodedStripe, FrameTiming, Nv12Planes};
57use crate::pipeline::X11Pipeline;
58use crate::recording_sink::RecordingSink;
59
60pub mod computer_use;
61pub mod cursor;
62pub mod dri3;
63mod framebuffer;
64pub mod nvfbc;
65
66use framebuffer::{FramebufferGrab, Opened, SharedFramebuffer};
67
68/// Cross-thread controls for a running capture: a bag of atomics (plus two mutex-guarded
69/// payloads) the owning `ScreenCapture` pyclass flips from the Python thread and the capture thread
70/// reads at the top of each iteration.
71///
72/// It exists so Python never has to reach into the pipeline. The `X11Pipeline` and its encoder are
73/// only safe to touch from the capture and encode threads, so `request_idr` / rate / fps / region
74/// changes are posted here as atomic flags and applied later on the thread that owns the pipeline,
75/// rather than mutating encoder state across the thread boundary from Python.
76///
77/// 1. **Lifecycle**: `stop` ends the capture loop; `force_idr` requests an on-demand keyframe on the
78///    next processed frame; `reference_reports` are what clients said of frames, lost or held
79///    by every one of them, which the next processed frame applies first.
80/// 2. **Rate control** (gated by `rate_dirty`): `bitrate_kbps`, `vbv_mult_milli` (the VBV frame-time
81///    multiplier * 1000, held as an integer for atomics; `<= 0` selects the policy default), and
82///    `fps_bits` (the target fps as `f64` bits, so a fractional rate reaches the encoders exactly,
83///    re-read every frame for dynamic pacing and rate control), and `link_kbps`, the rate the path
84///    carries (`CaptureSettings::video_link_kbps`), also re-read every frame. These
85///    atomics always hold the CURRENT values, so a pipeline rebuild can carry live rates forward.
86/// 3. **Tunables** (gated by `tunables_dirty`): one `LiveTunables` struct behind `tunables`, set
87///    rarely, carrying the per-frame quality knobs for the encode thread.
88/// 4. **Capture geometry**: `capture_cursor` toggles the cursor overlay (read on every grab);
89///    `region_dirty` guards `region` = `(x, y, w, h)` (w/h `<= 0` extends to the root edge), applied
90///    by the capture thread with the same drain/recreate machinery as auto-adjust.
91pub struct Controls {
92    pub stop: AtomicBool,
93    /// Set by the capture thread as it returns — after it has joined its encode thread and
94    /// so dropped the NVENC/CUDA session. The atexit sweep waits on this (bounded) before
95    /// letting the interpreter finalize, because that hardware-session drop racing process
96    /// exit segfaults, exactly as the Wayland branch fences with a Barrier.
97    pub finished: AtomicBool,
98    pub force_idr: AtomicBool,
99    pub reference_reports: Mutex<Vec<crate::encoders::reference::ReferenceReport>>,
100    pub rate_dirty: AtomicBool,
101    pub bitrate_kbps: AtomicI32,
102    pub vbv_mult_milli: AtomicI32,
103    pub fps_bits: AtomicU64,
104    pub link_kbps: AtomicI32,
105    pub tunables_dirty: AtomicBool,
106    pub tunables: Mutex<Option<crate::LiveTunables>>,
107    pub capture_cursor: AtomicBool,
108    /// The codec the pipeline streams, as `Codec::id`, once one has been built; the
109    /// selection ladder may have demoted the requested one. `u32::MAX` until then.
110    pub codec: AtomicU32,
111    /// Whether the pipeline encodes planes the capture converts itself
112    /// (`X11Pipeline::takes_host_planes`), which a capture reading the shared framebuffer then
113    /// converts straight from the screen in place of copying it.
114    pub takes_planes: AtomicBool,
115    pub region_dirty: AtomicBool,
116    pub region: Mutex<(i32, i32, i32, i32)>,
117    /// What this capture streams and how, for `ScreenCapture::stream_info`.
118    pub report: Arc<crate::report::StreamReport>,
119}
120
121impl Controls {
122    /// Seed the controls from the initial settings so the first loop iteration reads the
123    /// configured bitrate / VBV / fps / region / cursor state rather than defaults.
124    pub fn new(s: &CaptureSettings) -> Self {
125        Self {
126            stop: AtomicBool::new(false),
127            finished: AtomicBool::new(false),
128            force_idr: AtomicBool::new(false),
129            reference_reports: Mutex::new(Vec::new()),
130            rate_dirty: AtomicBool::new(false),
131            bitrate_kbps: AtomicI32::new(s.video_bitrate_kbps),
132            vbv_mult_milli: AtomicI32::new((s.video_vbv_multiplier * 1000.0).round() as i32),
133            fps_bits: AtomicU64::new(s.target_fps.max(1.0).to_bits()),
134            link_kbps: AtomicI32::new(s.video_link_kbps),
135            tunables_dirty: AtomicBool::new(false),
136            tunables: Mutex::new(None),
137            capture_cursor: AtomicBool::new(s.capture_cursor),
138            codec: AtomicU32::new(u32::MAX),
139            takes_planes: AtomicBool::new(false),
140            region_dirty: AtomicBool::new(false),
141            region: Mutex::new((s.capture_x, s.capture_y, s.width, s.height)),
142            report: crate::report::StreamReport::new("x11"),
143        }
144    }
145}
146
147/// A shared-memory image surface: a POSIX shm segment mapped into both this process and the
148/// X server, into which `shm_get_image` writes one BGRA frame.
149///
150/// Both ends map the same physical pages, which is the whole point: a full-screen grab then costs no
151/// per-frame copy across the X socket, because the server's blit lands directly in memory the
152/// capture thread already reads. The surface owns the server-side segment id (`shmseg`), the local
153/// mapping (`addr` / `size`), and the geometry (`width` / `height` / `stride`) needed to interpret
154/// the bytes.
155struct ShmSurface {
156    shmseg: u32,
157    addr: *mut u8,
158    size: usize,
159    width: u16,
160    height: u16,
161    stride: usize,
162}
163
164impl ShmSurface {
165    /// Allocate a shm segment of `width*height*4` bytes, attach it both locally and to the X
166    /// server, and hand back a surface both ends can read.
167    ///
168    /// The segment is created with `shmget(IPC_PRIVATE)` and mapped locally via `shmat`, then
169    /// attached on the server (`shm_attach`). That attach is confirmed with a round-trip `check()`
170    /// BEFORE the segment is marked `IPC_RMID`: deleting only after both ends hold a reference means
171    /// the kernel frees the segment once this process and the server have both detached, never while
172    /// either still needs it. Every failure path unwinds the partial state (local detach and/or
173    /// `IPC_RMID`) so a failed allocation leaks neither memory nor a server attachment; a zero-sized
174    /// request is rejected up front.
175    fn create(conn: &RustConnection, width: u16, height: u16) -> Result<Self, String> {
176        let stride = width as usize * 4;
177        let size = stride * height as usize;
178        if size == 0 {
179            return Err("zero-sized capture surface".into());
180        }
181        unsafe {
182            let shmid = libc::shmget(libc::IPC_PRIVATE, size, libc::IPC_CREAT | 0o600);
183            if shmid < 0 {
184                return Err("shmget failed".into());
185            }
186            let addr = libc::shmat(shmid, std::ptr::null(), 0);
187            if addr == (-1isize) as *mut libc::c_void {
188                libc::shmctl(shmid, libc::IPC_RMID, std::ptr::null_mut());
189                return Err("shmat failed".into());
190            }
191            let shmseg = match conn.generate_id() {
192                Ok(id) => id,
193                Err(e) => {
194                    libc::shmdt(addr);
195                    libc::shmctl(shmid, libc::IPC_RMID, std::ptr::null_mut());
196                    return Err(format!("generate_id: {e}"));
197                }
198            };
199            let attach = conn
200                .shm_attach(shmseg, shmid as u32, false)
201                .map_err(|e| format!("shm_attach: {e}"))
202                .and_then(|c| c.check().map_err(|e| format!("shm_attach check: {e}")));
203            libc::shmctl(shmid, libc::IPC_RMID, std::ptr::null_mut());
204            if let Err(e) = attach {
205                libc::shmdt(addr);
206                return Err(e);
207            }
208            Ok(Self {
209                shmseg,
210                addr: addr as *mut u8,
211                size,
212                width,
213                height,
214                stride,
215            })
216        }
217    }
218
219    /// Borrow the mapped segment as a mutable byte slice for the capture blit and overlays.
220    fn as_mut_slice(&mut self) -> &mut [u8] {
221        unsafe { std::slice::from_raw_parts_mut(self.addr, self.size) }
222    }
223
224    /// Borrow the mapped segment read-only, for the stability comparison.
225    fn as_slice(&self) -> &[u8] {
226        unsafe { std::slice::from_raw_parts(self.addr, self.size) }
227    }
228
229    /// Detach the segment from the X server and then locally, clearing the mapped pointer.
230    ///
231    /// This is a manual method rather than a `Drop` impl because releasing the server-side
232    /// attachment is an X protocol request that needs the live connection, which a `Drop` could not
233    /// reach; the owner of the connection must call it before the surface is dropped. The order —
234    /// server `shm_detach` first, then the local `shmdt` — completes the release of the segment that
235    /// was marked `IPC_RMID` at creation, so the kernel reclaims it once neither end holds it.
236    fn destroy(&mut self, conn: &RustConnection) {
237        let _ = conn.shm_detach(self.shmseg);
238        let _ = conn.flush();
239        unsafe {
240            libc::shmdt(self.addr as *mut libc::c_void);
241        }
242        self.addr = std::ptr::null_mut();
243    }
244}
245
246/// Reject a server whose root visual is not 32 bits per pixel.
247///
248/// Every `ShmSurface` is sized at `width * 4` bytes per row and the capture is read as packed
249/// BGRA, so a root of any other depth would be grabbed as garbage rather than failing. Checked
250/// on the initial connection and again on every reconnect, since a restarted server can come
251/// back with a different visual.
252fn require_32bpp(conn: &RustConnection, screen: usize) -> Result<(), String> {
253    let root_depth = conn.setup().roots[screen].root_depth;
254    let bpp = conn
255        .setup()
256        .pixmap_formats
257        .iter()
258        .find(|f| f.depth == root_depth)
259        .map(|f| f.bits_per_pixel)
260        .unwrap_or(32);
261    if bpp != 32 {
262        return Err(format!(
263            "unsupported root depth {root_depth} ({bpp} bpp); only 32-bpp BGRA is supported"
264        ));
265    }
266    Ok(())
267}
268
269/// Clamp a capture offset to the live root so a grab always has at least one root pixel in
270/// range: a past-end offset fails every `shm_get_image` with BadMatch and would burn out the
271/// failure budget with no chance of recovery. The upper clamp also keeps protocol-range roots
272/// (INT16 coordinates) from truncating a wider value through the `as i16` cast.
273fn clamp_offset(x: i32, root: u16) -> i16 {
274    let upper = i32::from(root)
275        .saturating_sub(1)
276        .min(i32::from(i16::MAX) - 1);
277    x.max(0).min(upper) as i16
278}
279
280/// Resolve the capture dimensions `shm_get_image` will read, bounded so the region can never
281/// run past the live root from the (clamped) capture offset: with auto-adjust (or unset
282/// width/height `<= 0`) the capture tracks the full root minus the offset, otherwise the
283/// requested size is clamped to what is available from it. Saturating subtraction plus a final
284/// `u16` clamp (minimum 2) keep pathological settings from overflowing or collapsing to an
285/// unusable surface. Video codecs require even dimensions, so both are then rounded down to
286/// a multiple of two.
287fn resolve_dims(root_w: u16, root_h: u16, s: &CaptureSettings) -> (u16, u16) {
288    let cap_x = i32::from(clamp_offset(s.capture_x, root_w));
289    let cap_y = i32::from(clamp_offset(s.capture_y, root_h));
290    let avail_w = i32::from(root_w).saturating_sub(cap_x).max(2);
291    let avail_h = i32::from(root_h).saturating_sub(cap_y).max(2);
292    let mut w = if s.auto_adjust_screen_capture_size || s.width <= 0 {
293        avail_w
294    } else {
295        s.width.min(avail_w)
296    };
297    let mut h = if s.auto_adjust_screen_capture_size || s.height <= 0 {
298        avail_h
299    } else {
300        s.height.min(avail_h)
301    };
302    w = w.clamp(2, i32::from(u16::MAX));
303    h = h.clamp(2, i32::from(u16::MAX));
304    if s.codec.is_video() {
305        w &= !1;
306        h &= !1;
307    }
308    (w as u16, h as u16)
309}
310
311/// Rebuild the X channel + capture surfaces after a connection-level failure (Xorg restart,
312/// VT switch), refreshing the offset/dims state against the live root. The pool is drained
313/// first so the encode thread can never be reading a surface as it is destroyed, and its
314/// generation is bumped so the encoder rebuilds against the new layout. Every fallible step
315/// completes before the swap, and the old channel/surfaces are destroyed best-effort after
316/// it (their server is likely gone; the local detaches still run, and the kernel frees
317/// each old segment once nothing maps it).
318#[allow(clippy::too_many_arguments)]
319fn try_rebuild_channel(
320    conn: &mut RustConnection,
321    root: &mut u32,
322    rsettings: &CaptureSettings,
323    cap_x: &mut i16,
324    cap_y: &mut i16,
325    cap_w: &mut u16,
326    cap_h: &mut u16,
327    root_w: &mut u16,
328    root_h: &mut u16,
329    pool_n: usize,
330    pool: &FramePool,
331    surfaces: &mut Vec<ShmSurface>,
332    stop: &AtomicBool,
333) -> Result<(), String> {
334    let (new_conn, new_screen) =
335        x11rb::connect(None).map_err(|e| format!("X11 reconnect failed: {e}"))?;
336    require_32bpp(&new_conn, new_screen)?;
337    new_conn
338        .shm_query_version()
339        .map_err(|e| format!("shm_query_version: {e}"))?
340        .reply()
341        .map_err(|e| format!("XShm unavailable on reconnect: {e}"))?;
342    new_conn
343        .xfixes_query_version(5, 0)
344        .map_err(|e| format!("xfixes_query_version: {e}"))?
345        .reply()
346        .map_err(|e| format!("XFixes unavailable on reconnect: {e}"))?;
347    let new_root = new_conn.setup().roots[new_screen].root;
348    let geo = new_conn
349        .get_geometry(new_root)
350        .map_err(|e| format!("get_geometry: {e}"))?
351        .reply()
352        .map_err(|e| format!("get_geometry reply on reconnect: {e}"))?;
353    if !pool.drain_for_resize(pool_n, stop) {
354        return Err("pool drain interrupted by stop during reconnect".to_string());
355    }
356    let (fw, fh) = resolve_dims(geo.width, geo.height, rsettings);
357    let mut fresh: Vec<ShmSurface> = Vec::with_capacity(pool_n);
358    for _ in 0..pool_n {
359        match ShmSurface::create(&new_conn, fw, fh) {
360            Ok(s) => fresh.push(s),
361            Err(e) => {
362                for s in fresh.iter_mut() {
363                    s.destroy(&new_conn);
364                }
365                return Err(e);
366            }
367        }
368    }
369    for s in surfaces.iter_mut() {
370        s.destroy(conn);
371    }
372    *conn = new_conn;
373    *root = new_root;
374    *cap_x = clamp_offset(rsettings.capture_x, geo.width);
375    *cap_y = clamp_offset(rsettings.capture_y, geo.height);
376    *cap_w = fw;
377    *cap_h = fh;
378    *root_w = geo.width;
379    *root_h = geo.height;
380    *surfaces = fresh;
381    pool.bump_generation();
382    Ok(())
383}
384
385/// The X server's report that the root window changed, which lets a capture publish a change as
386/// it lands instead of on the next tick. `None` where the server carries no DAMAGE extension,
387/// leaving the frame timer to pace the capture by itself.
388///
389/// Reporting is one notification per non-empty region: the server stays quiet until the region
390/// is subtracted, which is what [`Self::clear`] does to re-arm the next report.
391struct RootDamage {
392    id: Damage,
393    /// The XFixes region [`Self::take`] moves a report's damage into, to read where the root
394    /// changed; None where the server's XFixes has no regions.
395    parts: Option<Region>,
396    /// A report read off the connection and not yet acted on. Latched here because reading the
397    /// connection is what notices one, and that happens well before the frame it wakes.
398    reported: Cell<bool>,
399}
400
401impl RootDamage {
402    fn create(conn: &RustConnection, root: Window) -> Option<Self> {
403        conn.damage_query_version(1, 1).ok()?.reply().ok()?;
404        let id = conn.generate_id().ok()?;
405        conn.damage_create(id, root, ReportLevel::NON_EMPTY)
406            .ok()?
407            .check()
408            .ok()?;
409        let parts = (|| {
410            if conn
411                .xfixes_query_version(2, 0)
412                .ok()?
413                .reply()
414                .ok()?
415                .major_version
416                < 2
417            {
418                return None;
419            }
420            let parts = conn.generate_id().ok()?;
421            conn.xfixes_create_region(parts, &[]).ok()?.check().ok()?;
422            Some(parts)
423        })();
424        Some(Self {
425            id,
426            parts,
427            reported: Cell::new(false),
428        })
429    }
430
431    /// Whether the server has reported a change since the last [`Self::clear`], taking in
432    /// whatever else the connection carries along the way.
433    fn reported(&self, conn: &RustConnection) -> bool {
434        while let Ok(Some(event)) = conn.poll_for_event() {
435            if matches!(event, Event::DamageNotify(_)) {
436                self.reported.set(true);
437            }
438        }
439        self.reported.get()
440    }
441
442    /// Empty the region behind a report that has been acted on, so the next change is reported
443    /// in turn; a tick nothing reported leaves the server alone. Called before a grab, which
444    /// leaves a change racing the grab to wake the next frame instead of being cleared along
445    /// with what the grab captured.
446    fn clear(&self, conn: &RustConnection) {
447        if !self.reported(conn) {
448            return;
449        }
450        self.reported.set(false);
451        let _ = conn.damage_subtract(self.id, x11rb::NONE, x11rb::NONE);
452        let _ = conn.flush();
453    }
454
455    /// [`Self::clear`], saying where the root changed: the damaged rectangles, empty where
456    /// nothing was reported, None where a change was reported but the server cannot say where.
457    fn take(&self, conn: &RustConnection) -> Option<Vec<Rectangle>> {
458        if !self.reported(conn) {
459            return Some(Vec::new());
460        }
461        let Some(parts) = self.parts else {
462            self.clear(conn);
463            return None;
464        };
465        self.reported.set(false);
466        conn.damage_subtract(self.id, x11rb::NONE, parts).ok()?;
467        Some(
468            conn.xfixes_fetch_region(parts)
469                .ok()?
470                .reply()
471                .ok()?
472                .rectangles,
473        )
474    }
475}
476
477/// [`RootDamage::take`] for a tick `trigger` woke: whether the root changed since the last
478/// frame, and where. A report the take finds counts whatever woke the tick: one landing after
479/// the wait gave up is spent here all the same, and called clean, its change would be cleared
480/// with nothing left to report it (a capture that sends only changes would never send it).
481fn take_tick(
482    damage: &RootDamage,
483    conn: &RustConnection,
484    trigger: TickTrigger,
485) -> (bool, Option<Vec<Rectangle>>) {
486    let rects = damage.take(conn);
487    let dirty =
488        trigger == TickTrigger::Damage || rects.as_ref().is_none_or(|rects| !rects.is_empty());
489    (dirty, rects)
490}
491
492/// The share of the captured area at (`x`, `y`), `width` x `height`, that the root's damaged
493/// `rects` cover.
494fn captured_damage(
495    rects: &[Rectangle],
496    x: i32,
497    y: i32,
498    width: i32,
499    height: i32,
500) -> crate::pipeline::Damage {
501    let local: Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> = rects
502        .iter()
503        .map(|r| {
504            smithay::utils::Rectangle::new(
505                (i32::from(r.x) - x, i32::from(r.y) - y).into(),
506                (i32::from(r.width), i32::from(r.height)).into(),
507            )
508        })
509        .collect();
510    crate::pipeline::Damage::of_rects(&local, width, height)
511}
512
513/// The root's damaged `rects` in the coordinates of the captured area at (`x`, `y`), `width` x
514/// `height`, clipped to it.
515pub(crate) fn frame_rects(
516    rects: &[Rectangle],
517    x: i16,
518    y: i16,
519    width: u16,
520    height: u16,
521) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
522    rects
523        .iter()
524        .filter_map(|r| {
525            let left = (i32::from(r.x) - i32::from(x)).max(0);
526            let top = (i32::from(r.y) - i32::from(y)).max(0);
527            let right = (i32::from(r.x) + i32::from(r.width) - i32::from(x)).min(width.into());
528            let bottom = (i32::from(r.y) + i32::from(r.height) - i32::from(y)).min(height.into());
529            (right > left && bottom > top).then(|| {
530                smithay::utils::Rectangle::new(
531                    (left, top).into(),
532                    (right - left, bottom - top).into(),
533                )
534            })
535        })
536        .collect()
537}
538
539/// Wait for the connection to carry something, for at most `timeout`.
540fn readable(conn: &RustConnection, timeout: Duration) -> bool {
541    let mut poll = libc::pollfd {
542        fd: conn.stream().as_raw_fd(),
543        events: libc::POLLIN,
544        revents: 0,
545    };
546    let deadline = libc::timespec {
547        tv_sec: timeout.as_secs() as _,
548        tv_nsec: libc::c_long::from(timeout.subsec_nanos()),
549    };
550    unsafe { libc::ppoll(&mut poll, 1, &deadline, std::ptr::null()) > 0 }
551}
552
553/// Block until the capture is due a frame and say what asked for it: a change the server
554/// reported, brought forward by as much as the pacing budget allows, or the frame timer.
555///
556/// Without DAMAGE the wait is the timer's alone, which is the cadence a capture keeps when
557/// nothing else can say the screen moved.
558fn wait_for_frame(
559    conn: &RustConnection,
560    damage: Option<&RootDamage>,
561    pace: &FramePace,
562    period: Duration,
563) -> TickTrigger {
564    let now = Instant::now();
565    let due = pace.next_due(period, now);
566    let sleep_until = |t: Instant| {
567        if let Some(wait) = t.checked_duration_since(Instant::now()) {
568            thread::sleep(wait);
569        }
570    };
571    let Some(damage) = damage else {
572        sleep_until(due);
573        return TickTrigger::Timer;
574    };
575    // Nothing may be published before the budget allows it, so the report of a change that
576    // lands earlier is left queued until then rather than read and cleared.
577    sleep_until(pace.pull_at(TickTrigger::Damage, period, now).min(due));
578    loop {
579        if damage.reported(conn) {
580            return TickTrigger::Damage;
581        }
582        let Some(wait) = due.checked_duration_since(Instant::now()) else {
583            return TickTrigger::Timer;
584        };
585        if !readable(conn, wait) {
586            return TickTrigger::Timer;
587        }
588    }
589}
590
591/// [`wait_for_frame`] on a shared framebuffer's watch (`framebuffer`): a drawing operation the
592/// server signals on it ends the wait as a reported change would, with no round trip, and so
593/// does a resize retiring it.
594fn wait_for_framebuffer(fb: &SharedFramebuffer, pace: &FramePace, period: Duration) -> TickTrigger {
595    let now = Instant::now();
596    let due = pace.next_due(period, now);
597    if let Some(wait) = pace
598        .pull_at(TickTrigger::Damage, period, now)
599        .min(due)
600        .checked_duration_since(Instant::now())
601    {
602        thread::sleep(wait);
603    }
604    match due.checked_duration_since(Instant::now()) {
605        Some(wait) if fb.wait(wait) => TickTrigger::Damage,
606        None if fb.wait(Duration::ZERO) => TickTrigger::Damage,
607        _ => TickTrigger::Timer,
608    }
609}
610
611/// The shared framebuffer of `root`'s screen (`framebuffer`), where the server offers one and
612/// its pixels over the capture `region` read as an XShm grab of it into `surface` does, or
613/// `None`, saying why where the server offers one that is not used. The comparison is tried a
614/// few times, since the screen may change between the grab and the read.
615fn open_framebuffer(
616    conn: &RustConnection,
617    root: Window,
618    surface: &mut ShmSurface,
619    region: (i16, i16, u16, u16),
620    surfaces: usize,
621) -> Option<FramebufferGrab> {
622    let fb = match SharedFramebuffer::open(conn, root) {
623        Opened::Watch(fb) => fb,
624        Opened::Absent => return None,
625        Opened::Declined(reason) => {
626            println!("[X11] Shared framebuffer not used: {reason}");
627            crate::report::capture_declined("Framebuffer", &reason);
628            return None;
629        }
630    };
631    let (x, y, w, h) = region;
632    let mut grab = FramebufferGrab::new(fb, surfaces);
633    let mut read = vec![0u8; surface.size];
634    let matches = (0..3).any(|_| {
635        grab.invalidate();
636        grab_frame(conn, root, surface, x, y, w, h).is_ok()
637            && grab.read(
638                0,
639                &mut read,
640                surface.stride,
641                (x as u32, y as u32, u32::from(w), u32::from(h)),
642            )
643            && read == surface.as_slice()
644    });
645    if !matches {
646        let reason = "its pixels differ from what GetImage returns";
647        println!("[X11] Shared framebuffer not used: {reason}");
648        crate::report::capture_declined("Framebuffer", reason);
649        grab.fb.close(conn);
650        return None;
651    }
652    grab.invalidate();
653    println!(
654        "[X11] Reading the screen in place from the X server's shared framebuffer ({}x{})",
655        grab.fb.width, grab.fb.height
656    );
657    crate::report::capture("Framebuffer", false);
658    Some(grab)
659}
660
661/// One `shm_get_image` request is atomic — the server never interleaves another client
662/// inside it — so the grab is a coherent snapshot of whatever the screen held at that
663/// instant. Every grabbed frame is published; whether any of it changed is decided
664/// downstream by the encoder's own per-stripe damage detection.
665fn grab_frame(
666    conn: &RustConnection,
667    root: u32,
668    surface: &ShmSurface,
669    cap_x: i16,
670    cap_y: i16,
671    cap_w: u16,
672    cap_h: u16,
673) -> Result<(), String> {
674    conn.shm_get_image(
675        root,
676        cap_x,
677        cap_y,
678        cap_w,
679        cap_h,
680        !0u32,
681        ImageFormat::Z_PIXMAP.into(),
682        surface.shmseg,
683        0,
684    )
685    .map_err(|e| format!("shm_get_image: {e}"))?
686    .reply()
687    .map_err(|e| format!("shm_get_image reply: {e}"))?;
688    Ok(())
689}
690
691/// Frame-space top-left of the cursor image, given the XFixes hotspot position.
692///
693/// XFixes reports the cursor position at its HOTSPOT; X draws the image with its top-left at
694/// `(pos - hot)`, and the capture origin `(cap_x, cap_y)` is subtracted to move it into frame space.
695/// The result may go negative near the frame edges, which `overlay_cursor` clips per pixel to match
696/// the server's own edge clipping.
697#[inline]
698pub(crate) fn cursor_image_origin(
699    x: i16,
700    y: i16,
701    xhot: u16,
702    yhot: u16,
703    cap_x: i32,
704    cap_y: i32,
705) -> (i32, i32) {
706    (
707        i32::from(x) - i32::from(xhot) - cap_x,
708        i32::from(y) - i32::from(yhot) - cap_y,
709    )
710}
711
712/// Composite the XFixes cursor (ARGB `u32` per pixel, premultiplied by the XFixes
713/// format definition) onto the BGRA frame with its top-left at `(img_x, img_y)`,
714/// blending each pixel through `blend_pixel_premultiplied` with per-pixel bounds
715/// clipping so an image straddling a frame edge writes only its in-frame portion.
716#[allow(clippy::too_many_arguments)]
717pub(crate) fn overlay_cursor(
718    frame: &mut [u8],
719    stride: usize,
720    frame_w: i32,
721    frame_h: i32,
722    cur_w: i32,
723    cur_h: i32,
724    pixels: &[u32],
725    img_x: i32,
726    img_y: i32,
727) {
728    for y in 0..cur_h {
729        let ty = img_y + y;
730        if ty < 0 || ty >= frame_h {
731            continue;
732        }
733        for x in 0..cur_w {
734            let tx = img_x + x;
735            if tx < 0 || tx >= frame_w {
736                continue;
737            }
738            let px = pixels[(y * cur_w + x) as usize];
739            let a = ((px >> 24) & 0xFF) as u8;
740            let r = ((px >> 16) & 0xFF) as u8;
741            let g = ((px >> 8) & 0xFF) as u8;
742            let b = (px & 0xFF) as u8;
743            let off = ty as usize * stride + tx as usize * 4;
744            blend_pixel_premultiplied(&mut frame[off..off + 4], r, g, b, a);
745        }
746    }
747}
748
749/// A captured raw BGRA frame held in a pooled shm surface, ready to encode.
750///
751/// It carries the surface pointer plus geometry so the encode thread reads the pixels directly (no
752/// copy) and can rebuild its pipeline when the capture size changed (auto-adjust). It also carries
753/// the pool's surface `generation`, so a rebuild is still triggered when the surfaces were recreated
754/// at the SAME size (a resize flap) and the new segments happen to reuse the old virtual addresses.
755struct RawFrame {
756    idx: usize,
757    ptr: *mut u8,
758    len: usize,
759    width: u16,
760    height: u16,
761    stride: usize,
762    generation: u64,
763    /// CLOCK_MONOTONIC nanoseconds at which the pixels were in hand.
764    captured_ns: i64,
765    /// Where the frame changed since the one before, in its own coordinates, as the server
766    /// reported it and the overlays moved; empty where the server cannot say.
767    changed: Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>>,
768    /// Whether `changed` holds every change: the shared framebuffer's watch, which the server
769    /// fills as it draws (`X11Pipeline::process_reported`), where XDamage can miss directly
770    /// rendered GL.
771    complete: bool,
772    /// The frame's picture where the capture converted it from the shared framebuffer instead of
773    /// copying its pixels (the `Nv12Planes` of the surface's index), which `ptr` then does not
774    /// hold.
775    planes: Option<*const Nv12Planes>,
776}
777/// `RawFrame` is `Send`: its raw pointer addresses a pooled shm surface that the pool
778/// guarantees is not reused until the encode thread recycles this frame, so the handle is safe to
779/// move across the capture -> encode thread boundary.
780unsafe impl Send for RawFrame {}
781
782/// Mutex-guarded interior of `FramePool`: the free-surface index list, the single
783/// capture -> encode handoff slot, and the surface the encoder still reads, under one lock so
784/// moving a surface between them — acquire, publish, take, recycle, settle — is always a single
785/// atomic step.
786struct PoolInner {
787    free: Vec<usize>,
788    slot: Option<RawFrame>,
789    /// The surface whose picture the encoder reads through its next encode, out of `free`
790    /// until a later encode stops reading it (`settle`).
791    held: Option<usize>,
792    /// Whether `drain_for_resize` asks the encode thread to stop reading `held` and return it.
793    release: bool,
794}
795
796/// Demand-driven capture -> encode handoff: a bounded single-slot channel over a fixed set of
797/// pooled shm surfaces.
798///
799/// The capture thread writes into a pooled surface and `publish`es it into the single `slot`; the
800/// encode thread `take`s it. Capture stays at most one frame ahead of encode because `acquire` and
801/// `publish` BLOCK (bounded) until the encoder frees a surface / drains the slot, throttling capture
802/// to the encode rate. Since X11 capture is pull-based (no backlog), throttling — rather than
803/// capturing-then-dropping — means a full-resolution shm round-trip is never spent on a frame that
804/// would be discarded, while the next capture still overlaps the current encode (the throughput win).
805/// The encoder therefore only ever sees a contiguous frame stream, keeping the H.264 reference chain
806/// valid.
807///
808/// An x264 session encoding a surface's picture in place reads it through the next encode as
809/// well, so the encode thread keeps that surface (`held`) until an encode no longer reads it: of
810/// the `POOL_N` surfaces one is then held, one encoded, and the third left for the capture to
811/// convert the next frame into while the encode runs, the capture still one frame ahead.
812///
813/// `generation` is bumped by the capture thread each time the backing shm surfaces are destroyed and
814/// recreated. Published frames carry it so the encode thread rebuilds its pipeline — dropping encoder
815/// state keyed to surface base pointers (e.g. NVENC's pinned-host registrations) — even when a resize
816/// flap lands back on the old dimensions and the recreated segments reuse the old virtual addresses.
817struct FramePool {
818    inner: Mutex<PoolInner>,
819    cv: Condvar,
820    stop: AtomicBool,
821    generation: AtomicU64,
822}
823
824impl FramePool {
825    /// Create a pool with `n` free surfaces, an empty handoff slot, and generation 0.
826    fn new(n: usize) -> Self {
827        Self {
828            inner: Mutex::new(PoolInner {
829                free: (0..n).collect(),
830                slot: None,
831                held: None,
832                release: false,
833            }),
834            cv: Condvar::new(),
835            stop: AtomicBool::new(false),
836            generation: AtomicU64::new(0),
837        }
838    }
839
840    /// Capture: record that the surfaces were recreated by advancing the generation. Only
841    /// called after `drain_for_resize` succeeded, so no frame from the previous generation is still
842    /// in flight.
843    fn bump_generation(&self) {
844        self.generation.fetch_add(1, Ordering::Relaxed);
845    }
846
847    /// Read the current surface generation, stamped onto each published frame.
848    fn generation(&self) -> u64 {
849        self.generation.load(Ordering::Relaxed)
850    }
851
852    /// Capture: claim a free surface to write the next frame into, or `None` on stop.
853    ///
854    /// Blocks (bounded, re-checking `stop` every 20ms) until a surface is free, which throttles
855    /// capture to the encode rate so a full-resolution capture is never spent on a frame that would
856    /// be dropped. The bounded re-check means a stop that races the wait cannot hang the thread.
857    fn acquire(&self, stop: &AtomicBool) -> Option<usize> {
858        let mut g = self.inner.lock().unwrap();
859        loop {
860            if let Some(idx) = g.free.pop() {
861                return Some(idx);
862            }
863            if stop.load(Ordering::Relaxed) {
864                return None;
865            }
866            let (gg, _) = self.cv.wait_timeout(g, Duration::from_millis(20)).unwrap();
867            g = gg;
868        }
869    }
870
871    /// Capture: publish the just-captured frame into the single slot, or `false` (frame
872    /// discarded) on stop.
873    ///
874    /// Blocks (bounded, re-checking `stop` every 20ms) until the encode thread has taken the previous
875    /// frame, so capture stays at most one frame ahead and never drops under normal flow.
876    fn publish(&self, frame: RawFrame, stop: &AtomicBool) -> bool {
877        let mut g = self.inner.lock().unwrap();
878        loop {
879            if g.slot.is_none() {
880                g.slot = Some(frame);
881                drop(g);
882                self.cv.notify_all();
883                return true;
884            }
885            if stop.load(Ordering::Relaxed) {
886                return false;
887            }
888            let (gg, _) = self.cv.wait_timeout(g, Duration::from_millis(20)).unwrap();
889            g = gg;
890        }
891    }
892
893    /// Encode: block until a frame is available (`Some`) or stop is signaled (`None`).
894    ///
895    /// The wait is bounded (re-checking `stop` every 20ms) as defense-in-depth against a lost wakeup,
896    /// so a stop that races the park can never leave the encode thread blocked forever. A drain
897    /// waiting for the held surface is answered here: `release` stops the encoder reading it,
898    /// with the lock dropped, and the surface returns to the free list.
899    fn take(&self, mut release: impl FnMut()) -> Option<RawFrame> {
900        let mut g = self.inner.lock().unwrap();
901        loop {
902            if let Some(f) = g.slot.take() {
903                return Some(f);
904            }
905            if std::mem::take(&mut g.release) {
906                drop(g);
907                release();
908                g = self.inner.lock().unwrap();
909                if let Some(idx) = g.held.take() {
910                    g.free.push(idx);
911                }
912                self.cv.notify_all();
913                continue;
914            }
915            if self.stop.load(Ordering::Acquire) {
916                return None;
917            }
918            let (gg, _) = self.cv.wait_timeout(g, Duration::from_millis(20)).unwrap();
919            g = gg;
920        }
921    }
922
923    /// Encode: return a surface to the free list after it has been encoded, waking any waiter
924    /// parked in `acquire` / `publish` / `drain_for_resize`.
925    fn recycle(&self, idx: usize) {
926        self.inner.lock().unwrap().free.push(idx);
927        self.cv.notify_all();
928    }
929
930    /// Encode: after a frame's encode, return the surfaces `back` to the free list and keep
931    /// `held`, the one whose picture the encoder reads through its next encode, out of it.
932    fn settle(&self, back: &[usize], held: Option<usize>) {
933        let mut g = self.inner.lock().unwrap();
934        g.free.extend_from_slice(back);
935        g.held = held;
936        drop(g);
937        self.cv.notify_all();
938    }
939
940    /// Capture: before recreating surfaces (auto-adjust / region resize), reclaim the pending
941    /// slot and wait until every surface is back in the free list, so no surface is destroyed while
942    /// the encode thread is still reading it.
943    ///
944    /// Reclaiming the slot returns any un-taken frame to the free list; the loop then waits until
945    /// `free.len() == n`, i.e. the encode thread has finished any in-flight frame and, asked
946    /// through `release`, returned the surface its encoder held. The wait is bounded
947    /// and also breaks on stop — pool shutdown OR the external `stop` — so a panicked or dead encode
948    /// thread that never recycles cannot wedge the capture thread here forever; a requested stop
949    /// unblocks it. Returns `true` if it fully drained (safe to recreate surfaces), or `false` if it
950    /// aborted on stop — in which case the caller tears down, which joins the encode thread before
951    /// destroying surfaces, so the resize-safety guarantee still holds.
952    fn drain_for_resize(&self, n: usize, stop: &AtomicBool) -> bool {
953        let mut g = self.inner.lock().unwrap();
954        if let Some(old) = g.slot.take() {
955            g.free.push(old.idx);
956        }
957        while g.free.len() < n {
958            if self.stop.load(Ordering::Acquire) || stop.load(Ordering::Relaxed) {
959                return false;
960            }
961            if g.held.is_some() && !g.release {
962                g.release = true;
963                self.cv.notify_all();
964            }
965            let (gg, _) = self.cv.wait_timeout(g, Duration::from_millis(20)).unwrap();
966            g = gg;
967        }
968        true
969    }
970
971    /// Signal every waiter to stop and wake them, ending the encode thread's `take` loop.
972    ///
973    /// The inner mutex is acquired BEFORE storing `stop` and notifying, which closes the lost-wakeup
974    /// window: with the lock held, `take` is either still before its `stop` check or already parked on
975    /// the condvar (and will receive the notify), never in the gap between the two. The notify is
976    /// issued after the guard is dropped so the woken thread does not immediately re-block on the lock
977    /// this call holds.
978    fn shutdown(&self) {
979        let g = self.inner.lock().unwrap();
980        self.stop.store(true, Ordering::Release);
981        drop(g);
982        self.cv.notify_all();
983    }
984}
985
986/// Encode thread body: consume captured frames from the pool, keep the [`pipeline::X11Pipeline`] in
987/// step with the capture size and cross-thread controls, encode, recycle, and deliver.
988///
989/// The loop runs until `FramePool::take` returns `None` (stop). For each frame:
990///
991/// 1. **(Re)build the pipeline** when it is missing, the frame size changed, or the surface
992///    generation changed. A generation change alone forces the rebuild path because recreated shm
993///    segments often reuse the old virtual base addresses, so encoder state keyed to base pointers
994///    (NVENC's pinned-host cache) must be dropped even at identical dimensions. An in-place
995///    [`X11Pipeline::reshape`] is preferred — NVENC reconfigures its live session and the CPU
996///    path just re-derives its region state — so a resize never stalls on a full encoder
997///    re-init; encoders that cannot follow in place (VAAPI) report `false` and the pipeline
998///    is rebuilt. On a rebuild the old pipeline (with its GPU session/surfaces) is dropped BEFORE the
999///    new one is built, so an auto-adjust resize never holds two full encoder allocations at once
1000///    (transient 2x GPU memory). The rebuilt settings pull the CURRENT bitrate / VBV / fps from the
1001///    controls atomics, carrying live rate changes forward instead of reverting to the capture-start
1002///    values.
1003/// 2. **Apply cross-thread controls** here, on the thread that owns the pipeline: a pending
1004///    `force_idr` requests a keyframe; a `rate_dirty` swap (Acquire, pairing with the setters' Release
1005///    stores so the bitrate / VBV / fps payload is never seen half-applied) pushes a live rate change;
1006///    a `tunables_dirty` swap applies per-frame tunables into both the local settings copy and the
1007///    pipeline.
1008/// 3. **Encode and hand off**: read the pooled surface directly through its raw pointer — sound
1009///    because the pool guarantees the surface is not reused until it is recycled — run
1010///    [`X11Pipeline::process`], recycle the surface BEFORE delivering so a slow consumer never holds a
1011///    capture surface, then deliver any encoded stripes through `on_frame`.
1012///
1013/// The optional Unix-socket recording sink (parity with the Wayland path) is bound ONCE here and
1014/// owned outside the pipeline, so pipeline rebuilds on resize keep the socket listener and any
1015/// attached recorders alive. It can only carry a full-frame video stream, so JPEG output is
1016/// warned about up front.
1017fn encode_loop<F>(
1018    pool: &FramePool,
1019    controls: &Controls,
1020    settings: &CaptureSettings,
1021    on_frame: &mut F,
1022) where
1023    F: FnMut(Vec<EncodedStripe>),
1024{
1025    let _report = crate::report::enter(&controls.report);
1026    let mut psettings = settings.clone();
1027    let recording_sink = RecordingSink::try_bind(&settings.recording_socket, settings.target_fps);
1028    if recording_sink.is_some() && !settings.codec.is_video() {
1029        eprintln!(
1030            "[recording_sink] recording_socket set but the codec is JPEG; no recordable video stream."
1031        );
1032    }
1033    let mut pipeline: Option<X11Pipeline> = None;
1034    let (mut pw, mut ph) = (0i32, 0i32);
1035    let mut pgen = 0u64;
1036    let mut last_log_time = Instant::now();
1037    let mut frame_count: u64 = 0;
1038    let mut stripe_count: u64 = 0;
1039
1040    // The surface whose picture the session reads through its next encode, and that picture.
1041    let mut held: Option<(usize, *const u8)> = None;
1042
1043    while let Some(frame) = pool.take(|| {
1044        if let Some(pl) = pipeline.as_mut() {
1045            pl.release_input();
1046        }
1047        held = None;
1048    }) {
1049        let (fw, fh) = (i32::from(frame.width), i32::from(frame.height));
1050        if pipeline.is_none() || pw != fw || ph != fh || pgen != frame.generation {
1051            let size_changed = pw != fw || ph != fh;
1052            psettings.width = fw;
1053            psettings.height = fh;
1054            psettings.video_bitrate_kbps = controls.bitrate_kbps.load(Ordering::Relaxed);
1055            psettings.video_vbv_multiplier =
1056                f64::from(controls.vbv_mult_milli.load(Ordering::Relaxed)) / 1000.0;
1057            psettings.target_fps =
1058                f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
1059            let reshaped = pipeline
1060                .as_mut()
1061                .is_some_and(|pl| pl.reshape(&psettings, size_changed));
1062            if !reshaped {
1063                drop(pipeline.take());
1064                pipeline = Some(X11Pipeline::new(psettings.clone()));
1065                if let Some(pl) = &pipeline {
1066                    let mut log_msg = format!(
1067                        "[X11] Stream settings active -> Res: {}x{} | FPS: {:.1} | Encoder: {}",
1068                        psettings.width,
1069                        psettings.height,
1070                        psettings.target_fps,
1071                        pl.encoder_name()
1072                    );
1073                    let fixed = pl.fixed_rate_control();
1074                    let paint_over =
1075                        crate::encoders::paint_over_desc(&psettings, fixed, pl.holds_quantizer());
1076                    if !pl.codec().is_video() {
1077                        log_msg.push_str(&format!(
1078                            " | Mode: JPEG | Quality: {}",
1079                            psettings.jpeg_quality
1080                        ));
1081                        if let Some(paint_over) = paint_over {
1082                            log_msg.push_str(&format!(" | {paint_over}"));
1083                        }
1084                    } else {
1085                        log_msg.push_str(&format!(
1086                            " | Mode: {} | {}",
1087                            pl.codec().display(),
1088                            crate::encoders::rate_desc(&psettings, fixed, pl.capped())
1089                        ));
1090                        if let Some(paint_over) = paint_over {
1091                            log_msg.push_str(&format!(" | {paint_over}"));
1092                        }
1093                        log_msg.push_str(&format!(" | Colorspace: {}", pl.colorspace_desc()));
1094                    }
1095                    log_msg.push_str(&format!(
1096                        " | Damage Thresh: {}f | Damage Dur: {}f",
1097                        psettings.damage_block_threshold, psettings.damage_block_duration
1098                    ));
1099                    println!("{}", log_msg);
1100                }
1101            }
1102            pw = fw;
1103            ph = fh;
1104            pgen = frame.generation;
1105            if let Some(pl) = &pipeline {
1106                controls
1107                    .takes_planes
1108                    .store(pl.takes_host_planes(), Ordering::Relaxed);
1109            }
1110        }
1111        let pl = pipeline.as_mut().unwrap();
1112
1113        // A recorder connecting to the socket sink needs a fresh decode entry point; it is
1114        // folded into the same standard request-IDR path as a client-driven force_idr.
1115        let sink_idr = recording_sink
1116            .as_ref()
1117            .map(|s| s.should_force_idr())
1118            .unwrap_or(false);
1119        for report in std::mem::take(&mut *controls.reference_reports.lock().unwrap()) {
1120            pl.take_report(report);
1121        }
1122        if controls.force_idr.swap(false, Ordering::Relaxed) || sink_idr {
1123            pl.request_idr();
1124        }
1125        if controls.rate_dirty.swap(false, Ordering::Acquire) {
1126            let b = controls.bitrate_kbps.load(Ordering::Relaxed);
1127            let v = f64::from(controls.vbv_mult_milli.load(Ordering::Relaxed)) / 1000.0;
1128            let fps = f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
1129            pl.update_rate(b, v, fps);
1130        }
1131        if controls.tunables_dirty.swap(false, Ordering::Acquire)
1132            && let Some(t) = controls.tunables.lock().unwrap().take()
1133        {
1134            t.apply_to(&mut psettings);
1135            pl.update_tunables(&t);
1136        }
1137
1138        let buf = unsafe { std::slice::from_raw_parts(frame.ptr, frame.len) };
1139        let encode_start_ns = crate::wayland::host::now_ns();
1140        pl.set_link_rate(controls.link_kbps.load(Ordering::Relaxed));
1141        // SAFETY: the planes belong to the surface this frame holds, which the capture does not
1142        // touch again until `settle` below returns it, or a later one once the session stops
1143        // reading its picture.
1144        let planes = frame
1145            .planes
1146            .map(|p| unsafe { &*p })
1147            .filter(|_| pl.takes_host_planes());
1148        let mut stripes = if let Some(p) = planes {
1149            pl.process_reported_planes(buf, frame.stride, p, &frame.changed)
1150        } else if frame.complete {
1151            pl.process_reported(buf, frame.stride, &frame.changed)
1152        } else {
1153            pl.process_damaged(buf, frame.stride, &frame.changed)
1154        };
1155        FrameTiming::stamp(&mut stripes, frame.captured_ns, encode_start_ns);
1156        controls.codec.store(pl.codec().id(), Ordering::Relaxed);
1157        let reading = pl.held_input();
1158        let keep = match reading {
1159            Some(id) if planes.map(Nv12Planes::id) == Some(id) => Some((frame.idx, id)),
1160            Some(id) if held.is_some_and(|(_, h)| h == id) => held,
1161            _ => None,
1162        };
1163        let mut back = Vec::with_capacity(2);
1164        for idx in std::iter::once(frame.idx).chain(held.map(|(idx, _)| idx)) {
1165            if keep.is_none_or(|(k, _)| k != idx) && !back.contains(&idx) {
1166                back.push(idx);
1167            }
1168        }
1169        pool.settle(&back, keep.map(|(idx, _)| idx));
1170        held = keep;
1171        if !stripes.is_empty() {
1172            frame_count += 1;
1173            stripe_count += stripes.len() as u64;
1174            // IDR arming is consumed inside X11Pipeline::process; this tap only fans out.
1175            if let Some(ref socket) = recording_sink {
1176                socket.write_frame(&stripes, psettings.width, psettings.height);
1177            }
1178            on_frame(stripes);
1179        }
1180
1181        let now = Instant::now();
1182        let elapsed = now.duration_since(last_log_time).as_secs_f64();
1183        if elapsed >= 1.0 {
1184            crate::log::debug!(
1185                "[X11] Res: {}x{} Encoder: {} EncFPS: {:.2} EncStripes/s: {:.2}",
1186                psettings.width,
1187                psettings.height,
1188                pl.encoder_name(),
1189                frame_count as f64 / elapsed,
1190                stripe_count as f64 / elapsed
1191            );
1192            frame_count = 0;
1193            stripe_count = 0;
1194            last_log_time = now;
1195        }
1196    }
1197}
1198
1199/// The rate the X server's fake vblank runs at while this process captures it: the fastest live
1200/// capture's frame rate, published on the root window as `_FAKE_SCREEN_MILLIHZ` (CARDINAL,
1201/// millihertz, 1000 to 1000000) and, rounded to whole frames for servers that read only that,
1202/// `_FAKE_SCREEN_FPS` (1 to 1000), both deleted when the last capture ends. The Selkies Xvfb paces
1203/// the clients that wait on Present by it, keeping the rate it started at as a floor unless the
1204/// published one is at most 0.1% slower (59.94 under 60), so a vsynced application presents as
1205/// fast as it is streamed; any other server holds it as inert data.
1206struct VblankRate {
1207    captures: Vec<Arc<Controls>>,
1208    /// Millihertz last published, 0 for none.
1209    published: u32,
1210    /// The connection, the root window, and the millihertz and frames-per-second atoms.
1211    conn: Option<(RustConnection, Window, Atom, Atom)>,
1212}
1213
1214static VBLANK_RATE: Mutex<VblankRate> = Mutex::new(VblankRate {
1215    captures: Vec::new(),
1216    published: 0,
1217    conn: None,
1218});
1219
1220impl VblankRate {
1221    fn publish(&mut self) {
1222        let want = self
1223            .captures
1224            .iter()
1225            .map(|c| {
1226                (f64::from_bits(c.fps_bits.load(Ordering::Relaxed)) * 1000.0)
1227                    .round()
1228                    .clamp(1000.0, 1_000_000.0) as u32
1229            })
1230            .max()
1231            .unwrap_or(0);
1232        if want == self.published {
1233            return;
1234        }
1235        if self.conn.is_none() {
1236            self.conn = x11rb::connect(None).ok().and_then(|(conn, screen)| {
1237                let root = conn.setup().roots[screen].root;
1238                let atom =
1239                    |name: &[u8]| Some(conn.intern_atom(false, name).ok()?.reply().ok()?.atom);
1240                let millihz = atom(b"_FAKE_SCREEN_MILLIHZ")?;
1241                let fps = atom(b"_FAKE_SCREEN_FPS")?;
1242                Some((conn, root, millihz, fps))
1243            });
1244        }
1245        let sent = self
1246            .conn
1247            .as_ref()
1248            .is_some_and(|(conn, root, millihz, fps)| {
1249                let queued = if want > 0 {
1250                    [(*millihz, want), (*fps, (want + 500) / 1000)]
1251                        .iter()
1252                        .all(|&(atom, value)| {
1253                            conn.change_property32(
1254                                PropMode::REPLACE,
1255                                *root,
1256                                atom,
1257                                AtomEnum::CARDINAL,
1258                                &[value],
1259                            )
1260                            .is_ok()
1261                        })
1262                } else {
1263                    [*millihz, *fps]
1264                        .iter()
1265                        .all(|&atom| conn.delete_property(*root, atom).is_ok())
1266                };
1267                // A round trip, not a flush: a server may drop what is still queued when a client
1268                // closes, and the connection closes right after the last capture's delete.
1269                queued
1270                    && conn
1271                        .get_input_focus()
1272                        .ok()
1273                        .and_then(|c| c.reply().ok())
1274                        .is_some()
1275            });
1276        self.published = if sent { want } else { 0 };
1277        if !sent || want == 0 {
1278            self.conn = None;
1279        }
1280    }
1281}
1282
1283/// A running capture's place in [`VblankRate`], given up when the capture ends.
1284struct VblankClaim(Arc<Controls>);
1285
1286impl VblankClaim {
1287    fn new(controls: &Arc<Controls>) -> Self {
1288        let mut rate = VBLANK_RATE.lock().unwrap();
1289        rate.captures.push(controls.clone());
1290        rate.publish();
1291        Self(controls.clone())
1292    }
1293}
1294
1295impl Drop for VblankClaim {
1296    fn drop(&mut self) {
1297        let mut rate = VBLANK_RATE.lock().unwrap();
1298        rate.captures.retain(|c| !Arc::ptr_eq(c, &self.0));
1299        rate.publish();
1300    }
1301}
1302
1303/// Republish [`VblankRate`] after a running capture's frame rate changed.
1304pub fn follow_frame_rate() {
1305    VBLANK_RATE.lock().unwrap().publish();
1306}
1307
1308/// Capture an X11 display until `stop` is set, on the best path this session can run.
1309///
1310/// [`dri3::run_capture`] is tried first: on a server whose screen lives on the GPU, the server
1311/// blits the root into dmabufs this process allocated and the hardware session reads them in
1312/// place. On an NVIDIA X server it reaches the encoder sooner than NvFBC and at a fraction of the
1313/// CPU. [`nvfbc::run_capture`] is next, for an NVIDIA server DRI3 cannot serve (a driver without
1314/// GBM): the driver composites the screen into video memory and NVENC encodes it in place. Each
1315/// reports that it cannot serve the session — a codec its engine has no support for, software
1316/// encoding, a watermark NvFBC would have to blend into host pixels, a server or device that does
1317/// not qualify — and the capture then runs on the general XShm path below, which every X server
1318/// supports. Whichever path runs, the
1319/// capture's frame rate paces the server's fake vblank for as long as it does ([`VblankRate`]).
1320///
1321/// Blocking; intended to run on a dedicated thread.
1322pub fn run_capture<F>(
1323    settings: CaptureSettings,
1324    controls: Arc<Controls>,
1325    encode_tid_tx: Sender<thread::ThreadId>,
1326    mut on_frame: F,
1327) -> Result<(), String>
1328where
1329    F: FnMut(Vec<EncodedStripe>) + Send + 'static,
1330{
1331    let _report = crate::report::enter(&controls.report);
1332    let _vblank = VblankClaim::new(&controls);
1333    if let Some(result) = dri3::run_capture(
1334        settings.clone(),
1335        controls.clone(),
1336        encode_tid_tx.clone(),
1337        &mut on_frame,
1338    ) {
1339        return result;
1340    }
1341    if let Some(result) = nvfbc::run_capture(
1342        settings.clone(),
1343        controls.clone(),
1344        encode_tid_tx.clone(),
1345        &mut on_frame,
1346    ) {
1347        return result;
1348    }
1349    crate::report::capture("XShm", false);
1350    run_shm_capture(settings, controls, encode_tid_tx, on_frame)
1351}
1352
1353/// Run the X11 capture pipeline until `stop` is set, splitting capture and encode across two
1354/// threads that overlap for throughput.
1355///
1356/// This (the caller's) thread performs setup and then the capture loop; a spawned encode thread runs
1357/// `encode_loop` and invokes `on_frame(stripes)` once per encoded frame. The split lets capture and
1358/// encode overlap, and because frames are throttled/dropped as RAW frames before encode, the delivered
1359/// H.264 stays a valid contiguous reference chain.
1360///
1361/// 1. **Setup**: connect to X (a private connection) and require a 32-bpp BGRA root — the Z-pixmap
1362///    byte depth must be 4, which modern servers use for depth 24/32 — then negotiate XShm and
1363///    XFixes. XFixes is always negotiated (one round-trip) so the cursor overlay can be toggled on
1364///    live even when capture started without it. Initial dimensions and origin are resolved from the
1365///    settings and the live root geometry, and a `FramePool` of `POOL_N` = 3 shm surfaces is
1366///    allocated — the working set (one in-capture, one in-slot, one in-encode) that a one-frame-ahead
1367///    demand-driven capture needs, which also keeps the memory cost (`3 * W*H*4`, significant at 4K)
1368///    bounded.
1369/// 2. **Encode thread**: spawned with a raised scheduling priority; it reports its thread id back
1370///    through `encode_tid_tx` so the caller can detect a re-entrant stop issued from inside the
1371///    delivery callback.
1372/// 3. **Capture loop** (until `stop`): fps is re-read each iteration for live pacing. The wait
1373///    runs to the frame deadline, but a region the X server reports damaged ends it early once
1374///    the shared pacing budget allows (`crate::pace`), so a screen that changes mid-period is
1375///    published then rather than on the next tick. A server without DAMAGE keeps the deadline
1376///    alone.
1377///    - **Live region change** (`region_dirty`): re-target the grab origin immediately (an x/y pan
1378///      needs no surface work); a size change reuses the drain/recreate path below.
1379///    - **Auto-adjust**: on a root geometry change, drain in-flight frames then recreate the surfaces
1380///      and bump the generation (which the encode thread turns into a reshape or rebuild). The geometry
1381///      is re-resolved AFTER the drain, because a slow drain can outlast another geometry change (a
1382///      fast flap) and recreating at a stale size would make the next `shm_get_image` exceed the root;
1383///      a fully-reverted flap keeps its surfaces as-is. A stop that races the drain breaks out to
1384///      teardown.
1385///    - **Grab**: acquire a pooled surface (blocks until the encoder frees one), then bring it up
1386///      to the screen: from a shared framebuffer, the tiles drawn since the surface last held
1387///      the screen, and the cursor's and the watermark's, whose wait ends on the watch rather
1388///      than on DAMAGE (`framebuffer`); else, or where no whole read comes, `shm_get_image` of
1389///      the region synchronously (`reply()` waits). A framebuffer a resize retired, or one of a
1390///      server that went away, is watched again on the next grab. Where the encode thread's
1391///      pipeline takes 8-bit 4:2:0 planes (`Controls::takes_planes`) and no overlay is drawn,
1392///      the surface's planes are converted from the shared framebuffer in place of copying its
1393///      pixels (`FramebufferGrab::convert`), and the frame carries them.
1394///    - **Overlays**: composite the XFixes cursor (drawn at its hotspot-offset origin; live-toggleable)
1395///      and the watermark onto the BGRA pixels on the CPU.
1396///    - **Publish**: hand the finished frame to the encode thread (blocks until the slot is free;
1397///      never drops). A stop observed while waiting in acquire/publish exits the loop.
1398/// 4. **Teardown**: stop and JOIN the encode thread BEFORE destroying the shm surfaces it may still be
1399///    reading, preserving the resize-safety guarantee.
1400///
1401/// Blocking; intended to run on a dedicated thread. The X connection and shm surfaces live on this
1402/// thread and the encoder lives on the encode thread — nothing X-related crosses the boundary.
1403fn run_shm_capture<F>(
1404    settings: CaptureSettings,
1405    controls: Arc<Controls>,
1406    encode_tid_tx: Sender<thread::ThreadId>,
1407    on_frame: F,
1408) -> Result<(), String>
1409where
1410    F: FnMut(Vec<EncodedStripe>) + Send + 'static,
1411{
1412    let (mut conn, screen_num) =
1413        x11rb::connect(None).map_err(|e| format!("X11 connect failed: {e}"))?;
1414    let mut root = conn.setup().roots[screen_num].root;
1415    require_32bpp(&conn, screen_num)?;
1416
1417    conn.shm_query_version()
1418        .map_err(|e| format!("shm_query_version: {e}"))?
1419        .reply()
1420        .map_err(|e| format!("XShm unavailable: {e}"))?;
1421    conn.xfixes_query_version(5, 0)
1422        .map_err(|e| format!("xfixes_query_version: {e}"))?
1423        .reply()
1424        .map_err(|e| format!("XFixes unavailable: {e}"))?;
1425
1426    let geo = conn
1427        .get_geometry(root)
1428        .map_err(|e| format!("get_geometry: {e}"))?
1429        .reply()
1430        .map_err(|e| format!("get_geometry reply: {e}"))?;
1431
1432    let mut rsettings = settings.clone();
1433    let (mut cap_w, mut cap_h) = resolve_dims(geo.width, geo.height, &rsettings);
1434    let mut cap_x = clamp_offset(rsettings.capture_x, geo.width);
1435    let mut cap_y = clamp_offset(rsettings.capture_y, geo.height);
1436    // Last root size seen. A live pan has to clamp against something even when the geometry
1437    // round-trip fails, or the requested origin would be dropped instead of applied.
1438    let (mut root_w, mut root_h) = (geo.width, geo.height);
1439
1440    const POOL_N: usize = 3;
1441    let mut surfaces: Vec<ShmSurface> = Vec::with_capacity(POOL_N);
1442    for _ in 0..POOL_N {
1443        surfaces.push(ShmSurface::create(&conn, cap_w, cap_h)?);
1444    }
1445    let mut shared = open_framebuffer(
1446        &conn,
1447        root,
1448        &mut surfaces[0],
1449        (cap_x, cap_y, cap_w, cap_h),
1450        POOL_N,
1451    );
1452    let mut damage = if shared.is_none() {
1453        println!("[X11] Capturing through XShm ({cap_w}x{cap_h})");
1454        RootDamage::create(&conn, root)
1455    } else {
1456        None
1457    };
1458    // A shared framebuffer that retired, or whose server went away, is watched again on the next
1459    // grab, the XShm path's damage standing in where that fails.
1460    let mut rewatch = false;
1461    // Where the cursor and the watermark were drawn onto the last frame published.
1462    let mut last_overlays: Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> =
1463        Vec::new();
1464    // The XFixes serial of the cursor image drawn onto the last frame published: a cursor that
1465    // changes its image in place changes no overlay rectangle.
1466    let mut last_cursor_serial = None;
1467    // Consecutive channel rebuilds without one good grab in between: geometry
1468    // errors can burn out the grab budget repeatedly under a dead region, so
1469    // recovery only gets a bounded number of channel rebuilds.
1470    let mut reconnect_streak = 0u32;
1471    let pool = Arc::new(FramePool::new(POOL_N));
1472
1473    let mut watermark = crate::encoders::overlay::OverlayState::default();
1474    if !settings.watermark_path.is_empty() {
1475        watermark.load_watermark(&settings.watermark_path, 1.0);
1476    }
1477
1478    let enc_pool = pool.clone();
1479    let enc_controls = controls.clone();
1480    let enc_settings = settings.clone();
1481    let encode_panicked = Arc::new(AtomicBool::new(false));
1482    let guard_panicked = encode_panicked.clone();
1483    let encode_thread = thread::spawn(move || {
1484        crate::boost_thread_priority(-10);
1485        let _ = encode_tid_tx.send(thread::current().id());
1486        let mut on_frame = on_frame;
1487        // An encoder panic must never wedge the capture: without the pool shutdown
1488        // the capture thread would block in publish forever while is_capturing still
1489        // reports true on this thread's handle. Shutting the pool unblocks it through
1490        // the same path a clean stop takes, so the panic is also recorded — otherwise
1491        // the capture would return Ok and Python could not tell the two apart.
1492        let guard_pool = enc_pool.clone();
1493        let guard_controls = enc_controls.clone();
1494        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1495            encode_loop(&enc_pool, &enc_controls, &enc_settings, &mut on_frame);
1496        }));
1497        if result.is_err() {
1498            eprintln!("[X11] encode thread panicked; shutting the pool to fail the capture");
1499            guard_panicked.store(true, Ordering::Release);
1500            guard_pool.shutdown();
1501            guard_controls.stop.store(true, Ordering::Relaxed);
1502        }
1503    });
1504
1505    // One frame a period and the extras a change in a taken one spends (FramePace::windowed).
1506    let mut pace = FramePace::windowed();
1507    // Each pool surface's planes, where the pipeline takes them (`Controls::takes_planes`), and
1508    // whether the last frame was converted into them rather than copied.
1509    let csc_bands = crate::encoders::session::h264_fullframe_threads(&rsettings).max(1) as usize;
1510    let mut planes_pool: Vec<Nv12Planes> = Vec::new();
1511    let mut planes_dims = (0u16, 0u16);
1512    let mut planes_mode = false;
1513    // Frames between root-geometry polls (auto-adjust); ~0.5s at 60fps.
1514    const GEOMETRY_POLL_FRAMES: i32 = 30;
1515    let mut geometry_check = 0i32;
1516    let mut grab_failures = 0u32;
1517
1518    let result = (|| -> Result<(), String> {
1519        while !controls.stop.load(Ordering::Relaxed) {
1520            let fps = f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
1521            let frame_dur = Duration::from_secs_f64(1.0 / fps.max(1.0));
1522            if pace.next_due(frame_dur, Instant::now()) <= Instant::now() {
1523                thread::yield_now();
1524            }
1525            let trigger = match &shared {
1526                Some(grab) => wait_for_framebuffer(&grab.fb, &pace, frame_dur),
1527                None => wait_for_frame(&conn, damage.as_ref(), &pace, frame_dur),
1528            };
1529            let woke = Instant::now();
1530            if shared.as_ref().is_some_and(|grab| grab.fb.retired()) {
1531                if let Some(grab) = shared.take() {
1532                    grab.fb.close(&conn);
1533                }
1534                rewatch = true;
1535                geometry_check = 0;
1536            }
1537            // Every grab is published; without Damage the still check never matters, since
1538            // nothing pulls a frame forward.
1539            pace.record_tick(trigger, frame_dur, woke, true);
1540            if controls.stop.load(Ordering::Relaxed) {
1541                break;
1542            }
1543
1544            if controls.region_dirty.swap(false, Ordering::Acquire) {
1545                let (nx, ny, nw, nh) = *controls.region.lock().unwrap();
1546                rsettings.capture_x = nx;
1547                rsettings.capture_y = ny;
1548                rsettings.width = nw;
1549                rsettings.height = nh;
1550                // An explicit live size pins the region: auto-adjust (from the start
1551                // settings) would glue resolve_dims back to the ROOT size, undoing the
1552                // re-target on the next geometry poll. w/h <= 0 keep following the root.
1553                rsettings.auto_adjust_screen_capture_size = nw <= 0 || nh <= 0;
1554                cap_x = clamp_offset(nx, root_w);
1555                cap_y = clamp_offset(ny, root_h);
1556                if let Some(grab) = shared.as_mut() {
1557                    grab.invalidate();
1558                }
1559                if let Some(g) = conn.get_geometry(root).ok().and_then(|c| c.reply().ok()) {
1560                    root_w = g.width;
1561                    root_h = g.height;
1562                    cap_x = clamp_offset(nx, root_w);
1563                    cap_y = clamp_offset(ny, root_h);
1564                    let (fw, fh) = resolve_dims(g.width, g.height, &rsettings);
1565                    if fw != cap_w || fh != cap_h {
1566                        if !pool.drain_for_resize(POOL_N, &controls.stop) {
1567                            break;
1568                        }
1569                        for s in surfaces.iter_mut() {
1570                            s.destroy(&conn);
1571                        }
1572                        surfaces.clear();
1573                        cap_w = fw;
1574                        cap_h = fh;
1575                        for _ in 0..POOL_N {
1576                            surfaces.push(ShmSurface::create(&conn, cap_w, cap_h)?);
1577                        }
1578                        pool.bump_generation();
1579                        if let Some(grab) = shared.as_mut() {
1580                            grab.invalidate();
1581                        }
1582                    }
1583                }
1584            }
1585
1586            // Root geometry is polled on a cadence, not per frame: the reply costs a
1587            // round-trip that would otherwise precede every grab, and external size
1588            // changes are rare (live resizes arrive through region_dirty instead).
1589            // The gate reads the live copy, so a region re-targeted to the root edge
1590            // starts following it from that point on. Grab-failure recovery enters
1591            // regardless of auto_adjust: a fixed-size region has no other re-clamp
1592            // path after an external root shrink, and without one the capture dies at
1593            // the failure cap instead of recovering.
1594            geometry_check = geometry_check.saturating_sub(1);
1595            if (rsettings.auto_adjust_screen_capture_size || grab_failures > 0)
1596                && geometry_check <= 0
1597            {
1598                geometry_check = GEOMETRY_POLL_FRAMES;
1599                if let Some(g) = conn.get_geometry(root).ok().and_then(|c| c.reply().ok()) {
1600                    // A root shrink can strand even a previously-valid offset past
1601                    // the new edge; re-clamp it against the live root.
1602                    root_w = g.width;
1603                    root_h = g.height;
1604                    let (px, py) = (cap_x, cap_y);
1605                    cap_x = clamp_offset(rsettings.capture_x, root_w);
1606                    cap_y = clamp_offset(rsettings.capture_y, root_h);
1607                    if (px, py) != (cap_x, cap_y)
1608                        && let Some(grab) = shared.as_mut()
1609                    {
1610                        grab.invalidate();
1611                    }
1612                    let (nw, nh) = resolve_dims(g.width, g.height, &rsettings);
1613                    if nw != cap_w || nh != cap_h {
1614                        if !pool.drain_for_resize(POOL_N, &controls.stop) {
1615                            break;
1616                        }
1617                        let (fw, fh) = conn
1618                            .get_geometry(root)
1619                            .ok()
1620                            .and_then(|c| c.reply().ok())
1621                            .map(|g| resolve_dims(g.width, g.height, &rsettings))
1622                            .unwrap_or((nw, nh));
1623                        if fw != cap_w || fh != cap_h {
1624                            for s in surfaces.iter_mut() {
1625                                s.destroy(&conn);
1626                            }
1627                            surfaces.clear();
1628                            cap_w = fw;
1629                            cap_h = fh;
1630                            for _ in 0..POOL_N {
1631                                surfaces.push(ShmSurface::create(&conn, cap_w, cap_h)?);
1632                            }
1633                            pool.bump_generation();
1634                            if let Some(grab) = shared.as_mut() {
1635                                grab.invalidate();
1636                            }
1637                            if let Some(grab) = shared.as_mut() {
1638                                grab.invalidate();
1639                            }
1640                        }
1641                    }
1642                }
1643            }
1644
1645            let idx = match pool.acquire(&controls.stop) {
1646                Some(i) => i,
1647                None => break,
1648            };
1649            let mut reported = None;
1650            if std::mem::take(&mut rewatch) {
1651                shared = open_framebuffer(
1652                    &conn,
1653                    root,
1654                    &mut surfaces[idx],
1655                    (cap_x, cap_y, cap_w, cap_h),
1656                    POOL_N,
1657                );
1658                if shared.is_none() && damage.is_none() {
1659                    damage = RootDamage::create(&conn, root);
1660                }
1661            }
1662            // A session taking planes converts them straight from the shared framebuffer, unless
1663            // the cursor or the watermark is drawn onto the pixels; a switch either way leaves the
1664            // other form stale, so it starts over from the whole screen.
1665            let use_planes = shared.is_some()
1666                && controls.takes_planes.load(Ordering::Relaxed)
1667                && !controls.capture_cursor.load(Ordering::Relaxed)
1668                && !watermark.is_active();
1669            let fresh =
1670                use_planes && (planes_pool.len() != POOL_N || planes_dims != (cap_w, cap_h));
1671            if fresh {
1672                planes_pool = (0..POOL_N)
1673                    .map(|_| Nv12Planes::new(cap_w as usize, cap_h as usize))
1674                    .collect();
1675                planes_dims = (cap_w, cap_h);
1676            }
1677            if fresh || use_planes != planes_mode {
1678                planes_mode = use_planes;
1679                if let Some(grab) = shared.as_mut() {
1680                    grab.invalidate();
1681                }
1682            }
1683            let grab_result = {
1684                let surface = &mut surfaces[idx];
1685                let stride = surface.stride;
1686                let region = (
1687                    cap_x as u32,
1688                    cap_y as u32,
1689                    u32::from(cap_w),
1690                    u32::from(cap_h),
1691                );
1692                match shared.as_mut() {
1693                    Some(grab) if use_planes => {
1694                        let planes = &mut planes_pool[idx];
1695                        if grab.convert(idx, planes, region, csc_bands) {
1696                            Ok(())
1697                        } else {
1698                            grab_frame(&conn, root, surface, cap_x, cap_y, cap_w, cap_h).and_then(
1699                                |()| {
1700                                    let strides = (planes.stride[0], planes.stride[1]);
1701                                    let (y, uv) = planes.planes_mut();
1702                                    crate::encoders::software::convert_to_nv12_mt(
1703                                        surface.as_mut_slice(),
1704                                        stride as u32,
1705                                        cap_w as usize,
1706                                        cap_h as usize,
1707                                        false,
1708                                        y,
1709                                        uv,
1710                                        strides,
1711                                        csc_bands,
1712                                    )
1713                                    .map_err(|e| format!("convert: {e:?}"))?;
1714                                    grab.grabbed(idx);
1715                                    Ok(())
1716                                },
1717                            )
1718                        }
1719                    }
1720                    Some(grab) => {
1721                        if grab.read(idx, surface.as_mut_slice(), stride, region) {
1722                            Ok(())
1723                        } else {
1724                            grab_frame(&conn, root, surface, cap_x, cap_y, cap_w, cap_h)
1725                                .map(|()| grab.grabbed(idx))
1726                        }
1727                    }
1728                    None => {
1729                        reported = damage.as_ref().and_then(|d| d.take(&conn));
1730                        grab_frame(&conn, root, surface, cap_x, cap_y, cap_w, cap_h)
1731                    }
1732                }
1733            };
1734            let region = (
1735                cap_x as u32,
1736                cap_y as u32,
1737                u32::from(cap_w),
1738                u32::from(cap_h),
1739            );
1740            let mut changed = match (shared.as_mut(), reported.take()) {
1741                (Some(grab), _) => grab.changed(region),
1742                (None, Some(rects)) => frame_rects(&rects, cap_x, cap_y, cap_w, cap_h),
1743                (None, None) => Vec::new(),
1744            };
1745            if let Err(e) = grab_result {
1746                // Most likely an external root resize between geometry polls made
1747                // the grab run past the root edge: return the surface, re-poll
1748                // geometry immediately, and only give up if it never recovers.
1749                pool.recycle(idx);
1750                grab_failures += 1;
1751                geometry_check = 0;
1752                if grab_failures > 120 {
1753                    // A burn-out usually means the X connection itself is gone
1754                    // (Xorg restart, VT switch): every request fails until the
1755                    // server is back. Rebuild the channel a few times before
1756                    // declaring the capture dead — a recovered server resumes the
1757                    // stream where a dead thread would leave a black screen until
1758                    // the Python watchdog restarts everything.
1759                    let mut recovered = false;
1760                    for _attempt in 0..5 {
1761                        if controls.stop.load(Ordering::Relaxed) {
1762                            break;
1763                        }
1764                        std::thread::sleep(Duration::from_secs(1));
1765                        match try_rebuild_channel(
1766                            &mut conn,
1767                            &mut root,
1768                            &rsettings,
1769                            &mut cap_x,
1770                            &mut cap_y,
1771                            &mut cap_w,
1772                            &mut cap_h,
1773                            &mut root_w,
1774                            &mut root_h,
1775                            POOL_N,
1776                            &pool,
1777                            &mut surfaces,
1778                            &controls.stop,
1779                        ) {
1780                            Ok(()) => {
1781                                // The reported region and the framebuffer belonged to the
1782                                // server that went away.
1783                                damage = RootDamage::create(&conn, root);
1784                                shared = None;
1785                                rewatch = true;
1786                                recovered = true;
1787                                break;
1788                            }
1789                            Err(re) => {
1790                                eprintln!("[X11] reconnect attempt failed: {re}");
1791                            }
1792                        }
1793                    }
1794                    if !recovered {
1795                        return Err(format!("capture could not recover: {e}"));
1796                    }
1797                    grab_failures = 0;
1798                    geometry_check = 0;
1799                    reconnect_streak += 1;
1800                    if reconnect_streak >= 4 {
1801                        return Err(
1802                            "capture recovered its channel but the region never grabbed again"
1803                                .to_string(),
1804                        );
1805                    }
1806                }
1807                continue;
1808            }
1809            grab_failures = 0;
1810            reconnect_streak = 0;
1811            let surface = &mut surfaces[idx];
1812
1813            let frame_w = i32::from(cap_w);
1814            let frame_h = i32::from(cap_h);
1815            let stride = surface.stride;
1816            let buf = surface.as_mut_slice();
1817            let mut overlays = Vec::new();
1818            let mut cursor_serial = None;
1819
1820            if controls.capture_cursor.load(Ordering::Relaxed)
1821                && let Some(c) = conn
1822                    .xfixes_get_cursor_image()
1823                    .ok()
1824                    .and_then(|c| c.reply().ok())
1825                && c.width > 0
1826                && c.height > 0
1827            {
1828                // Translate from root coordinates using the LIVE grab origin
1829                // (cap_x/cap_y follow region_dirty pans and clamp negatives
1830                // exactly like the grab itself), never the immutable startup
1831                // settings — those go stale on the first live region move.
1832                let (img_x, img_y) = cursor_image_origin(
1833                    c.x,
1834                    c.y,
1835                    c.xhot,
1836                    c.yhot,
1837                    i32::from(cap_x),
1838                    i32::from(cap_y),
1839                );
1840                cursor_serial = Some(c.cursor_serial);
1841                overlay_cursor(
1842                    buf,
1843                    stride,
1844                    frame_w,
1845                    frame_h,
1846                    i32::from(c.width),
1847                    i32::from(c.height),
1848                    &c.cursor_image,
1849                    img_x,
1850                    img_y,
1851                );
1852                if let Some(grab) = shared.as_mut() {
1853                    grab.overlaid(
1854                        idx,
1855                        img_x + i32::from(cap_x),
1856                        img_y + i32::from(cap_y),
1857                        i32::from(c.width),
1858                        i32::from(c.height),
1859                    );
1860                }
1861                overlays.push(smithay::utils::Rectangle::new(
1862                    (img_x, img_y).into(),
1863                    (i32::from(c.width), i32::from(c.height)).into(),
1864                ));
1865            }
1866
1867            if watermark.is_active() {
1868                watermark.update_position(frame_w, frame_h, settings.watermark_location_enum);
1869                watermark.blend_bgra(buf, stride, frame_w, frame_h);
1870                let drawn = watermark.damage_rect(frame_w, frame_h);
1871                overlays.extend(drawn);
1872                if let (Some(grab), Some(r)) = (shared.as_mut(), drawn) {
1873                    grab.overlaid(
1874                        idx,
1875                        r.loc.x + i32::from(cap_x),
1876                        r.loc.y + i32::from(cap_y),
1877                        r.size.w,
1878                        r.size.h,
1879                    );
1880                }
1881            }
1882
1883            // The overlays moved, or the cursor changed its image: where they were and where
1884            // they are now changed with them.
1885            if overlays != last_overlays || cursor_serial != last_cursor_serial {
1886                changed.extend(last_overlays.iter().copied());
1887                changed.extend(overlays.iter().copied());
1888                last_overlays = overlays;
1889                last_cursor_serial = cursor_serial;
1890            }
1891            let published = pool.publish(
1892                RawFrame {
1893                    idx,
1894                    ptr: surface.addr,
1895                    len: surface.size,
1896                    width: cap_w,
1897                    height: cap_h,
1898                    stride,
1899                    generation: pool.generation(),
1900                    captured_ns: crate::wayland::host::now_ns(),
1901                    changed,
1902                    complete: shared.is_some(),
1903                    planes: use_planes.then(|| &planes_pool[idx] as *const Nv12Planes),
1904                },
1905                &controls.stop,
1906            );
1907            if !published {
1908                break;
1909            }
1910        }
1911        Ok(())
1912    })();
1913
1914    pool.shutdown();
1915    let _ = encode_thread.join();
1916    if let Some(grab) = shared.take() {
1917        grab.fb.close(&conn);
1918    }
1919    for s in surfaces.iter_mut() {
1920        s.destroy(&conn);
1921    }
1922    match result {
1923        // A real capture error is the more specific diagnosis, so it outranks the panic flag.
1924        Err(e) => Err(e),
1925        Ok(()) if encode_panicked.load(Ordering::Acquire) => Err("encode thread panicked".into()),
1926        Ok(()) => Ok(()),
1927    }
1928}
1929
1930/// Helpers the hardware checks of the zero-copy backends share: a known picture on the root of
1931/// `$DISPLAY`, and the color a decoded frame of it must average to.
1932#[cfg(test)]
1933pub(crate) mod gpu_test_support {
1934    use std::time::Duration;
1935
1936    use crate::CaptureSettings;
1937    use crate::encoders::codec::Codec;
1938    use crate::webcam::decode::{Decoder, VideoDecoder};
1939
1940    /// Full-frame capture settings for `codec` at CRF 25, streaming every frame, on the render
1941    /// node `auto_gpu` picks first.
1942    pub(crate) fn settings(codec: Codec) -> CaptureSettings {
1943        CaptureSettings {
1944            codec,
1945            target_fps: 60.0,
1946            video_crf: 25,
1947            video_streaming_mode: true,
1948            encode_node_index: crate::auto_render_node("true")
1949                .and_then(|node| crate::render_node_index(&node))
1950                .unwrap_or(0),
1951            ..Default::default()
1952        }
1953    }
1954
1955    /// Paint the whole root of `$DISPLAY` one solid color and let the server finish, so the
1956    /// next capture has a known picture in it.
1957    ///
1958    /// The screen saver is turned off first: a test display sees no input, so a server left
1959    /// with the default ten-minute blanking timeout hands the capture a black screen and every
1960    /// color comparison fails for a reason that has nothing to do with the capture.
1961    pub(crate) fn paint_root(rgb: (u8, u8, u8)) -> bool {
1962        let _ = std::process::Command::new("xset")
1963            .args(["s", "off", "s", "noblank"])
1964            .output();
1965        let _ = std::process::Command::new("xset")
1966            .arg("s")
1967            .arg("reset")
1968            .output();
1969        let spec = format!("#{:02x}{:02x}{:02x}", rgb.0, rgb.1, rgb.2);
1970        let out = std::process::Command::new("xsetroot")
1971            .args(["-solid", &spec])
1972            .output();
1973        if !out.map(|o| o.status.success()).unwrap_or(false) {
1974            return false;
1975        }
1976        std::thread::sleep(Duration::from_millis(120));
1977        true
1978    }
1979
1980    /// Limited-range Y/Cb/Cr of an 8-bit RGB triple, what the chroma convert emits for a
1981    /// captured surface.
1982    pub(crate) fn painted_ycbcr(rgb: (u8, u8, u8)) -> [f64; 3] {
1983        use crate::encoders::chroma_siting::{BT709, ycbcr};
1984        ycbcr([rgb.0, rgb.1, rgb.2].map(f64::from), BT709)
1985    }
1986
1987    /// Mean Y/Cb/Cr of a decoded picture.
1988    pub(crate) fn decoded_mean(dec: &mut VideoDecoder, payload: &[u8]) -> [f64; 3] {
1989        assert!(
1990            dec.decode(payload).expect("decode"),
1991            "no picture from this access unit"
1992        );
1993        let v = dec.frame().expect("decoded frame");
1994        let mut acc = [0f64; 3];
1995        let mut n = 0f64;
1996        for y in 0..v.height {
1997            for x in 0..v.width {
1998                acc[0] += f64::from(v.y[y * v.y_stride + x]);
1999                acc[1] += f64::from(v.u[(y / 2) * v.uv_stride + x / 2]);
2000                acc[2] += f64::from(v.v[(y / 2) * v.uv_stride + x / 2]);
2001                n += 1.0;
2002            }
2003        }
2004        [acc[0] / n, acc[1] / n, acc[2] / n]
2005    }
2006
2007    /// A GPU capture path (`dri3::run_capture`, `nvfbc::run_capture`).
2008    pub(crate) type Capture = fn(
2009        CaptureSettings,
2010        std::sync::Arc<super::Controls>,
2011        std::sync::mpsc::Sender<std::thread::ThreadId>,
2012        &mut dyn FnMut(Vec<crate::encoders::software::EncodedStripe>),
2013    ) -> Option<Result<(), String>>;
2014
2015    /// A page scrolled on `$DISPLAY` and then held still, captured through `capture` at CRF 25,
2016    /// the paint-over at 18 armed or not (`paint_over`): uncapped, and capped at a peak of 300 kbit/s
2017    /// the scroll overflows and then raised to 100 Mbit/s for the still page. For each, the
2018    /// PSNR-Y of the last picture shown against the page, the frames sent while still and their
2019    /// bytes, and the key frames after the first; `None` where the path declines the host.
2020    pub(crate) fn capped_crf_codes_again_what_it_coded_bound(
2021        capture: Capture,
2022        paint_over: bool,
2023    ) -> Option<[(f64, usize, usize, usize); 2]> {
2024        use crate::encoders::codec::{FRAME_KEY, VIDEO_HEADER_LEN, parse_video_type};
2025        use std::sync::atomic::Ordering;
2026        use std::time::Instant;
2027        use x11rb::connection::Connection;
2028        use x11rb::protocol::xproto::{ConnectionExt as _, CreateGCAux, ImageFormat};
2029        const W: usize = 640;
2030        const H: usize = 360;
2031        const SCROLL: usize = 120;
2032        if !paint_root((0x12, 0x12, 0x12)) {
2033            println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
2034            return None;
2035        }
2036        let (conn, screen) = x11rb::connect(None).expect("connect");
2037        let root = conn.setup().roots[screen].root;
2038        let gc = conn.generate_id().expect("id");
2039        conn.create_gc(gc, root, &CreateGCAux::new()).expect("gc");
2040        let paint = |t: usize| {
2041            let page = crate::encoders::scrolled_text(W, H, t);
2042            for (band, rows) in page.chunks(W * 4 * 64).enumerate() {
2043                conn.put_image(
2044                    ImageFormat::Z_PIXMAP,
2045                    root,
2046                    gc,
2047                    W as u16,
2048                    (rows.len() / (W * 4)) as u16,
2049                    0,
2050                    (band * 64) as i16,
2051                    0,
2052                    24,
2053                    rows,
2054                )
2055                .expect("put")
2056                .check()
2057                .expect("put done");
2058            }
2059        };
2060        let run = |capped: bool| -> Option<(f64, usize, usize, usize)> {
2061            paint(0);
2062            let mut s = settings(Codec::H264);
2063            s.width = W as i32;
2064            s.height = H as i32;
2065            s.video_streaming_mode = false;
2066            s.use_paint_over_quality = paint_over;
2067            s.video_paintover_crf = 18;
2068            s.video_bitrate_kbps = if capped { 300 } else { 8000 };
2069            s.video_vbv_multiplier = if capped {
2070                crate::encoders::CAPPED_VBV_FRAMES
2071            } else {
2072                0.0
2073            };
2074            let vbv = s.video_vbv_multiplier;
2075            let controls = std::sync::Arc::new(super::Controls::new(&s));
2076            let (tx, rx) = std::sync::mpsc::channel::<(Instant, Vec<u8>)>();
2077            let thread = {
2078                let controls = controls.clone();
2079                std::thread::spawn(move || {
2080                    let (tid, _rx) = std::sync::mpsc::channel();
2081                    capture(s, controls, tid, &mut |stripes| {
2082                        for st in stripes {
2083                            let _ = tx.send((Instant::now(), st.data.to_vec()));
2084                        }
2085                    })
2086                })
2087            };
2088            // A session can take over a second to open; the path declining ends the thread.
2089            let mut frames = Vec::new();
2090            let end = Instant::now() + Duration::from_secs(15);
2091            while frames.is_empty() && !thread.is_finished() && Instant::now() < end {
2092                paint(0);
2093                std::thread::sleep(Duration::from_millis(50));
2094                frames.extend(rx.try_iter());
2095            }
2096            if thread.is_finished() {
2097                return None;
2098            }
2099            for t in 1..=SCROLL {
2100                paint(t);
2101                std::thread::sleep(Duration::from_millis(16));
2102            }
2103            std::thread::sleep(Duration::from_millis(100));
2104            let still = Instant::now();
2105            if capped {
2106                controls.bitrate_kbps.store(100_000, Ordering::Relaxed);
2107                controls
2108                    .vbv_mult_milli
2109                    .store((vbv * 1000.0).round() as i32, Ordering::Relaxed);
2110                controls.rate_dirty.store(true, Ordering::Release);
2111            }
2112            std::thread::sleep(Duration::from_secs(3));
2113            controls.stop.store(true, Ordering::Relaxed);
2114            let ended = thread.join().expect("capture thread");
2115            assert!(matches!(ended, Some(Ok(()))), "{ended:?}");
2116            frames.extend(rx.try_iter());
2117            let mut dec = VideoDecoder::new(crate::webcam::decode::Codec::H264).expect("decoder");
2118            let (mut sent, mut bytes, mut keys) = (0, 0, 0);
2119            for (i, (at, data)) in frames.iter().enumerate() {
2120                if *at > still {
2121                    sent += 1;
2122                    bytes += data.len();
2123                }
2124                keys += usize::from(
2125                    i > 0 && parse_video_type(data[1]).map(|(_, k)| k) == Some(FRAME_KEY),
2126                );
2127                dec.decode(&data[VIDEO_HEADER_LEN..]).expect("decode");
2128            }
2129            let shown = dec.frame().expect("picture");
2130            let page = crate::encoders::scrolled_text(W, H, SCROLL);
2131            let mut mse = 0.0;
2132            for y in 0..H {
2133                for x in 0..W {
2134                    let want = 16.0 + f64::from(page[(y * W + x) * 4]) * 219.0 / 255.0;
2135                    let d = f64::from(shown.y[y * shown.y_stride + x]) - want;
2136                    mse += d * d;
2137                }
2138            }
2139            let psnr = 10.0 * (255.0f64 * 255.0 / (mse / (W * H) as f64)).log10();
2140            Some((psnr, sent, bytes, keys))
2141        };
2142        let plain = run(false)?;
2143        Some([plain, run(true).expect("the capped capture")])
2144    }
2145}
2146
2147#[cfg(test)]
2148mod pool_tests {
2149    use super::*;
2150
2151    fn dummy(idx: usize) -> RawFrame {
2152        RawFrame {
2153            idx,
2154            ptr: std::ptr::null_mut(),
2155            len: 0,
2156            width: 0,
2157            height: 0,
2158            stride: 0,
2159            generation: 0,
2160            captured_ns: 0,
2161            changed: Vec::new(),
2162            complete: false,
2163            planes: None,
2164        }
2165    }
2166
2167    /// A full acquire -> publish -> take -> recycle round-trip returns the same surface, and
2168    /// afterwards all three pool surfaces are acquirable again and distinct.
2169    #[test]
2170    fn roundtrip_then_recycle_returns_all_surfaces() {
2171        let p = FramePool::new(3);
2172        let stop = AtomicBool::new(false);
2173        let a = p.acquire(&stop).unwrap();
2174        assert!(p.publish(dummy(a), &stop));
2175        let f = p.take(|| {}).unwrap();
2176        assert_eq!(f.idx, a);
2177        p.recycle(f.idx);
2178        let (x, y, z) = (
2179            p.acquire(&stop).unwrap(),
2180            p.acquire(&stop).unwrap(),
2181            p.acquire(&stop).unwrap(),
2182        );
2183        assert!(x != y && y != z && x != z);
2184    }
2185
2186    /// With every surface held and `stop` set, `acquire` returns `None` after its bounded wait
2187    /// rather than blocking forever.
2188    #[test]
2189    fn acquire_returns_none_when_exhausted_and_stopped() {
2190        let p = FramePool::new(2);
2191        let stop = AtomicBool::new(false);
2192        let _a = p.acquire(&stop).unwrap();
2193        let _b = p.acquire(&stop).unwrap();
2194        stop.store(true, Ordering::Relaxed);
2195        assert!(p.acquire(&stop).is_none());
2196    }
2197
2198    /// With the handoff slot already occupied and `stop` set, `publish` returns `false` (frame
2199    /// discarded) instead of blocking.
2200    #[test]
2201    fn publish_returns_false_when_slot_full_and_stopped() {
2202        let p = FramePool::new(3);
2203        let stop = AtomicBool::new(false);
2204        let a = p.acquire(&stop).unwrap();
2205        assert!(p.publish(dummy(a), &stop));
2206        let b = p.acquire(&stop).unwrap();
2207        stop.store(true, Ordering::Relaxed);
2208        assert!(!p.publish(dummy(b), &stop));
2209    }
2210
2211    /// `drain_for_resize` blocks until every held surface has been recycled (`free == n`), then
2212    /// reports a full drain and leaves the pool whole; a helper thread recycles the three held surfaces
2213    /// shortly after the drain begins.
2214    #[test]
2215    fn drain_for_resize_waits_until_all_free() {
2216        let p = Arc::new(FramePool::new(3));
2217        let stop = AtomicBool::new(false);
2218        let held = [
2219            p.acquire(&stop).unwrap(),
2220            p.acquire(&stop).unwrap(),
2221            p.acquire(&stop).unwrap(),
2222        ];
2223        let p2 = p.clone();
2224        let t = thread::spawn(move || {
2225            thread::sleep(Duration::from_millis(30));
2226            for idx in held {
2227                p2.recycle(idx);
2228            }
2229        });
2230        assert!(p.drain_for_resize(3, &stop));
2231        t.join().unwrap();
2232        assert!(p.acquire(&stop).is_some());
2233    }
2234
2235    /// The surface the encoder still reads stays out of the free list while the others return,
2236    /// and comes back once a later encode no longer reads it.
2237    #[test]
2238    fn a_held_surface_stays_out_until_settled() {
2239        let p = FramePool::new(3);
2240        let stop = AtomicBool::new(false);
2241        let (a, b, c) = (
2242            p.acquire(&stop).unwrap(),
2243            p.acquire(&stop).unwrap(),
2244            p.acquire(&stop).unwrap(),
2245        );
2246        p.settle(&[b, c], Some(a));
2247        let (x, y) = (p.acquire(&stop).unwrap(), p.acquire(&stop).unwrap());
2248        assert!(![x, y].contains(&a));
2249        stop.store(true, Ordering::Relaxed);
2250        assert!(p.acquire(&stop).is_none(), "the held surface is not free");
2251        stop.store(false, Ordering::Relaxed);
2252        p.settle(&[a, x], Some(y));
2253        assert!(p.acquire(&stop).is_some() && p.acquire(&stop).is_some());
2254        p.recycle(a);
2255        stop.store(true, Ordering::Relaxed);
2256        assert!(
2257            p.acquire(&stop).is_some() && p.acquire(&stop).is_none(),
2258            "a recycle by the capture leaves the held surface held"
2259        );
2260    }
2261
2262    /// A drain asks the encode thread, parked in `take`, to stop its encoder reading the held
2263    /// surface, which then returns, so the drain completes instead of waiting for a frame that
2264    /// cannot come.
2265    #[test]
2266    fn drain_for_resize_releases_the_held_surface() {
2267        let p = Arc::new(FramePool::new(3));
2268        let stop = AtomicBool::new(false);
2269        let a = p.acquire(&stop).unwrap();
2270        p.settle(&[], Some(a));
2271        let p2 = p.clone();
2272        let drain = thread::spawn(move || {
2273            let drained = p2.drain_for_resize(3, &AtomicBool::new(false));
2274            let b = p2.acquire(&AtomicBool::new(false)).unwrap();
2275            assert!(p2.publish(dummy(b), &AtomicBool::new(false)));
2276            drained
2277        });
2278        let mut released = 0;
2279        let f = p.take(|| released += 1).unwrap();
2280        assert!(drain.join().unwrap());
2281        assert_eq!(released, 1);
2282        p.recycle(f.idx);
2283    }
2284
2285    /// A surface is held and never recycled (as a dead encode thread would leave it): setting
2286    /// `stop` unblocks the bounded drain, which reports it did NOT fully drain instead of hanging.
2287    #[test]
2288    fn drain_for_resize_aborts_on_stop() {
2289        let p = FramePool::new(3);
2290        let stop = AtomicBool::new(false);
2291        let _held = p.acquire(&stop).unwrap();
2292        stop.store(true, Ordering::Relaxed);
2293        assert!(!p.drain_for_resize(3, &stop));
2294    }
2295
2296    /// A W1 -> W2 -> W1 resize flap where the encoder never takes the W2 frame:
2297    /// `drain_for_resize` reclaims the un-taken frame from the slot, the surfaces recreate twice, and
2298    /// the next taken frame lands back on the ORIGINAL dimensions — so the bumped generation is the
2299    /// only rebuild signal that invalidates encoder state keyed to the reused surface addresses.
2300    #[test]
2301    fn resize_flap_reclaim_changes_generation_at_identical_dims() {
2302        let p = FramePool::new(3);
2303        let stop = AtomicBool::new(false);
2304        let a = p.acquire(&stop).unwrap();
2305        assert!(p.publish(
2306            RawFrame {
2307                generation: p.generation(),
2308                width: 1920,
2309                height: 1080,
2310                ..dummy(a)
2311            },
2312            &stop
2313        ));
2314        let f = p.take(|| {}).unwrap();
2315        let (last_gen, last_dims) = (f.generation, (f.width, f.height));
2316        p.recycle(f.idx);
2317        assert!(p.drain_for_resize(3, &stop));
2318        p.bump_generation();
2319        let b = p.acquire(&stop).unwrap();
2320        assert!(p.publish(
2321            RawFrame {
2322                generation: p.generation(),
2323                width: 2560,
2324                height: 1600,
2325                ..dummy(b)
2326            },
2327            &stop
2328        ));
2329        assert!(p.drain_for_resize(3, &stop));
2330        p.bump_generation();
2331        let c = p.acquire(&stop).unwrap();
2332        assert!(p.publish(
2333            RawFrame {
2334                generation: p.generation(),
2335                width: 1920,
2336                height: 1080,
2337                ..dummy(c)
2338            },
2339            &stop
2340        ));
2341        let g = p.take(|| {}).unwrap();
2342        assert_eq!(
2343            (g.width, g.height),
2344            last_dims,
2345            "flap lands on identical dimensions"
2346        );
2347        assert_eq!(g.generation, 2);
2348        assert_ne!(
2349            g.generation, last_gen,
2350            "generation is the only rebuild signal"
2351        );
2352    }
2353
2354    /// Recreating the surfaces bumps the generation, and frames published afterwards carry the
2355    /// new value — the signal the encode thread uses to trigger a rebuild at identical dimensions.
2356    #[test]
2357    fn generation_bumps_on_recreate_and_rides_published_frames() {
2358        let p = FramePool::new(3);
2359        let stop = AtomicBool::new(false);
2360        assert_eq!(p.generation(), 0);
2361        let a = p.acquire(&stop).unwrap();
2362        assert!(p.publish(
2363            RawFrame {
2364                generation: p.generation(),
2365                ..dummy(a)
2366            },
2367            &stop
2368        ));
2369        let f = p.take(|| {}).unwrap();
2370        assert_eq!(f.generation, 0);
2371        p.recycle(f.idx);
2372        p.bump_generation();
2373        let b = p.acquire(&stop).unwrap();
2374        assert!(p.publish(
2375            RawFrame {
2376                generation: p.generation(),
2377                ..dummy(b)
2378            },
2379            &stop
2380        ));
2381        assert_eq!(p.take(|| {}).unwrap().generation, 1);
2382    }
2383}
2384
2385#[cfg(test)]
2386mod cursor_tests {
2387    use super::*;
2388
2389    /// `cursor_image_origin` subtracts both the hotspot and the capture-region offset from the
2390    /// reported pointer position, and goes negative when the hotspot sits near the frame origin (the
2391    /// result is clipped when drawn).
2392    #[test]
2393    fn origin_subtracts_hotspot_and_capture_offset() {
2394        assert_eq!(cursor_image_origin(100, 80, 4, 6, 0, 0), (96, 74));
2395        assert_eq!(cursor_image_origin(100, 80, 4, 6, 10, 20), (86, 54));
2396        assert_eq!(cursor_image_origin(1, 1, 8, 8, 0, 0), (-7, -7));
2397    }
2398
2399    /// `overlay_cursor` blits at the hotspot-offset origin, not the raw pointer position: a
2400    /// 2x2 cursor with hotspot (1,1) at pointer (4,4) lands at (3,3)..(4,4), leaving the un-offset
2401    /// (hotspot) corner untouched.
2402    #[test]
2403    fn overlay_blits_at_hotspot_offset_origin() {
2404        let stride = 8 * 4;
2405        let mut frame = vec![0u8; stride * 8];
2406        let pixels = [0xFFFF_FFFFu32; 4];
2407        let (ox, oy) = cursor_image_origin(4, 4, 1, 1, 0, 0);
2408        overlay_cursor(&mut frame, stride, 8, 8, 2, 2, &pixels, ox, oy);
2409        let px = |x: usize, y: usize| frame[y * stride + x * 4];
2410        assert_eq!(px(3, 3), 255);
2411        assert_eq!(px(4, 4), 255);
2412        assert_eq!(px(2, 2), 0);
2413        assert_eq!(px(5, 5), 0, "no pixel at the un-offset (hotspot) corner");
2414    }
2415
2416    #[test]
2417    fn offset_clamps_into_root() {
2418        assert_eq!(clamp_offset(100, 1920), 100);
2419        assert_eq!(clamp_offset(-50, 1920), 0);
2420        // Past-end offsets keep one root pixel instead of failing forever.
2421        assert_eq!(clamp_offset(100000, 1920), 1919);
2422        // A protocol-max root still maps into i16 X coordinates.
2423        assert_eq!(clamp_offset(40000, u16::MAX), 32766);
2424    }
2425
2426    #[test]
2427    fn resolve_dims_survives_past_end_offset() {
2428        let s = CaptureSettings {
2429            capture_x: 100000,
2430            capture_y: 100000,
2431            width: 1920,
2432            height: 1080,
2433            codec: crate::encoders::Codec::H264,
2434            ..Default::default()
2435        };
2436        let (w, h) = resolve_dims(1920, 1080, &s);
2437        assert!(
2438            w >= 2 && h >= 2,
2439            "a past-end offset now degrades to a small grab, not death"
2440        );
2441        assert_eq!(w % 2, 0);
2442        assert_eq!(h % 2, 0);
2443    }
2444
2445    /// `overlay_cursor` treats XFixes pixels as premultiplied: half-alpha at half
2446    /// intensity stays at that intensity over black (the straight-alpha formula
2447    /// would multiply alpha twice and darken it).
2448    #[test]
2449    fn overlay_blends_premultiplied_pixels() {
2450        let stride = 4 * 4;
2451        let mut frame = vec![0u8; stride * 4];
2452        // a=128, g=128 (premultiplied half-intensity), b=64.
2453        let pixels = [0x8000_8040u32; 4];
2454        overlay_cursor(&mut frame, stride, 4, 4, 2, 2, &pixels, 0, 0);
2455        assert_eq!(frame[0], 64, "blue channel: dst = 64 over black");
2456        assert_eq!(
2457            frame[1], 128,
2458            "premultiplied half-alpha stays 128, not darkened to 64"
2459        );
2460    }
2461
2462    /// `overlay_cursor` clips a negative origin at the frame edge: with the origin at (-1,-1)
2463    /// only the in-frame quadrant is written, landing the cursor's bottom-right pixel at (0,0).
2464    #[test]
2465    fn overlay_clips_negative_origin_at_frame_edge() {
2466        let stride = 4 * 4;
2467        let mut frame = vec![0u8; stride * 4];
2468        let pixels = [0xFFFF_FFFFu32; 4];
2469        let (ox, oy) = cursor_image_origin(0, 0, 1, 1, 0, 0);
2470        overlay_cursor(&mut frame, stride, 4, 4, 2, 2, &pixels, ox, oy);
2471        assert_eq!(frame[0], 255);
2472        assert!(
2473            frame[4..].iter().all(|&b| b == 0),
2474            "no writes outside (0,0)"
2475        );
2476    }
2477}
2478
2479#[cfg(test)]
2480mod damage_tests {
2481    //! The `x11_damage` tests need an X server of their own, quiet enough that nothing else
2482    //! repaints it: `DISPLAY=:N cargo test x11_damage -- --ignored --nocapture`; the
2483    //! `x11_framebuffer` ones one that shares its framebuffer, as the images' Xvfb does.
2484
2485    use super::*;
2486    use crate::pipeline::Damage as Changed;
2487    use x11rb::protocol::xproto::CreateGCAux;
2488
2489    const PERIOD: Duration = Duration::from_millis(400);
2490
2491    fn fill(color: u32, x: i16, y: i16, width: u16, height: u16) {
2492        Painter::connect().fill(color, x, y, width, height);
2493    }
2494
2495    /// A client of its own that paints the root, connected ahead of when it paints, so a paint
2496    /// timed within a period is not held up by the connection's setup.
2497    struct Painter {
2498        conn: RustConnection,
2499        root: u32,
2500    }
2501
2502    impl Painter {
2503        fn connect() -> Self {
2504            let (conn, screen) = x11rb::connect(None).expect("connect");
2505            let root = conn.setup().roots[screen].root;
2506            Self { conn, root }
2507        }
2508
2509        /// Paints and waits for the server to have done it: a request only flushed may be
2510        /// carried out after the test has moved on, and land in the next one.
2511        fn fill(&self, color: u32, x: i16, y: i16, width: u16, height: u16) {
2512            let gc = self.conn.generate_id().expect("id");
2513            self.conn
2514                .create_gc(gc, self.root, &CreateGCAux::new().foreground(color))
2515                .expect("gc");
2516            self.conn
2517                .poly_fill_rectangle(
2518                    self.root,
2519                    gc,
2520                    &[Rectangle {
2521                        x,
2522                        y,
2523                        width,
2524                        height,
2525                    }],
2526                )
2527                .expect("fill");
2528            self.conn.free_gc(gc).expect("free");
2529            self.conn
2530                .get_input_focus()
2531                .expect("sync")
2532                .reply()
2533                .expect("sync");
2534        }
2535    }
2536
2537    /// The root's damaged rectangles read in the captured area's own coordinates, clipped to it.
2538    #[test]
2539    fn the_root_damage_reads_as_the_captured_share() {
2540        let caret = [Rectangle {
2541            x: 110,
2542            y: 60,
2543            width: 2,
2544            height: 20,
2545        }];
2546        assert_eq!(
2547            captured_damage(&caret, 100, 50, 200, 100),
2548            Changed::Area(0.002)
2549        );
2550        assert_eq!(
2551            captured_damage(&caret, 120, 50, 200, 100),
2552            Changed::None,
2553            "left of the captured area"
2554        );
2555        let wide = [Rectangle {
2556            x: 0,
2557            y: 0,
2558            width: 1000,
2559            height: 1000,
2560        }];
2561        assert_eq!(
2562            captured_damage(&wide, 100, 50, 200, 100),
2563            Changed::Area(1.0)
2564        );
2565        assert_eq!(captured_damage(&[], 0, 0, 200, 100), Changed::None);
2566    }
2567
2568    /// A caret-sized repaint reads as that small a share of the screen, and one outside the
2569    /// captured area as none of it.
2570    #[test]
2571    #[ignore]
2572    fn x11_damage_says_where_the_root_changed() {
2573        let (conn, screen) = x11rb::connect(None).expect("connect");
2574        let root = conn.setup().roots[screen].root;
2575        let (w, h) = (
2576            i32::from(conn.setup().roots[screen].width_in_pixels),
2577            i32::from(conn.setup().roots[screen].height_in_pixels),
2578        );
2579        let damage = RootDamage::create(&conn, root).expect("the server carries DAMAGE");
2580        damage.clear(&conn);
2581        assert!(
2582            damage.take(&conn).is_some_and(|rects| rects.is_empty()),
2583            "nothing reported yet"
2584        );
2585        let mut pace = FramePace::windowed();
2586        pace.ticked(TickTrigger::Timer, PERIOD, Instant::now(), false);
2587        fill(0x00ff_ffff, 300, 200, 2, 20);
2588        assert_eq!(
2589            wait_for_frame(&conn, Some(&damage), &pace, PERIOD),
2590            TickTrigger::Damage
2591        );
2592        let asked = Instant::now();
2593        let rects = damage.take(&conn).expect("the server says where");
2594        let took = asked.elapsed();
2595        let caret = captured_damage(&rects, 0, 0, w, h);
2596        assert_eq!(
2597            caret,
2598            Changed::Area((40.0 / f64::from(w * h)) as f32),
2599            "{rects:?}"
2600        );
2601        assert_eq!(
2602            captured_damage(&rects, 400, 0, w - 400, h),
2603            Changed::None,
2604            "right of the caret"
2605        );
2606        println!("a 2x20 caret on a {w}x{h} root reads as {caret:?}, read in {took:?}");
2607    }
2608
2609    /// A change reported after the wait gave up, before the tick takes the report, is the
2610    /// tick's: taken and called clean, it would be cleared and never sent.
2611    #[test]
2612    #[ignore]
2613    fn x11_damage_a_change_after_the_wait_is_the_ticks() {
2614        let (conn, screen) = x11rb::connect(None).expect("connect");
2615        let root = conn.setup().roots[screen].root;
2616        let damage = RootDamage::create(&conn, root).expect("the server carries DAMAGE");
2617        let mut pace = FramePace::windowed();
2618        pace.ticked(TickTrigger::Timer, PERIOD, Instant::now(), false);
2619        damage.clear(&conn);
2620        let trigger = wait_for_frame(&conn, Some(&damage), &pace, PERIOD);
2621        assert_eq!(trigger, TickTrigger::Timer, "a quiet screen is the timer's");
2622        fill(0x0040_8020, 10, 10, 8, 8);
2623        let (dirty, rects) = take_tick(&damage, &conn, trigger);
2624        assert!(dirty, "the late change is the tick's: {rects:?}");
2625        assert!(
2626            rects.is_some_and(|rects| !rects.is_empty()),
2627            "and says where"
2628        );
2629        let (dirty, rects) = take_tick(&damage, &conn, TickTrigger::Timer);
2630        assert!(!dirty, "taken once: {rects:?}");
2631    }
2632
2633    /// A change right after a frame is published as it lands, not held back half a period: the
2634    /// frame it would have waited for is the window's, and no change had taken it.
2635    #[test]
2636    #[ignore]
2637    fn x11_damage_publishes_a_change_right_after_a_frame() {
2638        let (conn, screen) = x11rb::connect(None).expect("connect");
2639        let root = conn.setup().roots[screen].root;
2640        let damage = RootDamage::create(&conn, root).expect("the server carries DAMAGE");
2641        let mut pace = FramePace::windowed();
2642        pace.record_tick(TickTrigger::Timer, PERIOD, Instant::now(), true);
2643        damage.clear(&conn);
2644        let brush = Painter::connect();
2645        let painter = thread::spawn(move || {
2646            thread::sleep(PERIOD.mul_f64(0.1));
2647            brush.fill(0x0060_2040, 0, 0, 64, 64);
2648        });
2649        let start = Instant::now();
2650        let pulled = wait_for_frame(&conn, Some(&damage), &pace, PERIOD);
2651        let waited = start.elapsed();
2652        painter.join().expect("painter");
2653        assert_eq!(pulled, TickTrigger::Damage);
2654        assert!(
2655            waited < PERIOD.mul_f64(0.35),
2656            "a change a tenth of a period after a frame was published after {waited:?}"
2657        );
2658    }
2659
2660    /// A screen that changes mid-period publishes the change then, while a screen that does not
2661    /// keeps the cadence: the latency of a change is its own, not the frame rate's.
2662    #[test]
2663    #[ignore]
2664    fn x11_damage_publishes_a_change_before_the_next_tick() {
2665        let (conn, screen) = x11rb::connect(None).expect("connect");
2666        let root = conn.setup().roots[screen].root;
2667        let damage = RootDamage::create(&conn, root).expect("the server carries DAMAGE");
2668        let mut pace = FramePace::windowed();
2669
2670        pace.ticked(TickTrigger::Timer, PERIOD, Instant::now(), false);
2671        damage.clear(&conn);
2672        let start = Instant::now();
2673        let quiet = wait_for_frame(&conn, Some(&damage), &pace, PERIOD);
2674        let waited = start.elapsed();
2675        assert_eq!(
2676            quiet,
2677            TickTrigger::Timer,
2678            "a quiet screen is the timer's to pace"
2679        );
2680        assert!(
2681            waited >= PERIOD.mul_f64(0.9),
2682            "the timer's own tick came after {waited:?}"
2683        );
2684
2685        pace.ticked(TickTrigger::Timer, PERIOD, Instant::now(), false);
2686        damage.clear(&conn);
2687        let brush = Painter::connect();
2688        let painter = thread::spawn(move || {
2689            thread::sleep(PERIOD.mul_f64(0.6));
2690            brush.fill(0x0020_4060, 0, 0, 64, 64);
2691        });
2692        let start = Instant::now();
2693        let pulled = wait_for_frame(&conn, Some(&damage), &pace, PERIOD);
2694        let waited = start.elapsed();
2695        painter.join().expect("painter");
2696        assert_eq!(
2697            pulled,
2698            TickTrigger::Damage,
2699            "the repaint is what asked for the frame"
2700        );
2701        assert!(
2702            waited < PERIOD.mul_f64(0.9),
2703            "the repaint was published after {waited:?}"
2704        );
2705        println!(
2706            "repaint at 0.6 of a period published after {:.0}ms",
2707            waited.as_secs_f64() * 1000.0
2708        );
2709    }
2710
2711    /// The same, read off a shared framebuffer's watch: a change ends the wait without a round
2712    /// trip. Needs the images' Xvfb, which shares its framebuffer.
2713    #[test]
2714    #[ignore]
2715    fn x11_framebuffer_publishes_a_change_before_the_next_tick() {
2716        let (conn, screen) = x11rb::connect(None).expect("connect");
2717        let root = conn.setup().roots[screen].root;
2718        let Opened::Watch(fb) = SharedFramebuffer::open(&conn, root) else {
2719            panic!("the server shares no framebuffer");
2720        };
2721        let mut pace = FramePace::windowed();
2722        let mut grab = FramebufferGrab::new(fb, 1);
2723        let mut frame = vec![0u8; grab.fb.width as usize * grab.fb.height as usize * 4];
2724        let region = (0, 0, grab.fb.width, grab.fb.height);
2725        assert!(grab.read(0, &mut frame, grab.fb.width as usize * 4, region));
2726
2727        pace.ticked(TickTrigger::Timer, PERIOD, Instant::now(), false);
2728        let start = Instant::now();
2729        let quiet = wait_for_framebuffer(&grab.fb, &pace, PERIOD);
2730        assert_eq!(
2731            quiet,
2732            TickTrigger::Timer,
2733            "a quiet screen is the timer's to pace"
2734        );
2735        assert!(start.elapsed() >= PERIOD.mul_f64(0.9));
2736
2737        pace.ticked(TickTrigger::Timer, PERIOD, Instant::now(), false);
2738        let brush = Painter::connect();
2739        let painter = thread::spawn(move || {
2740            thread::sleep(PERIOD.mul_f64(0.6));
2741            brush.fill(0x0020_4060, 0, 0, 64, 64);
2742        });
2743        let start = Instant::now();
2744        let pulled = wait_for_framebuffer(&grab.fb, &pace, PERIOD);
2745        let waited = start.elapsed();
2746        painter.join().expect("painter");
2747        assert_eq!(
2748            pulled,
2749            TickTrigger::Damage,
2750            "the repaint is what asked for the frame"
2751        );
2752        assert!(
2753            waited < PERIOD.mul_f64(0.9),
2754            "the repaint was published after {waited:?}"
2755        );
2756        assert!(grab.read(0, &mut frame, grab.fb.width as usize * 4, region));
2757        assert_eq!(
2758            &frame[..4],
2759            &0x0020_4060u32.to_le_bytes(),
2760            "and the frame read after it holds it"
2761        );
2762        println!(
2763            "repaint at 0.6 of a period published after {:.2}ms",
2764            waited.as_secs_f64() * 1000.0
2765        );
2766        grab.fb.close(&conn);
2767    }
2768
2769    /// Over a server that shares its framebuffer, a capture reads the screen in place, says
2770    /// so, and a repaint reaches the frames it sends.
2771    #[test]
2772    #[ignore]
2773    fn x11_framebuffer_capture_reads_the_screen_in_place() {
2774        use crate::encoders::codec::{Codec, VIDEO_HEADER_LEN};
2775        use crate::webcam::decode::{Codec as DecCodec, VideoDecoder};
2776        use crate::x11::gpu_test_support::{decoded_mean, painted_ycbcr};
2777        let (conn, screen) = x11rb::connect(None).expect("connect");
2778        let (width, height) = (
2779            conn.setup().roots[screen].width_in_pixels,
2780            conn.setup().roots[screen].height_in_pixels,
2781        );
2782        let settings = CaptureSettings {
2783            codec: Codec::H264,
2784            use_cpu: true,
2785            video_streaming_mode: true,
2786            target_fps: 30.0,
2787            capture_cursor: true,
2788            ..Default::default()
2789        };
2790        let controls = Arc::new(Controls::new(&settings));
2791        let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
2792        let (tid_tx, _tid_rx) = std::sync::mpsc::channel();
2793        let capture = {
2794            let controls = controls.clone();
2795            thread::spawn(move || {
2796                run_capture(settings, controls, tid_tx, move |stripes| {
2797                    for s in stripes {
2798                        let _ = tx.send(s.data.to_vec());
2799                    }
2800                })
2801            })
2802        };
2803        thread::sleep(Duration::from_millis(500));
2804        const PAINTED: (u8, u8, u8) = (0xc0, 0x40, 0x20);
2805        fill(0x00c0_4020, 0, 0, width, height);
2806        thread::sleep(Duration::from_millis(500));
2807        controls.stop.store(true, Ordering::Relaxed);
2808        capture.join().expect("capture").expect("capture ran");
2809        let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
2810        let mut mean = [0.0; 3];
2811        for pkt in rx.try_iter() {
2812            mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
2813        }
2814        let info = controls.report.info();
2815        assert_eq!(info.capture, "Framebuffer", "{}", info.capture_reason);
2816        let want = painted_ycbcr(PAINTED);
2817        for (got, want) in mean.iter().zip(want) {
2818            assert!(
2819                (got - want).abs() < 2.0,
2820                "decoded {mean:?}, painted {want:?}"
2821            );
2822        }
2823    }
2824
2825    /// A cursor that changes its image without moving reaches the frames sent, though no
2826    /// rectangle moved and, over a shared framebuffer, no hash is taken: each change of the
2827    /// root's cursor to the same arrow in another color, on a still screen, sends a frame.
2828    #[test]
2829    #[ignore]
2830    fn x11_framebuffer_publishes_a_cursor_that_changes_its_image() {
2831        use crate::encoders::codec::Codec;
2832        use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
2833        const XC_LEFT_PTR: u16 = 68;
2834        let (conn, screen) = x11rb::connect(None).expect("connect");
2835        let root = conn.setup().roots[screen].root;
2836        let (width, height) = (
2837            conn.setup().roots[screen].width_in_pixels,
2838            conn.setup().roots[screen].height_in_pixels,
2839        );
2840        let font = conn.generate_id().expect("id");
2841        conn.open_font(font, b"cursor").expect("cursor font");
2842        let arrow = |red: u16| {
2843            let cursor = conn.generate_id().expect("id");
2844            conn.create_glyph_cursor(
2845                cursor,
2846                font,
2847                font,
2848                XC_LEFT_PTR,
2849                XC_LEFT_PTR + 1,
2850                red,
2851                0,
2852                0,
2853                0xffff,
2854                0xffff,
2855                0xffff,
2856            )
2857            .expect("cursor");
2858            cursor
2859        };
2860        let shapes = [arrow(0xffff), arrow(0)];
2861        let settings = CaptureSettings {
2862            codec: Codec::H264,
2863            use_cpu: true,
2864            target_fps: 30.0,
2865            width: i32::from(width),
2866            height: i32::from(height),
2867            capture_cursor: true,
2868            use_paint_over_quality: false,
2869            ..Default::default()
2870        };
2871        let controls = Arc::new(Controls::new(&settings));
2872        let (tx, rx) = std::sync::mpsc::channel::<()>();
2873        let (tid_tx, _tid_rx) = std::sync::mpsc::channel();
2874        let capture = {
2875            let controls = controls.clone();
2876            thread::spawn(move || {
2877                run_capture(settings, controls, tid_tx, move |stripes| {
2878                    if !stripes.is_empty() {
2879                        let _ = tx.send(());
2880                    }
2881                })
2882            })
2883        };
2884        thread::sleep(Duration::from_millis(800));
2885        while rx.try_recv().is_ok() {}
2886        let mut sent = 0;
2887        for &cursor in shapes.iter().cycle().take(4) {
2888            conn.change_window_attributes(root, &ChangeWindowAttributesAux::new().cursor(cursor))
2889                .expect("cursor")
2890                .check()
2891                .expect("set cursor");
2892            thread::sleep(Duration::from_millis(300));
2893            sent += usize::from(rx.try_iter().count() > 0);
2894        }
2895        controls.stop.store(true, Ordering::Relaxed);
2896        capture.join().expect("capture").expect("capture ran");
2897        let info = controls.report.info();
2898        assert_eq!(info.capture, "Framebuffer", "{}", info.capture_reason);
2899        assert_eq!(sent, 4, "a frame for each change of the cursor's image");
2900    }
2901
2902    /// CPU seconds a process has run, user and system, read from `/proc/<pid>/stat`.
2903    fn cpu_seconds(pid: &str) -> f64 {
2904        let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).expect("stat");
2905        let fields: Vec<&str> = stat
2906            .rsplit(')')
2907            .next()
2908            .unwrap()
2909            .split_whitespace()
2910            .collect();
2911        let ticks: f64 = fields[11].parse::<f64>().unwrap() + fields[12].parse::<f64>().unwrap();
2912        ticks / unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64
2913    }
2914
2915    /// What a capture costs the X server and the capture process: the root of `$DISPLAY` at
2916    /// its own size, x264, a box of `PF_BENCH_BOX` pixels (0 for the whole screen, unset for
2917    /// none) repainted every frame at 60 fps for five seconds. `PF_BENCH_XPID` names the
2918    /// server's pid. Run once against a server that shares its framebuffer and once against
2919    /// one that does not, to compare the read in place with XShm. Prints rather than asserts.
2920    #[test]
2921    #[ignore]
2922    fn x11_framebuffer_bench_capture_cost() {
2923        use crate::encoders::codec::Codec;
2924        let xpid = std::env::var("PF_BENCH_XPID").expect("PF_BENCH_XPID");
2925        let boxed: Option<u16> = std::env::var("PF_BENCH_BOX")
2926            .ok()
2927            .and_then(|v| v.parse().ok());
2928        let (conn, screen) = x11rb::connect(None).expect("connect");
2929        let (width, height) = (
2930            conn.setup().roots[screen].width_in_pixels,
2931            conn.setup().roots[screen].height_in_pixels,
2932        );
2933        let settings = CaptureSettings {
2934            codec: Codec::H264,
2935            use_cpu: true,
2936            target_fps: 60.0,
2937            width: i32::from(width),
2938            height: i32::from(height),
2939            capture_cursor: true,
2940            ..Default::default()
2941        };
2942        let controls = Arc::new(Controls::new(&settings));
2943        let frames = Arc::new(AtomicU64::new(0));
2944        let (tid_tx, _tid_rx) = std::sync::mpsc::channel();
2945        let capture = {
2946            let (controls, frames) = (controls.clone(), frames.clone());
2947            thread::spawn(move || {
2948                run_capture(settings, controls, tid_tx, move |stripes| {
2949                    if !stripes.is_empty() {
2950                        frames.fetch_add(1, Ordering::Relaxed);
2951                    }
2952                })
2953            })
2954        };
2955        thread::sleep(Duration::from_secs(2));
2956        let painter = Painter::connect();
2957        let (x0, s0, f0) = (
2958            cpu_seconds(&xpid),
2959            cpu_seconds("self"),
2960            frames.load(Ordering::Relaxed),
2961        );
2962        let start = Instant::now();
2963        let mut n = 0u32;
2964        while start.elapsed() < Duration::from_secs(5) {
2965            if let Some(size) = boxed {
2966                let color = 0x0010_2030u32.wrapping_mul(n + 1) & 0x00ff_ffff;
2967                match size {
2968                    0 => painter.fill(color, 0, 0, width, height),
2969                    _ => painter.fill(color, 64, 64, size, size),
2970                }
2971            }
2972            n += 1;
2973            thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
2974        }
2975        let secs = start.elapsed().as_secs_f64();
2976        let (x1, s1, f1) = (
2977            cpu_seconds(&xpid),
2978            cpu_seconds("self"),
2979            frames.load(Ordering::Relaxed),
2980        );
2981        controls.stop.store(true, Ordering::Relaxed);
2982        capture.join().expect("capture").expect("capture ran");
2983        println!(
2984            "{} {}x{} box {:?}: {:.1} fps painted, {:.1} fps sent, X server {:.3} cores, capture {:.3} cores",
2985            controls.report.info().capture,
2986            width,
2987            height,
2988            boxed,
2989            if boxed.is_some() {
2990                f64::from(n) / secs
2991            } else {
2992                0.0
2993            },
2994            (f1 - f0) as f64 / secs,
2995            (x1 - x0) / secs,
2996            (s1 - s0) / secs
2997        );
2998    }
2999
3000    /// How long a change takes from landing on the screen of `$DISPLAY` to leaving the capture
3001    /// as a frame: a box of `PF_BENCH_BOX` pixels (default 256, 0 for the whole screen) painted
3002    /// every 100 ms on an otherwise still screen, 60 fps, 60 paints, through the full-frame
3003    /// session of `PF_BENCH_CODEC` (h264 by default) on the software encoder, or on the
3004    /// hardware one where `PF_BENCH_CPU` is 0. Prints the median and the 99th percentile rather
3005    /// than asserting.
3006    #[test]
3007    #[ignore]
3008    fn x11_framebuffer_bench_change_latency() {
3009        use crate::encoders::codec::Codec;
3010        let size: u16 = std::env::var("PF_BENCH_BOX")
3011            .ok()
3012            .and_then(|v| v.parse().ok())
3013            .unwrap_or(256);
3014        let (conn, screen) = x11rb::connect(None).expect("connect");
3015        let (width, height) = (
3016            conn.setup().roots[screen].width_in_pixels,
3017            conn.setup().roots[screen].height_in_pixels,
3018        );
3019        let codec = std::env::var("PF_BENCH_CODEC")
3020            .ok()
3021            .and_then(|c| Codec::parse(&c))
3022            .unwrap_or(Codec::H264);
3023        let settings = CaptureSettings {
3024            codec,
3025            use_cpu: std::env::var("PF_BENCH_CPU").map_or(true, |v| v != "0"),
3026            target_fps: 60.0,
3027            width: i32::from(width),
3028            height: i32::from(height),
3029            use_paint_over_quality: false,
3030            ..Default::default()
3031        };
3032        let controls = Arc::new(Controls::new(&settings));
3033        let (tx, rx) = std::sync::mpsc::channel::<Instant>();
3034        let (tid_tx, _tid_rx) = std::sync::mpsc::channel();
3035        let capture = {
3036            let controls = controls.clone();
3037            thread::spawn(move || {
3038                run_capture(settings, controls, tid_tx, move |stripes| {
3039                    if !stripes.is_empty() {
3040                        let _ = tx.send(Instant::now());
3041                    }
3042                })
3043            })
3044        };
3045        thread::sleep(Duration::from_secs(2));
3046        while rx.try_recv().is_ok() {}
3047        let painter = Painter::connect();
3048        let mut waits = Vec::new();
3049        for n in 0..60u32 {
3050            let color = 0x0010_2030u32.wrapping_mul(n + 1) & 0x00ff_ffff;
3051            match size {
3052                0 => painter.fill(color, 0, 0, width, height),
3053                _ => painter.fill(color, 64, 64, size, size),
3054            }
3055            let painted = Instant::now();
3056            if let Ok(sent) = rx.recv_timeout(Duration::from_millis(100)) {
3057                waits.push(sent.saturating_duration_since(painted).as_secs_f64() * 1000.0);
3058            }
3059            thread::sleep(Duration::from_millis(100));
3060            while rx.try_recv().is_ok() {}
3061        }
3062        controls.stop.store(true, Ordering::Relaxed);
3063        capture.join().expect("capture").expect("capture ran");
3064        waits.sort_by(f64::total_cmp);
3065        let at = |q: f64| waits[((waits.len() - 1) as f64 * q).round() as usize];
3066        println!(
3067            "{} {}x{} box {size}: change to frame p50 {:.2} ms, p99 {:.2} ms over {} changes",
3068            controls.report.info().capture,
3069            width,
3070            height,
3071            at(0.5),
3072            at(0.99),
3073            waits.len()
3074        );
3075    }
3076
3077    /// Noise in BGRX for a `width` x `height` area, from `seed`.
3078    fn noise(width: usize, height: usize, mut seed: u32) -> Vec<u8> {
3079        (0..width * height)
3080            .flat_map(|_| {
3081                seed ^= seed << 13;
3082                seed ^= seed >> 17;
3083                seed ^= seed << 5;
3084                [seed as u8, (seed >> 8) as u8, (seed >> 16) as u8, 0]
3085            })
3086            .collect()
3087    }
3088
3089    /// Over a capture of `$DISPLAY` that keeps changing in one box, the still area around it is
3090    /// refined (`ledger`) from where the server says the screen changed: a texture around a box
3091    /// repainted with fresh noise at 60 fps, x264 at CRF 35 with the paint-over at 18, four
3092    /// seconds, the decoded area around the box measured against what `GetImage` returns.
3093    /// OpenH264, the H.264 encoder of a build without `gpl`, takes no quantizer per macroblock.
3094    #[cfg(feature = "gpl")]
3095    #[test]
3096    #[ignore]
3097    fn x11_ledger_refines_the_still_area_beside_motion() {
3098        refines_the_still_area_beside_motion(true);
3099    }
3100
3101    /// The same through the hardware encoder of the capture's GPU, on a server whose screen
3102    /// lives on it (DRI3, NVENC's QP delta map).
3103    #[test]
3104    #[ignore]
3105    fn gpu_dri3_ledger_refines_the_still_area_beside_motion() {
3106        refines_the_still_area_beside_motion(false);
3107    }
3108
3109    /// `x11_ledger_refines_the_still_area_beside_motion` in software (`use_cpu`) or not.
3110    fn refines_the_still_area_beside_motion(use_cpu: bool) {
3111        use crate::cleanup_bench::{psnr_outside, source_luma};
3112        use crate::encoders::codec::{Codec, VIDEO_HEADER_LEN};
3113        use crate::webcam::decode::{Codec as DecCodec, Decoder, VideoDecoder};
3114        use x11rb::protocol::xproto::ImageFormat;
3115        let (w, h) = (512u16, 256u16);
3116        let painter = Painter::connect();
3117        let gc = painter.conn.generate_id().expect("id");
3118        painter
3119            .conn
3120            .create_gc(gc, painter.root, &CreateGCAux::new())
3121            .expect("gc");
3122        let put = |pixels: &[u8], x: i16, y: i16, pw: u16, ph: u16| {
3123            painter
3124                .conn
3125                .put_image(
3126                    ImageFormat::Z_PIXMAP,
3127                    painter.root,
3128                    gc,
3129                    pw,
3130                    ph,
3131                    x,
3132                    y,
3133                    0,
3134                    24,
3135                    pixels,
3136                )
3137                .expect("put")
3138                .check()
3139                .expect("put done");
3140        };
3141        put(&noise(w as usize, h as usize, 7), 0, 0, w, h);
3142        let settings = CaptureSettings {
3143            codec: Codec::H264,
3144            use_cpu,
3145            encode_node_index: if use_cpu {
3146                -1
3147            } else {
3148                crate::auto_render_node("true")
3149                    .and_then(|node| crate::render_node_index(&node))
3150                    .unwrap_or(0)
3151            },
3152            video_streaming_mode: false,
3153            video_cbr_mode: false,
3154            video_crf: 35,
3155            video_paintover_crf: 18,
3156            use_paint_over_quality: true,
3157            paint_over_trigger_frames: 15,
3158            target_fps: 60.0,
3159            width: i32::from(w),
3160            height: i32::from(h),
3161            capture_cursor: false,
3162            ..Default::default()
3163        };
3164        let controls = Arc::new(Controls::new(&settings));
3165        let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
3166        let (tid_tx, _tid_rx) = std::sync::mpsc::channel();
3167        let capture = {
3168            let controls = controls.clone();
3169            thread::spawn(move || {
3170                run_capture(settings, controls, tid_tx, move |stripes| {
3171                    for s in stripes {
3172                        let _ = tx.send(s.data.to_vec());
3173                    }
3174                })
3175            })
3176        };
3177        let area = (384usize, 128usize, 128usize, 128usize);
3178        let start = Instant::now();
3179        let mut n = 0u32;
3180        while start.elapsed() < Duration::from_secs(4) {
3181            put(&noise(128, 128, n + 1), 384, 128, 128, 128);
3182            n += 1;
3183            thread::sleep(Duration::from_millis(16));
3184        }
3185        controls.stop.store(true, Ordering::Relaxed);
3186        capture.join().expect("capture").expect("capture ran");
3187        let mut dec = VideoDecoder::new(DecCodec::H264).expect("decoder");
3188        let mut shown = vec![0u8; w as usize * h as usize];
3189        for pkt in rx.try_iter() {
3190            if dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode") {
3191                let pic = dec.frame().expect("picture");
3192                for r in 0..h as usize {
3193                    shown[r * w as usize..(r + 1) * w as usize]
3194                        .copy_from_slice(&pic.y[r * pic.y_stride..][..w as usize]);
3195                }
3196            }
3197        }
3198        let image = painter
3199            .conn
3200            .get_image(ImageFormat::Z_PIXMAP, painter.root, 0, 0, w, h, !0)
3201            .expect("get")
3202            .reply()
3203            .expect("image");
3204        let around = psnr_outside(
3205            &shown,
3206            &source_luma(&image.data, Codec::H264),
3207            w as usize,
3208            area,
3209        );
3210        let info = controls.report.info();
3211        println!("{}: around the box {around:.2} dB", info.capture);
3212        assert!(
3213            around > 40.0,
3214            "{}: the still area reached {around:.2} dB",
3215            info.capture
3216        );
3217    }
3218}
3219
3220#[cfg(test)]
3221mod vblank_tests {
3222    //! Needs an X server of its own: `DISPLAY=:N cargo test x11_vblank -- --ignored`.
3223
3224    use super::*;
3225
3226    fn capture_at(fps: f64) -> Arc<Controls> {
3227        Arc::new(Controls::new(&CaptureSettings {
3228            target_fps: fps,
3229            ..Default::default()
3230        }))
3231    }
3232
3233    /// The root window's `_FAKE_SCREEN_FPS` and `_FAKE_SCREEN_MILLIHZ`.
3234    fn published() -> (Option<u32>, Option<u32>) {
3235        (
3236            property(b"_FAKE_SCREEN_FPS"),
3237            property(b"_FAKE_SCREEN_MILLIHZ"),
3238        )
3239    }
3240
3241    fn property(name: &[u8]) -> Option<u32> {
3242        let (conn, screen) = x11rb::connect(None).expect("connect");
3243        let root = conn.setup().roots[screen].root;
3244        let atom = conn
3245            .intern_atom(false, name)
3246            .expect("intern")
3247            .reply()
3248            .expect("atom")
3249            .atom;
3250        let reply = conn
3251            .get_property(false, root, atom, AtomEnum::CARDINAL, 0, 1)
3252            .expect("get_property")
3253            .reply()
3254            .expect("reply");
3255        reply.value32().and_then(|mut v| v.next())
3256    }
3257
3258    /// The fastest running capture sets the rate, a change of rate follows, a fractional one to the
3259    /// millihertz, and the last capture to end takes the properties away.
3260    #[test]
3261    #[ignore]
3262    fn x11_vblank_rate_follows_the_fastest_capture() {
3263        let fast = capture_at(144.0);
3264        let slow = capture_at(90.0);
3265        let fast_claim = VblankClaim::new(&fast);
3266        let slow_claim = VblankClaim::new(&slow);
3267        assert_eq!(published(), (Some(144), Some(144_000)));
3268        drop(fast_claim);
3269        assert_eq!(published(), (Some(90), Some(90_000)));
3270        slow.fps_bits.store(119.6f64.to_bits(), Ordering::Relaxed);
3271        follow_frame_rate();
3272        assert_eq!(published(), (Some(120), Some(119_600)));
3273        slow.fps_bits
3274            .store((60000.0f64 / 1001.0).to_bits(), Ordering::Relaxed);
3275        follow_frame_rate();
3276        assert_eq!(published(), (Some(60), Some(59_940)));
3277        drop(slow_claim);
3278        assert_eq!(published(), (None, None));
3279    }
3280}