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: [`nvfbc`] on an NVIDIA X server has the driver composite the
16//! screen straight into video memory and registers that buffer with NVENC in place, and [`dri3`]
17//! on any server whose screen lives on the GPU has the server blit the root into dmabufs the
18//! hardware encoder reads in place. `run_capture` tries them in that order and falls back here
19//! otherwise.
20//!
21//! `run_capture` splits the work across two threads because grabbing the next frame and encoding
22//! the previous one have no reason to wait on each other: the caller's thread grabs frames and owns
23//! the x11rb connection and the pool of shm surfaces, while a spawned encode thread owns the
24//! [`pipeline::X11Pipeline`] (and thus the encoder) and runs the delivery callback, so the two overlap for
25//! throughput. Frames hand off through a bounded `FramePool` that carries only a raw pointer and
26//! geometry — never an X object, none of which is safe to share — so nothing X-related ever crosses
27//! the thread boundary and the encoder never has to touch X. Multi-instance safety for the encoders
28//! is handled inside them (e.g. the libx264 open/close lock); each capture owns its own private xcb
29//! connection, so there is no shared X state to serialize here.
30
31use std::cell::Cell;
32use std::os::unix::io::AsRawFd;
33use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering};
34use std::sync::mpsc::Sender;
35use std::sync::{Arc, Condvar, Mutex};
36use std::thread;
37use std::time::{Duration, Instant};
38
39use x11rb::connection::Connection;
40use x11rb::protocol::Event;
41use x11rb::protocol::damage::{ConnectionExt as DamageExt, Damage, ReportLevel};
42use x11rb::protocol::shm::ConnectionExt as ShmExt;
43use x11rb::protocol::xfixes::{ConnectionExt as XfixesExt, Region};
44use x11rb::protocol::xproto::{
45    Atom, AtomEnum, ConnectionExt as XprotoExt, ImageFormat, PropMode, Rectangle, Window,
46};
47use x11rb::rust_connection::RustConnection;
48use x11rb::wrapper::ConnectionExt as WrapperExt;
49
50use crate::pace::{FramePace, TickTrigger};
51
52use crate::RustCaptureSettings;
53use crate::encoders::overlay::blend_pixel_premultiplied;
54use crate::encoders::software::{EncodedStripe, FrameTiming};
55use crate::pipeline::X11Pipeline;
56use crate::recording_sink::RecordingSink;
57
58pub mod computer_use;
59pub mod cursor;
60pub mod dri3;
61pub mod nvfbc;
62
63/// Cross-thread controls for a running capture: a bag of atomics (plus two mutex-guarded
64/// payloads) the owning `ScreenCapture` pyclass flips from the Python thread and the capture thread
65/// reads at the top of each iteration.
66///
67/// It exists so Python never has to reach into the pipeline. The `X11Pipeline` and its encoder are
68/// only safe to touch from the capture and encode threads, so `request_idr` / rate / fps / region
69/// changes are posted here as atomic flags and applied later on the thread that owns the pipeline,
70/// rather than mutating encoder state across the thread boundary from Python.
71///
72/// 1. **Lifecycle**: `stop` ends the capture loop; `force_idr` requests an on-demand keyframe on the
73///    next processed frame; `reference_reports` are what clients said of frames, lost or held
74///    by every one of them, which the next processed frame applies first.
75/// 2. **Rate control** (gated by `rate_dirty`): `bitrate_kbps`, `vbv_mult_milli` (the VBV frame-time
76///    multiplier * 1000, held as an integer for atomics; `<= 0` selects the policy default), and
77///    `fps_bits` (the target fps as `f64` bits, so a fractional rate reaches the encoders exactly,
78///    re-read every frame for dynamic pacing and rate control). These
79///    atomics always hold the CURRENT values, so a pipeline rebuild can carry live rates forward.
80/// 3. **Tunables** (gated by `tunables_dirty`): one `LiveTunables` struct behind `tunables`, set
81///    rarely, carrying the per-frame quality knobs for the encode thread.
82/// 4. **Capture geometry**: `capture_cursor` toggles the cursor overlay (read on every grab);
83///    `region_dirty` guards `region` = `(x, y, w, h)` (w/h `<= 0` extends to the root edge), applied
84///    by the capture thread with the same drain/recreate machinery as auto-adjust.
85pub struct Controls {
86    pub stop: AtomicBool,
87    /// Set by the capture thread as it returns — after it has joined its encode thread and
88    /// so dropped the NVENC/CUDA session. The atexit sweep waits on this (bounded) before
89    /// letting the interpreter finalize, because that hardware-session drop racing process
90    /// exit segfaults, exactly as the Wayland branch fences with a Barrier.
91    pub finished: AtomicBool,
92    pub force_idr: AtomicBool,
93    pub reference_reports: Mutex<Vec<crate::encoders::reference::ReferenceReport>>,
94    pub rate_dirty: AtomicBool,
95    pub bitrate_kbps: AtomicI32,
96    pub vbv_mult_milli: AtomicI32,
97    pub fps_bits: AtomicU64,
98    pub tunables_dirty: AtomicBool,
99    pub tunables: Mutex<Option<crate::LiveTunables>>,
100    pub capture_cursor: AtomicBool,
101    /// The codec the pipeline streams, as `Codec::id`, once one has been built; the
102    /// selection ladder may have demoted the requested one. `u32::MAX` until then.
103    pub codec: AtomicU32,
104    pub region_dirty: AtomicBool,
105    pub region: Mutex<(i32, i32, i32, i32)>,
106    /// What this capture streams and how, for `ScreenCapture::stream_info`.
107    pub report: Arc<crate::report::StreamReport>,
108}
109
110impl Controls {
111    /// Seed the controls from the initial settings so the first loop iteration reads the
112    /// configured bitrate / VBV / fps / region / cursor state rather than defaults.
113    pub fn new(s: &RustCaptureSettings) -> Self {
114        Self {
115            stop: AtomicBool::new(false),
116            finished: AtomicBool::new(false),
117            force_idr: AtomicBool::new(false),
118            reference_reports: Mutex::new(Vec::new()),
119            rate_dirty: AtomicBool::new(false),
120            bitrate_kbps: AtomicI32::new(s.video_bitrate_kbps),
121            vbv_mult_milli: AtomicI32::new((s.video_vbv_multiplier * 1000.0).round() as i32),
122            fps_bits: AtomicU64::new(s.target_fps.max(1.0).to_bits()),
123            tunables_dirty: AtomicBool::new(false),
124            tunables: Mutex::new(None),
125            capture_cursor: AtomicBool::new(s.capture_cursor),
126            codec: AtomicU32::new(u32::MAX),
127            region_dirty: AtomicBool::new(false),
128            region: Mutex::new((s.capture_x, s.capture_y, s.width, s.height)),
129            report: crate::report::StreamReport::new("x11"),
130        }
131    }
132}
133
134/// A shared-memory image surface: a POSIX shm segment mapped into both this process and the
135/// X server, into which `shm_get_image` writes one BGRA frame.
136///
137/// Both ends map the same physical pages, which is the whole point: a full-screen grab then costs no
138/// per-frame copy across the X socket, because the server's blit lands directly in memory the
139/// capture thread already reads. The surface owns the server-side segment id (`shmseg`), the local
140/// mapping (`addr` / `size`), and the geometry (`width` / `height` / `stride`) needed to interpret
141/// the bytes.
142struct ShmSurface {
143    shmseg: u32,
144    addr: *mut u8,
145    size: usize,
146    width: u16,
147    height: u16,
148    stride: usize,
149}
150
151impl ShmSurface {
152    /// Allocate a shm segment of `width*height*4` bytes, attach it both locally and to the X
153    /// server, and hand back a surface both ends can read.
154    ///
155    /// The segment is created with `shmget(IPC_PRIVATE)` and mapped locally via `shmat`, then
156    /// attached on the server (`shm_attach`). That attach is confirmed with a round-trip `check()`
157    /// BEFORE the segment is marked `IPC_RMID`: deleting only after both ends hold a reference means
158    /// the kernel frees the segment once this process and the server have both detached, never while
159    /// either still needs it. Every failure path unwinds the partial state (local detach and/or
160    /// `IPC_RMID`) so a failed allocation leaks neither memory nor a server attachment; a zero-sized
161    /// request is rejected up front.
162    fn create(conn: &RustConnection, width: u16, height: u16) -> Result<Self, String> {
163        let stride = width as usize * 4;
164        let size = stride * height as usize;
165        if size == 0 {
166            return Err("zero-sized capture surface".into());
167        }
168        unsafe {
169            let shmid = libc::shmget(libc::IPC_PRIVATE, size, libc::IPC_CREAT | 0o600);
170            if shmid < 0 {
171                return Err("shmget failed".into());
172            }
173            let addr = libc::shmat(shmid, std::ptr::null(), 0);
174            if addr == (-1isize) as *mut libc::c_void {
175                libc::shmctl(shmid, libc::IPC_RMID, std::ptr::null_mut());
176                return Err("shmat failed".into());
177            }
178            let shmseg = match conn.generate_id() {
179                Ok(id) => id,
180                Err(e) => {
181                    libc::shmdt(addr);
182                    libc::shmctl(shmid, libc::IPC_RMID, std::ptr::null_mut());
183                    return Err(format!("generate_id: {e}"));
184                }
185            };
186            let attach = conn
187                .shm_attach(shmseg, shmid as u32, false)
188                .map_err(|e| format!("shm_attach: {e}"))
189                .and_then(|c| c.check().map_err(|e| format!("shm_attach check: {e}")));
190            libc::shmctl(shmid, libc::IPC_RMID, std::ptr::null_mut());
191            if let Err(e) = attach {
192                libc::shmdt(addr);
193                return Err(e);
194            }
195            Ok(Self {
196                shmseg,
197                addr: addr as *mut u8,
198                size,
199                width,
200                height,
201                stride,
202            })
203        }
204    }
205
206    /// Borrow the mapped segment as a mutable byte slice for the capture blit and overlays.
207    fn as_mut_slice(&mut self) -> &mut [u8] {
208        unsafe { std::slice::from_raw_parts_mut(self.addr, self.size) }
209    }
210
211    /// Borrow the mapped segment read-only, for the stability comparison.
212    fn as_slice(&self) -> &[u8] {
213        unsafe { std::slice::from_raw_parts(self.addr, self.size) }
214    }
215
216    /// Detach the segment from the X server and then locally, clearing the mapped pointer.
217    ///
218    /// This is a manual method rather than a `Drop` impl because releasing the server-side
219    /// attachment is an X protocol request that needs the live connection, which a `Drop` could not
220    /// reach; the owner of the connection must call it before the surface is dropped. The order —
221    /// server `shm_detach` first, then the local `shmdt` — completes the release of the segment that
222    /// was marked `IPC_RMID` at creation, so the kernel reclaims it once neither end holds it.
223    fn destroy(&mut self, conn: &RustConnection) {
224        let _ = conn.shm_detach(self.shmseg);
225        let _ = conn.flush();
226        unsafe {
227            libc::shmdt(self.addr as *mut libc::c_void);
228        }
229        self.addr = std::ptr::null_mut();
230    }
231}
232
233/// Reject a server whose root visual is not 32 bits per pixel.
234///
235/// Every `ShmSurface` is sized at `width * 4` bytes per row and the capture is read as packed
236/// BGRA, so a root of any other depth would be grabbed as garbage rather than failing. Checked
237/// on the initial connection and again on every reconnect, since a restarted server can come
238/// back with a different visual.
239fn require_32bpp(conn: &RustConnection, screen: usize) -> Result<(), String> {
240    let root_depth = conn.setup().roots[screen].root_depth;
241    let bpp = conn
242        .setup()
243        .pixmap_formats
244        .iter()
245        .find(|f| f.depth == root_depth)
246        .map(|f| f.bits_per_pixel)
247        .unwrap_or(32);
248    if bpp != 32 {
249        return Err(format!(
250            "unsupported root depth {root_depth} ({bpp} bpp); only 32-bpp BGRA is supported"
251        ));
252    }
253    Ok(())
254}
255
256/// Clamp a capture offset to the live root so a grab always has at least one root pixel in
257/// range: a past-end offset fails every `shm_get_image` with BadMatch and would burn out the
258/// failure budget with no chance of recovery. The upper clamp also keeps protocol-range roots
259/// (INT16 coordinates) from truncating a wider value through the `as i16` cast.
260fn clamp_offset(x: i32, root: u16) -> i16 {
261    let upper = (root as i32).saturating_sub(1).min(i16::MAX as i32 - 1);
262    x.max(0).min(upper) as i16
263}
264
265/// Resolve the capture dimensions `shm_get_image` will read, bounded so the region can never
266/// run past the live root from the (clamped) capture offset: with auto-adjust (or unset
267/// width/height `<= 0`) the capture tracks the full root minus the offset, otherwise the
268/// requested size is clamped to what is available from it. Saturating subtraction plus a final
269/// `u16` clamp (minimum 2) keep pathological settings from overflowing or collapsing to an
270/// unusable surface. Video codecs require even dimensions, so both are then rounded down to
271/// a multiple of two.
272fn resolve_dims(root_w: u16, root_h: u16, s: &RustCaptureSettings) -> (u16, u16) {
273    let cap_x = clamp_offset(s.capture_x, root_w) as i32;
274    let cap_y = clamp_offset(s.capture_y, root_h) as i32;
275    let avail_w = (root_w as i32).saturating_sub(cap_x).max(2);
276    let avail_h = (root_h as i32).saturating_sub(cap_y).max(2);
277    let mut w = if s.auto_adjust_screen_capture_size || s.width <= 0 {
278        avail_w
279    } else {
280        s.width.min(avail_w)
281    };
282    let mut h = if s.auto_adjust_screen_capture_size || s.height <= 0 {
283        avail_h
284    } else {
285        s.height.min(avail_h)
286    };
287    w = w.clamp(2, u16::MAX as i32);
288    h = h.clamp(2, u16::MAX as i32);
289    if s.codec.is_video() {
290        w &= !1;
291        h &= !1;
292    }
293    (w as u16, h as u16)
294}
295
296/// Rebuild the X channel + capture surfaces after a connection-level failure (Xorg restart,
297/// VT switch), refreshing the offset/dims state against the live root. The pool is drained
298/// first so the encode thread can never be reading a surface as it is destroyed, and its
299/// generation is bumped so the encoder rebuilds against the new layout. Every fallible step
300/// completes before the swap, and the old channel/surfaces are destroyed best-effort after
301/// it (their server is likely gone; the local detaches still run, and the kernel frees
302/// each old segment once nothing maps it).
303#[allow(clippy::too_many_arguments)]
304fn try_rebuild_channel(
305    conn: &mut RustConnection,
306    root: &mut u32,
307    rsettings: &RustCaptureSettings,
308    cap_x: &mut i16,
309    cap_y: &mut i16,
310    cap_w: &mut u16,
311    cap_h: &mut u16,
312    root_w: &mut u16,
313    root_h: &mut u16,
314    pool_n: usize,
315    pool: &FramePool,
316    surfaces: &mut Vec<ShmSurface>,
317    stop: &AtomicBool,
318) -> Result<(), String> {
319    let (new_conn, new_screen) =
320        x11rb::connect(None).map_err(|e| format!("X11 reconnect failed: {e}"))?;
321    require_32bpp(&new_conn, new_screen)?;
322    new_conn
323        .shm_query_version()
324        .map_err(|e| format!("shm_query_version: {e}"))?
325        .reply()
326        .map_err(|e| format!("XShm unavailable on reconnect: {e}"))?;
327    new_conn
328        .xfixes_query_version(5, 0)
329        .map_err(|e| format!("xfixes_query_version: {e}"))?
330        .reply()
331        .map_err(|e| format!("XFixes unavailable on reconnect: {e}"))?;
332    let new_root = new_conn.setup().roots[new_screen].root;
333    let geo = new_conn
334        .get_geometry(new_root)
335        .map_err(|e| format!("get_geometry: {e}"))?
336        .reply()
337        .map_err(|e| format!("get_geometry reply on reconnect: {e}"))?;
338    if !pool.drain_for_resize(pool_n, stop) {
339        return Err("pool drain interrupted by stop during reconnect".to_string());
340    }
341    let (fw, fh) = resolve_dims(geo.width, geo.height, rsettings);
342    let mut fresh: Vec<ShmSurface> = Vec::with_capacity(pool_n);
343    for _ in 0..pool_n {
344        match ShmSurface::create(&new_conn, fw, fh) {
345            Ok(s) => fresh.push(s),
346            Err(e) => {
347                for s in fresh.iter_mut() {
348                    s.destroy(&new_conn);
349                }
350                return Err(e);
351            }
352        }
353    }
354    for s in surfaces.iter_mut() {
355        s.destroy(conn);
356    }
357    *conn = new_conn;
358    *root = new_root;
359    *cap_x = clamp_offset(rsettings.capture_x, geo.width);
360    *cap_y = clamp_offset(rsettings.capture_y, geo.height);
361    *cap_w = fw;
362    *cap_h = fh;
363    *root_w = geo.width;
364    *root_h = geo.height;
365    *surfaces = fresh;
366    pool.bump_generation();
367    Ok(())
368}
369
370/// The X server's report that the root window changed, which lets a capture publish a change as
371/// it lands instead of on the next tick. `None` where the server carries no DAMAGE extension,
372/// leaving the frame timer to pace the capture by itself.
373///
374/// Reporting is one notification per non-empty region: the server stays quiet until the region
375/// is subtracted, which is what [`Self::clear`] does to re-arm the next report.
376struct RootDamage {
377    id: Damage,
378    /// The XFixes region [`Self::take`] moves a report's damage into, to read where the root
379    /// changed; None where the server's XFixes has no regions.
380    parts: Option<Region>,
381    /// A report read off the connection and not yet acted on. Latched here because reading the
382    /// connection is what notices one, and that happens well before the frame it wakes.
383    reported: Cell<bool>,
384}
385
386impl RootDamage {
387    fn create(conn: &RustConnection, root: Window) -> Option<Self> {
388        conn.damage_query_version(1, 1).ok()?.reply().ok()?;
389        let id = conn.generate_id().ok()?;
390        conn.damage_create(id, root, ReportLevel::NON_EMPTY)
391            .ok()?
392            .check()
393            .ok()?;
394        let parts = (|| {
395            if conn
396                .xfixes_query_version(2, 0)
397                .ok()?
398                .reply()
399                .ok()?
400                .major_version
401                < 2
402            {
403                return None;
404            }
405            let parts = conn.generate_id().ok()?;
406            conn.xfixes_create_region(parts, &[]).ok()?.check().ok()?;
407            Some(parts)
408        })();
409        Some(Self {
410            id,
411            parts,
412            reported: Cell::new(false),
413        })
414    }
415
416    /// Whether the server has reported a change since the last [`Self::clear`], taking in
417    /// whatever else the connection carries along the way.
418    fn reported(&self, conn: &RustConnection) -> bool {
419        while let Ok(Some(event)) = conn.poll_for_event() {
420            if matches!(event, Event::DamageNotify(_)) {
421                self.reported.set(true);
422            }
423        }
424        self.reported.get()
425    }
426
427    /// Empty the region behind a report that has been acted on, so the next change is reported
428    /// in turn; a tick nothing reported leaves the server alone. Called before a grab, which
429    /// leaves a change racing the grab to wake the next frame instead of being cleared along
430    /// with what the grab captured.
431    fn clear(&self, conn: &RustConnection) {
432        if !self.reported(conn) {
433            return;
434        }
435        self.reported.set(false);
436        let _ = conn.damage_subtract(self.id, x11rb::NONE, x11rb::NONE);
437        let _ = conn.flush();
438    }
439
440    /// [`Self::clear`], saying where the root changed: the damaged rectangles, empty where
441    /// nothing was reported, None where the server cannot say.
442    fn take(&self, conn: &RustConnection) -> Option<Vec<Rectangle>> {
443        let Some(parts) = self.parts else {
444            self.clear(conn);
445            return None;
446        };
447        if !self.reported(conn) {
448            return Some(Vec::new());
449        }
450        self.reported.set(false);
451        conn.damage_subtract(self.id, x11rb::NONE, parts).ok()?;
452        Some(
453            conn.xfixes_fetch_region(parts)
454                .ok()?
455                .reply()
456                .ok()?
457                .rectangles,
458        )
459    }
460}
461
462/// The share of the captured area at (`x`, `y`), `width` x `height`, that the root's damaged
463/// `rects` cover.
464fn captured_damage(
465    rects: &[Rectangle],
466    x: i32,
467    y: i32,
468    width: i32,
469    height: i32,
470) -> crate::pipeline::Damage {
471    let local: Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> = rects
472        .iter()
473        .map(|r| {
474            smithay::utils::Rectangle::new(
475                (i32::from(r.x) - x, i32::from(r.y) - y).into(),
476                (i32::from(r.width), i32::from(r.height)).into(),
477            )
478        })
479        .collect();
480    crate::pipeline::Damage::of_rects(&local, width, height)
481}
482
483/// Wait for the connection to carry something, for at most `timeout`.
484fn readable(conn: &RustConnection, timeout: Duration) -> bool {
485    let mut poll = libc::pollfd {
486        fd: conn.stream().as_raw_fd(),
487        events: libc::POLLIN,
488        revents: 0,
489    };
490    let deadline = libc::timespec {
491        tv_sec: timeout.as_secs() as _,
492        tv_nsec: timeout.subsec_nanos() as libc::c_long,
493    };
494    unsafe { libc::ppoll(&mut poll, 1, &deadline, std::ptr::null()) > 0 }
495}
496
497/// Block until the capture is due a frame and say what asked for it: a change the server
498/// reported, brought forward by as much as the pacing budget allows, or the frame timer.
499///
500/// Without DAMAGE the wait is the timer's alone, which is the cadence a capture keeps when
501/// nothing else can say the screen moved.
502fn wait_for_frame(
503    conn: &RustConnection,
504    damage: Option<&RootDamage>,
505    pace: &FramePace,
506    period: Duration,
507) -> TickTrigger {
508    let now = Instant::now();
509    let due = pace.next_due(period, now);
510    let sleep_until = |t: Instant| {
511        if let Some(wait) = t.checked_duration_since(Instant::now()) {
512            thread::sleep(wait);
513        }
514    };
515    let Some(damage) = damage else {
516        sleep_until(due);
517        return TickTrigger::Timer;
518    };
519    // Nothing may be published before the budget allows it, so the report of a change that
520    // lands earlier is left queued until then rather than read and cleared.
521    sleep_until(pace.pull_at(TickTrigger::Damage, period, now).min(due));
522    loop {
523        if damage.reported(conn) {
524            return TickTrigger::Damage;
525        }
526        let Some(wait) = due.checked_duration_since(Instant::now()) else {
527            return TickTrigger::Timer;
528        };
529        if !readable(conn, wait) {
530            return TickTrigger::Timer;
531        }
532    }
533}
534
535/// One `shm_get_image` request is atomic — the server never interleaves another client
536/// inside it — so the grab is a coherent snapshot of whatever the screen held at that
537/// instant. Every grabbed frame is published; whether any of it changed is decided
538/// downstream by the encoder's own per-stripe damage detection.
539fn grab_frame(
540    conn: &RustConnection,
541    root: u32,
542    surface: &ShmSurface,
543    cap_x: i16,
544    cap_y: i16,
545    cap_w: u16,
546    cap_h: u16,
547) -> Result<(), String> {
548    conn.shm_get_image(
549        root,
550        cap_x,
551        cap_y,
552        cap_w,
553        cap_h,
554        !0u32,
555        ImageFormat::Z_PIXMAP.into(),
556        surface.shmseg,
557        0,
558    )
559    .map_err(|e| format!("shm_get_image: {e}"))?
560    .reply()
561    .map_err(|e| format!("shm_get_image reply: {e}"))?;
562    Ok(())
563}
564
565/// Frame-space top-left of the cursor image, given the XFixes hotspot position.
566///
567/// XFixes reports the cursor position at its HOTSPOT; X draws the image with its top-left at
568/// `(pos - hot)`, and the capture origin `(cap_x, cap_y)` is subtracted to move it into frame space.
569/// The result may go negative near the frame edges, which `overlay_cursor` clips per pixel to match
570/// the server's own edge clipping.
571#[inline]
572pub(crate) fn cursor_image_origin(
573    x: i16,
574    y: i16,
575    xhot: u16,
576    yhot: u16,
577    cap_x: i32,
578    cap_y: i32,
579) -> (i32, i32) {
580    (
581        x as i32 - xhot as i32 - cap_x,
582        y as i32 - yhot as i32 - cap_y,
583    )
584}
585
586/// Composite the XFixes cursor (ARGB `u32` per pixel, premultiplied by the XFixes
587/// format definition) onto the BGRA frame with its top-left at `(img_x, img_y)`,
588/// blending each pixel through `blend_pixel_premultiplied` with per-pixel bounds
589/// clipping so an image straddling a frame edge writes only its in-frame portion.
590#[allow(clippy::too_many_arguments)]
591pub(crate) fn overlay_cursor(
592    frame: &mut [u8],
593    stride: usize,
594    frame_w: i32,
595    frame_h: i32,
596    cur_w: i32,
597    cur_h: i32,
598    pixels: &[u32],
599    img_x: i32,
600    img_y: i32,
601) {
602    for y in 0..cur_h {
603        let ty = img_y + y;
604        if ty < 0 || ty >= frame_h {
605            continue;
606        }
607        for x in 0..cur_w {
608            let tx = img_x + x;
609            if tx < 0 || tx >= frame_w {
610                continue;
611            }
612            let px = pixels[(y * cur_w + x) as usize];
613            let a = ((px >> 24) & 0xFF) as u8;
614            let r = ((px >> 16) & 0xFF) as u8;
615            let g = ((px >> 8) & 0xFF) as u8;
616            let b = (px & 0xFF) as u8;
617            let off = ty as usize * stride + tx as usize * 4;
618            blend_pixel_premultiplied(&mut frame[off..off + 4], r, g, b, a);
619        }
620    }
621}
622
623/// A captured raw BGRA frame held in a pooled shm surface, ready to encode.
624///
625/// It carries the surface pointer plus geometry so the encode thread reads the pixels directly (no
626/// copy) and can rebuild its pipeline when the capture size changed (auto-adjust). It also carries
627/// the pool's surface `generation`, so a rebuild is still triggered when the surfaces were recreated
628/// at the SAME size (a resize flap) and the new segments happen to reuse the old virtual addresses.
629struct RawFrame {
630    idx: usize,
631    ptr: *mut u8,
632    len: usize,
633    width: u16,
634    height: u16,
635    stride: usize,
636    generation: u64,
637    /// CLOCK_MONOTONIC nanoseconds at which the pixels were in hand.
638    captured_ns: i64,
639}
640/// `RawFrame` is `Send`: its raw pointer addresses a pooled shm surface that the pool
641/// guarantees is not reused until the encode thread recycles this frame, so the handle is safe to
642/// move across the capture -> encode thread boundary.
643unsafe impl Send for RawFrame {}
644
645/// Mutex-guarded interior of `FramePool`: the free-surface index list and the single
646/// capture -> encode handoff slot, under one lock so moving a surface between them — acquire,
647/// publish, take, recycle — is always a single atomic step.
648struct PoolInner {
649    free: Vec<usize>,
650    slot: Option<RawFrame>,
651}
652
653/// Demand-driven capture -> encode handoff: a bounded single-slot channel over a fixed set of
654/// pooled shm surfaces.
655///
656/// The capture thread writes into a pooled surface and `publish`es it into the single `slot`; the
657/// encode thread `take`s it. Capture stays at most one frame ahead of encode because `acquire` and
658/// `publish` BLOCK (bounded) until the encoder frees a surface / drains the slot, throttling capture
659/// to the encode rate. Since X11 capture is pull-based (no backlog), throttling — rather than
660/// capturing-then-dropping — means a full-resolution shm round-trip is never spent on a frame that
661/// would be discarded, while the next capture still overlaps the current encode (the throughput win).
662/// The encoder therefore only ever sees a contiguous frame stream, keeping the H.264 reference chain
663/// valid.
664///
665/// `generation` is bumped by the capture thread each time the backing shm surfaces are destroyed and
666/// recreated. Published frames carry it so the encode thread rebuilds its pipeline — dropping encoder
667/// state keyed to surface base pointers (e.g. NVENC's pinned-host registrations) — even when a resize
668/// flap lands back on the old dimensions and the recreated segments reuse the old virtual addresses.
669struct FramePool {
670    inner: Mutex<PoolInner>,
671    cv: Condvar,
672    stop: AtomicBool,
673    generation: AtomicU64,
674}
675
676impl FramePool {
677    /// Create a pool with `n` free surfaces, an empty handoff slot, and generation 0.
678    fn new(n: usize) -> Self {
679        Self {
680            inner: Mutex::new(PoolInner {
681                free: (0..n).collect(),
682                slot: None,
683            }),
684            cv: Condvar::new(),
685            stop: AtomicBool::new(false),
686            generation: AtomicU64::new(0),
687        }
688    }
689
690    /// Capture: record that the surfaces were recreated by advancing the generation. Only
691    /// called after `drain_for_resize` succeeded, so no frame from the previous generation is still
692    /// in flight.
693    fn bump_generation(&self) {
694        self.generation.fetch_add(1, Ordering::Relaxed);
695    }
696
697    /// Read the current surface generation, stamped onto each published frame.
698    fn generation(&self) -> u64 {
699        self.generation.load(Ordering::Relaxed)
700    }
701
702    /// Capture: claim a free surface to write the next frame into, or `None` on stop.
703    ///
704    /// Blocks (bounded, re-checking `stop` every 20ms) until a surface is free, which throttles
705    /// capture to the encode rate so a full-resolution capture is never spent on a frame that would
706    /// be dropped. The bounded re-check means a stop that races the wait cannot hang the thread.
707    fn acquire(&self, stop: &AtomicBool) -> Option<usize> {
708        let mut g = self.inner.lock().unwrap();
709        loop {
710            if let Some(idx) = g.free.pop() {
711                return Some(idx);
712            }
713            if stop.load(Ordering::Relaxed) {
714                return None;
715            }
716            let (gg, _) = self.cv.wait_timeout(g, Duration::from_millis(20)).unwrap();
717            g = gg;
718        }
719    }
720
721    /// Capture: publish the just-captured frame into the single slot, or `false` (frame
722    /// discarded) on stop.
723    ///
724    /// Blocks (bounded, re-checking `stop` every 20ms) until the encode thread has taken the previous
725    /// frame, so capture stays at most one frame ahead and never drops under normal flow.
726    fn publish(&self, frame: RawFrame, stop: &AtomicBool) -> bool {
727        let mut g = self.inner.lock().unwrap();
728        loop {
729            if g.slot.is_none() {
730                g.slot = Some(frame);
731                drop(g);
732                self.cv.notify_all();
733                return true;
734            }
735            if stop.load(Ordering::Relaxed) {
736                return false;
737            }
738            let (gg, _) = self.cv.wait_timeout(g, Duration::from_millis(20)).unwrap();
739            g = gg;
740        }
741    }
742
743    /// Encode: block until a frame is available (`Some`) or stop is signaled (`None`).
744    ///
745    /// The wait is bounded (re-checking `stop` every 20ms) as defense-in-depth against a lost wakeup,
746    /// so a stop that races the park can never leave the encode thread blocked forever.
747    fn take(&self) -> Option<RawFrame> {
748        let mut g = self.inner.lock().unwrap();
749        loop {
750            if let Some(f) = g.slot.take() {
751                return Some(f);
752            }
753            if self.stop.load(Ordering::Acquire) {
754                return None;
755            }
756            let (gg, _) = self.cv.wait_timeout(g, Duration::from_millis(20)).unwrap();
757            g = gg;
758        }
759    }
760
761    /// Encode: return a surface to the free list after it has been encoded, waking any waiter
762    /// parked in `acquire` / `publish` / `drain_for_resize`.
763    fn recycle(&self, idx: usize) {
764        self.inner.lock().unwrap().free.push(idx);
765        self.cv.notify_all();
766    }
767
768    /// Capture: before recreating surfaces (auto-adjust / region resize), reclaim the pending
769    /// slot and wait until every surface is back in the free list, so no surface is destroyed while
770    /// the encode thread is still reading it.
771    ///
772    /// Reclaiming the slot returns any un-taken frame to the free list; the loop then waits until
773    /// `free.len() == n`, i.e. the encode thread has finished any in-flight frame. The wait is bounded
774    /// and also breaks on stop — pool shutdown OR the external `stop` — so a panicked or dead encode
775    /// thread that never recycles cannot wedge the capture thread here forever; a requested stop
776    /// unblocks it. Returns `true` if it fully drained (safe to recreate surfaces), or `false` if it
777    /// aborted on stop — in which case the caller tears down, which joins the encode thread before
778    /// destroying surfaces, so the resize-safety guarantee still holds.
779    fn drain_for_resize(&self, n: usize, stop: &AtomicBool) -> bool {
780        let mut g = self.inner.lock().unwrap();
781        if let Some(old) = g.slot.take() {
782            g.free.push(old.idx);
783        }
784        while g.free.len() < n {
785            if self.stop.load(Ordering::Acquire) || stop.load(Ordering::Relaxed) {
786                return false;
787            }
788            let (gg, _) = self.cv.wait_timeout(g, Duration::from_millis(20)).unwrap();
789            g = gg;
790        }
791        true
792    }
793
794    /// Signal every waiter to stop and wake them, ending the encode thread's `take` loop.
795    ///
796    /// The inner mutex is acquired BEFORE storing `stop` and notifying, which closes the lost-wakeup
797    /// window: with the lock held, `take` is either still before its `stop` check or already parked on
798    /// the condvar (and will receive the notify), never in the gap between the two. The notify is
799    /// issued after the guard is dropped so the woken thread does not immediately re-block on the lock
800    /// this call holds.
801    fn shutdown(&self) {
802        let g = self.inner.lock().unwrap();
803        self.stop.store(true, Ordering::Release);
804        drop(g);
805        self.cv.notify_all();
806    }
807}
808
809/// Encode thread body: consume captured frames from the pool, keep the [`pipeline::X11Pipeline`] in
810/// step with the capture size and cross-thread controls, encode, recycle, and deliver.
811///
812/// The loop runs until `FramePool::take` returns `None` (stop). For each frame:
813///
814/// 1. **(Re)build the pipeline** when it is missing, the frame size changed, or the surface
815///    generation changed. A generation change alone forces the rebuild path because recreated shm
816///    segments often reuse the old virtual base addresses, so encoder state keyed to base pointers
817///    (NVENC's pinned-host cache) must be dropped even at identical dimensions. An in-place
818///    [`X11Pipeline::reshape`] is preferred — NVENC reconfigures its live session and the striped
819///    software path just re-derives stripe state — so a resize never stalls on a full encoder
820///    re-init; encoders that cannot follow in place (VAAPI) report `false` and the pipeline
821///    is rebuilt. On a rebuild the old pipeline (with its GPU session/surfaces) is dropped BEFORE the
822///    new one is built, so an auto-adjust resize never holds two full encoder allocations at once
823///    (transient 2x GPU memory). The rebuilt settings pull the CURRENT bitrate / VBV / fps from the
824///    controls atomics, carrying live rate changes forward instead of reverting to the capture-start
825///    values.
826/// 2. **Apply cross-thread controls** here, on the thread that owns the pipeline: a pending
827///    `force_idr` requests a keyframe; a `rate_dirty` swap (Acquire, pairing with the setters' Release
828///    stores so the bitrate / VBV / fps payload is never seen half-applied) pushes a live rate change;
829///    a `tunables_dirty` swap applies per-frame tunables into both the local settings copy and the
830///    pipeline.
831/// 3. **Encode and hand off**: read the pooled surface directly through its raw pointer — sound
832///    because the pool guarantees the surface is not reused until it is recycled — run
833///    [`X11Pipeline::process`], recycle the surface BEFORE delivering so a slow consumer never holds a
834///    capture surface, then deliver any encoded stripes through `on_frame`.
835///
836/// The optional Unix-socket recording sink (parity with the Wayland path) is bound ONCE here and
837/// owned outside the pipeline, so pipeline rebuilds on resize keep the socket listener and any
838/// attached recorders alive. It can only carry a single full-frame H.264 stream, so configurations
839/// that cannot produce one (JPEG output, or a striped CPU encoder) are warned about up front.
840fn encode_loop<F>(
841    pool: &FramePool,
842    controls: &Controls,
843    settings: &RustCaptureSettings,
844    on_frame: &mut F,
845) where
846    F: FnMut(Vec<EncodedStripe>),
847{
848    let _report = crate::report::enter(&controls.report);
849    let mut psettings = settings.clone();
850    let recording_sink = RecordingSink::try_bind(&settings.recording_socket, settings.target_fps);
851    if recording_sink.is_some() {
852        if !settings.codec.is_video() {
853            eprintln!(
854                "[recording_sink] recording_socket set but the codec is JPEG; no recordable video stream."
855            );
856        } else if settings.use_cpu && !settings.video_fullframe {
857            eprintln!(
858                "[recording_sink] recording_socket set but the CPU encoder is striped; set video_fullframe=true for a recordable stream."
859            );
860        }
861    }
862    let mut pipeline: Option<X11Pipeline> = None;
863    let (mut pw, mut ph) = (0i32, 0i32);
864    let mut pgen = 0u64;
865    let mut last_log_time = Instant::now();
866    let mut frame_count: u64 = 0;
867    let mut stripe_count: u64 = 0;
868
869    while let Some(frame) = pool.take() {
870        let (fw, fh) = (frame.width as i32, frame.height as i32);
871        if pipeline.is_none() || pw != fw || ph != fh || pgen != frame.generation {
872            let size_changed = pw != fw || ph != fh;
873            psettings.width = fw;
874            psettings.height = fh;
875            psettings.video_bitrate_kbps = controls.bitrate_kbps.load(Ordering::Relaxed);
876            psettings.video_vbv_multiplier =
877                controls.vbv_mult_milli.load(Ordering::Relaxed) as f64 / 1000.0;
878            psettings.target_fps =
879                f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
880            let reshaped = pipeline
881                .as_mut()
882                .is_some_and(|pl| pl.reshape(&psettings, size_changed));
883            if !reshaped {
884                drop(pipeline.take());
885                pipeline = Some(X11Pipeline::new(psettings.clone()));
886                if let Some(pl) = &pipeline {
887                    let mut log_msg = format!(
888                        "[X11] Stream settings active -> Res: {}x{} | FPS: {:.1} | Encoder: {}",
889                        psettings.width,
890                        psettings.height,
891                        psettings.target_fps,
892                        pl.encoder_name()
893                    );
894                    let fixed = pl.fixed_rate_control();
895                    let paint_over =
896                        crate::encoders::paint_over_desc(&psettings, fixed, pl.holds_quantizer());
897                    if !pl.codec().is_video() {
898                        log_msg.push_str(&format!(
899                            " | Mode: JPEG | Quality: {}",
900                            psettings.jpeg_quality
901                        ));
902                        if let Some(paint_over) = paint_over {
903                            log_msg.push_str(&format!(" | {paint_over}"));
904                        }
905                    } else {
906                        log_msg.push_str(&format!(
907                            " | Mode: {} | {}",
908                            pl.codec().display(),
909                            crate::encoders::rate_desc(&psettings, fixed)
910                        ));
911                        if let Some(paint_over) = paint_over {
912                            log_msg.push_str(&format!(" | {paint_over}"));
913                        }
914                        log_msg.push_str(&format!(" | Colorspace: {}", pl.colorspace_desc()));
915                    }
916                    log_msg.push_str(&format!(
917                        " | Damage Thresh: {}f | Damage Dur: {}f",
918                        psettings.damage_block_threshold, psettings.damage_block_duration
919                    ));
920                    println!("{}", log_msg);
921                }
922            }
923            pw = fw;
924            ph = fh;
925            pgen = frame.generation;
926        }
927        let pl = pipeline.as_mut().unwrap();
928
929        // A recorder connecting to the socket sink needs a fresh decode entry point; it is
930        // folded into the same standard request-IDR path as a client-driven force_idr.
931        let sink_idr = recording_sink
932            .as_ref()
933            .map(|s| s.should_force_idr())
934            .unwrap_or(false);
935        for report in std::mem::take(&mut *controls.reference_reports.lock().unwrap()) {
936            pl.take_report(report);
937        }
938        if controls.force_idr.swap(false, Ordering::Relaxed) || sink_idr {
939            pl.request_idr();
940        }
941        if controls.rate_dirty.swap(false, Ordering::Acquire) {
942            let b = controls.bitrate_kbps.load(Ordering::Relaxed);
943            let v = controls.vbv_mult_milli.load(Ordering::Relaxed) as f64 / 1000.0;
944            let fps = f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
945            pl.update_rate(b, v, fps);
946        }
947        if controls.tunables_dirty.swap(false, Ordering::Acquire)
948            && let Some(t) = controls.tunables.lock().unwrap().take()
949        {
950            t.apply_to(&mut psettings);
951            pl.update_tunables(&t);
952        }
953
954        let buf = unsafe { std::slice::from_raw_parts(frame.ptr, frame.len) };
955        let encode_start_ns = crate::wayland::host::now_ns();
956        let mut stripes = pl.process(buf, frame.stride);
957        FrameTiming::stamp(&mut stripes, frame.captured_ns, encode_start_ns);
958        controls.codec.store(pl.codec().id(), Ordering::Relaxed);
959        pool.recycle(frame.idx);
960        if !stripes.is_empty() {
961            frame_count += 1;
962            stripe_count += stripes.len() as u64;
963            // IDR arming is consumed inside X11Pipeline::process; this tap only fans out.
964            if let Some(ref socket) = recording_sink {
965                socket.write_frame(&stripes, psettings.width, psettings.height);
966            }
967            on_frame(stripes);
968        }
969
970        let now = Instant::now();
971        let elapsed = now.duration_since(last_log_time).as_secs_f64();
972        if elapsed >= 1.0 {
973            crate::log::debug!(
974                "[X11] Res: {}x{} Encoder: {} EncFPS: {:.2} EncStripes/s: {:.2}",
975                psettings.width,
976                psettings.height,
977                pl.encoder_name(),
978                frame_count as f64 / elapsed,
979                stripe_count as f64 / elapsed
980            );
981            frame_count = 0;
982            stripe_count = 0;
983            last_log_time = now;
984        }
985    }
986}
987
988/// The rate the X server's fake vblank runs at while this process captures it: the fastest live
989/// capture's frame rate, published on the root window as `_FAKE_SCREEN_MILLIHZ` (CARDINAL,
990/// millihertz, 1000 to 1000000) and, rounded to whole frames for servers that read only that,
991/// `_FAKE_SCREEN_FPS` (1 to 1000), both deleted when the last capture ends. The Selkies Xvfb paces
992/// the clients that wait on Present by it, keeping the rate it started at as a floor unless the
993/// published one is at most 0.1% slower (59.94 under 60), so a vsynced application presents as
994/// fast as it is streamed; any other server holds it as inert data.
995struct VblankRate {
996    captures: Vec<Arc<Controls>>,
997    /// Millihertz last published, 0 for none.
998    published: u32,
999    /// The connection, the root window, and the millihertz and frames-per-second atoms.
1000    conn: Option<(RustConnection, Window, Atom, Atom)>,
1001}
1002
1003static VBLANK_RATE: Mutex<VblankRate> = Mutex::new(VblankRate {
1004    captures: Vec::new(),
1005    published: 0,
1006    conn: None,
1007});
1008
1009impl VblankRate {
1010    fn publish(&mut self) {
1011        let want = self
1012            .captures
1013            .iter()
1014            .map(|c| {
1015                (f64::from_bits(c.fps_bits.load(Ordering::Relaxed)) * 1000.0)
1016                    .round()
1017                    .clamp(1000.0, 1_000_000.0) as u32
1018            })
1019            .max()
1020            .unwrap_or(0);
1021        if want == self.published {
1022            return;
1023        }
1024        if self.conn.is_none() {
1025            self.conn = x11rb::connect(None).ok().and_then(|(conn, screen)| {
1026                let root = conn.setup().roots[screen].root;
1027                let atom =
1028                    |name: &[u8]| Some(conn.intern_atom(false, name).ok()?.reply().ok()?.atom);
1029                let millihz = atom(b"_FAKE_SCREEN_MILLIHZ")?;
1030                let fps = atom(b"_FAKE_SCREEN_FPS")?;
1031                Some((conn, root, millihz, fps))
1032            });
1033        }
1034        let sent = self
1035            .conn
1036            .as_ref()
1037            .is_some_and(|(conn, root, millihz, fps)| {
1038                let queued = if want > 0 {
1039                    [(*millihz, want), (*fps, (want + 500) / 1000)]
1040                        .iter()
1041                        .all(|&(atom, value)| {
1042                            conn.change_property32(
1043                                PropMode::REPLACE,
1044                                *root,
1045                                atom,
1046                                AtomEnum::CARDINAL,
1047                                &[value],
1048                            )
1049                            .is_ok()
1050                        })
1051                } else {
1052                    [*millihz, *fps]
1053                        .iter()
1054                        .all(|&atom| conn.delete_property(*root, atom).is_ok())
1055                };
1056                // A round trip, not a flush: a server may drop what is still queued when a client
1057                // closes, and the connection closes right after the last capture's delete.
1058                queued
1059                    && conn
1060                        .get_input_focus()
1061                        .ok()
1062                        .and_then(|c| c.reply().ok())
1063                        .is_some()
1064            });
1065        self.published = if sent { want } else { 0 };
1066        if !sent || want == 0 {
1067            self.conn = None;
1068        }
1069    }
1070}
1071
1072/// A running capture's place in [`VblankRate`], given up when the capture ends.
1073struct VblankClaim(Arc<Controls>);
1074
1075impl VblankClaim {
1076    fn new(controls: &Arc<Controls>) -> Self {
1077        let mut rate = VBLANK_RATE.lock().unwrap();
1078        rate.captures.push(controls.clone());
1079        rate.publish();
1080        Self(controls.clone())
1081    }
1082}
1083
1084impl Drop for VblankClaim {
1085    fn drop(&mut self) {
1086        let mut rate = VBLANK_RATE.lock().unwrap();
1087        rate.captures.retain(|c| !Arc::ptr_eq(c, &self.0));
1088        rate.publish();
1089    }
1090}
1091
1092/// Republish [`VblankRate`] after a running capture's frame rate changed.
1093pub fn follow_frame_rate() {
1094    VBLANK_RATE.lock().unwrap().publish();
1095}
1096
1097/// Capture an X11 display until `stop` is set, on the best path this session can run.
1098///
1099/// [`nvfbc::run_capture`] is tried first: where the NVIDIA driver offers framebuffer capture and
1100/// the session encodes on NVENC, the screen is composited into video memory and encoded in place,
1101/// so a frame reaches the bitstream without being copied once. [`dri3::run_capture`] is next: on a
1102/// server whose screen lives on the GPU, the server blits the root into dmabufs this process
1103/// allocated and the hardware session reads them in place. Each reports that it cannot serve the
1104/// session — a codec its engine has no support for, software encoding, a watermark that has to be
1105/// blended into host pixels, a server or device that does not qualify — and the capture then runs
1106/// on the general XShm path below, which every X server supports. Whichever path runs, the
1107/// capture's frame rate paces the server's fake vblank for as long as it does ([`VblankRate`]).
1108///
1109/// Blocking; intended to run on a dedicated thread.
1110pub fn run_capture<F>(
1111    settings: RustCaptureSettings,
1112    controls: Arc<Controls>,
1113    encode_tid_tx: Sender<thread::ThreadId>,
1114    mut on_frame: F,
1115) -> Result<(), String>
1116where
1117    F: FnMut(Vec<EncodedStripe>) + Send + 'static,
1118{
1119    let _report = crate::report::enter(&controls.report);
1120    let _vblank = VblankClaim::new(&controls);
1121    if let Some(result) = nvfbc::run_capture(
1122        settings.clone(),
1123        controls.clone(),
1124        encode_tid_tx.clone(),
1125        &mut on_frame,
1126    ) {
1127        return result;
1128    }
1129    if let Some(result) = dri3::run_capture(
1130        settings.clone(),
1131        controls.clone(),
1132        encode_tid_tx.clone(),
1133        &mut on_frame,
1134    ) {
1135        return result;
1136    }
1137    crate::report::capture("XShm", false);
1138    run_shm_capture(settings, controls, encode_tid_tx, on_frame)
1139}
1140
1141/// Run the X11 capture pipeline until `stop` is set, splitting capture and encode across two
1142/// threads that overlap for throughput.
1143///
1144/// This (the caller's) thread performs setup and then the capture loop; a spawned encode thread runs
1145/// `encode_loop` and invokes `on_frame(stripes)` once per encoded frame. The split lets capture and
1146/// encode overlap, and because frames are throttled/dropped as RAW frames before encode, the delivered
1147/// H.264 stays a valid contiguous reference chain.
1148///
1149/// 1. **Setup**: connect to X (a private connection) and require a 32-bpp BGRA root — the Z-pixmap
1150///    byte depth must be 4, which modern servers use for depth 24/32 — then negotiate XShm and
1151///    XFixes. XFixes is always negotiated (one round-trip) so the cursor overlay can be toggled on
1152///    live even when capture started without it. Initial dimensions and origin are resolved from the
1153///    settings and the live root geometry, and a `FramePool` of `POOL_N` = 3 shm surfaces is
1154///    allocated — the working set (one in-capture, one in-slot, one in-encode) that a one-frame-ahead
1155///    demand-driven capture needs, which also keeps the memory cost (`3 * W*H*4`, significant at 4K)
1156///    bounded.
1157/// 2. **Encode thread**: spawned with a raised scheduling priority; it reports its thread id back
1158///    through `encode_tid_tx` so the caller can detect a re-entrant stop issued from inside the
1159///    delivery callback.
1160/// 3. **Capture loop** (until `stop`): fps is re-read each iteration for live pacing. The wait
1161///    runs to the frame deadline, but a region the X server reports damaged ends it early once
1162///    the shared pacing budget allows (`crate::pace`), so a screen that changes mid-period is
1163///    published then rather than on the next tick. A server without DAMAGE keeps the deadline
1164///    alone.
1165///    - **Live region change** (`region_dirty`): re-target the grab origin immediately (an x/y pan
1166///      needs no surface work); a size change reuses the drain/recreate path below.
1167///    - **Auto-adjust**: on a root geometry change, drain in-flight frames then recreate the surfaces
1168///      and bump the generation (which the encode thread turns into a reshape or rebuild). The geometry
1169///      is re-resolved AFTER the drain, because a slow drain can outlast another geometry change (a
1170///      fast flap) and recreating at a stale size would make the next `shm_get_image` exceed the root;
1171///      a fully-reverted flap keeps its surfaces as-is. A stop that races the drain breaks out to
1172///      teardown.
1173///    - **Grab**: acquire a pooled surface (blocks until the encoder frees one), then `shm_get_image`
1174///      the region into it synchronously (`reply()` waits).
1175///    - **Overlays**: composite the XFixes cursor (drawn at its hotspot-offset origin; live-toggleable)
1176///      and the watermark onto the BGRA pixels on the CPU.
1177///    - **Publish**: hand the finished frame to the encode thread (blocks until the slot is free;
1178///      never drops). A stop observed while waiting in acquire/publish exits the loop.
1179/// 4. **Teardown**: stop and JOIN the encode thread BEFORE destroying the shm surfaces it may still be
1180///    reading, preserving the resize-safety guarantee.
1181///
1182/// Blocking; intended to run on a dedicated thread. The X connection and shm surfaces live on this
1183/// thread and the encoder lives on the encode thread — nothing X-related crosses the boundary.
1184fn run_shm_capture<F>(
1185    settings: RustCaptureSettings,
1186    controls: Arc<Controls>,
1187    encode_tid_tx: Sender<thread::ThreadId>,
1188    on_frame: F,
1189) -> Result<(), String>
1190where
1191    F: FnMut(Vec<EncodedStripe>) + Send + 'static,
1192{
1193    let (mut conn, screen_num) =
1194        x11rb::connect(None).map_err(|e| format!("X11 connect failed: {e}"))?;
1195    let mut root = conn.setup().roots[screen_num].root;
1196    require_32bpp(&conn, screen_num)?;
1197
1198    conn.shm_query_version()
1199        .map_err(|e| format!("shm_query_version: {e}"))?
1200        .reply()
1201        .map_err(|e| format!("XShm unavailable: {e}"))?;
1202    conn.xfixes_query_version(5, 0)
1203        .map_err(|e| format!("xfixes_query_version: {e}"))?
1204        .reply()
1205        .map_err(|e| format!("XFixes unavailable: {e}"))?;
1206    let mut damage = RootDamage::create(&conn, root);
1207
1208    let geo = conn
1209        .get_geometry(root)
1210        .map_err(|e| format!("get_geometry: {e}"))?
1211        .reply()
1212        .map_err(|e| format!("get_geometry reply: {e}"))?;
1213
1214    let mut rsettings = settings.clone();
1215    let (mut cap_w, mut cap_h) = resolve_dims(geo.width, geo.height, &rsettings);
1216    let mut cap_x = clamp_offset(rsettings.capture_x, geo.width);
1217    let mut cap_y = clamp_offset(rsettings.capture_y, geo.height);
1218    // Last root size seen. A live pan has to clamp against something even when the geometry
1219    // round-trip fails, or the requested origin would be dropped instead of applied.
1220    let (mut root_w, mut root_h) = (geo.width, geo.height);
1221
1222    const POOL_N: usize = 3;
1223    let mut surfaces: Vec<ShmSurface> = Vec::with_capacity(POOL_N);
1224    for _ in 0..POOL_N {
1225        surfaces.push(ShmSurface::create(&conn, cap_w, cap_h)?);
1226    }
1227    // Consecutive channel rebuilds without one good grab in between: geometry
1228    // errors can burn out the grab budget repeatedly under a dead region, so
1229    // recovery only gets a bounded number of channel rebuilds.
1230    let mut reconnect_streak = 0u32;
1231    let pool = Arc::new(FramePool::new(POOL_N));
1232
1233    let mut watermark = crate::encoders::overlay::OverlayState::default();
1234    if !settings.watermark_path.is_empty() {
1235        watermark.load_watermark(&settings.watermark_path, 1.0);
1236    }
1237
1238    let enc_pool = pool.clone();
1239    let enc_controls = controls.clone();
1240    let enc_settings = settings.clone();
1241    let encode_panicked = Arc::new(AtomicBool::new(false));
1242    let guard_panicked = encode_panicked.clone();
1243    let encode_thread = thread::spawn(move || {
1244        crate::boost_thread_priority(-10);
1245        let _ = encode_tid_tx.send(thread::current().id());
1246        let mut on_frame = on_frame;
1247        // An encoder panic must never wedge the capture: without the pool shutdown
1248        // the capture thread would block in publish forever while is_capturing still
1249        // reports true on this thread's handle. Shutting the pool unblocks it through
1250        // the same path a clean stop takes, so the panic is also recorded — otherwise
1251        // the capture would return Ok and Python could not tell the two apart.
1252        let guard_pool = enc_pool.clone();
1253        let guard_controls = enc_controls.clone();
1254        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1255            encode_loop(&enc_pool, &enc_controls, &enc_settings, &mut on_frame);
1256        }));
1257        if result.is_err() {
1258            eprintln!("[X11] encode thread panicked; shutting the pool to fail the capture");
1259            guard_panicked.store(true, Ordering::Release);
1260            guard_pool.shutdown();
1261            guard_controls.stop.store(true, Ordering::Relaxed);
1262        }
1263    });
1264
1265    let mut pace = FramePace::default();
1266    // Frames between root-geometry polls (auto-adjust); ~0.5s at 60fps.
1267    const GEOMETRY_POLL_FRAMES: i32 = 30;
1268    let mut geometry_check = 0i32;
1269    let mut grab_failures = 0u32;
1270
1271    let result = (|| -> Result<(), String> {
1272        while !controls.stop.load(Ordering::Relaxed) {
1273            let fps = f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
1274            let frame_dur = Duration::from_secs_f64(1.0 / fps.max(1.0));
1275            if pace.next_due(frame_dur, Instant::now()) <= Instant::now() {
1276                thread::yield_now();
1277            }
1278            let trigger = wait_for_frame(&conn, damage.as_ref(), &pace, frame_dur);
1279            pace.ticked(trigger, frame_dur, Instant::now(), false);
1280            if controls.stop.load(Ordering::Relaxed) {
1281                break;
1282            }
1283
1284            if controls.region_dirty.swap(false, Ordering::Acquire) {
1285                let (nx, ny, nw, nh) = *controls.region.lock().unwrap();
1286                rsettings.capture_x = nx;
1287                rsettings.capture_y = ny;
1288                rsettings.width = nw;
1289                rsettings.height = nh;
1290                // An explicit live size pins the region: auto-adjust (from the start
1291                // settings) would glue resolve_dims back to the ROOT size, undoing the
1292                // re-target on the next geometry poll. w/h <= 0 keep following the root.
1293                rsettings.auto_adjust_screen_capture_size = nw <= 0 || nh <= 0;
1294                cap_x = clamp_offset(nx, root_w);
1295                cap_y = clamp_offset(ny, root_h);
1296                if let Some(g) = conn.get_geometry(root).ok().and_then(|c| c.reply().ok()) {
1297                    root_w = g.width;
1298                    root_h = g.height;
1299                    cap_x = clamp_offset(nx, root_w);
1300                    cap_y = clamp_offset(ny, root_h);
1301                    let (fw, fh) = resolve_dims(g.width, g.height, &rsettings);
1302                    if fw != cap_w || fh != cap_h {
1303                        if !pool.drain_for_resize(POOL_N, &controls.stop) {
1304                            break;
1305                        }
1306                        for s in surfaces.iter_mut() {
1307                            s.destroy(&conn);
1308                        }
1309                        surfaces.clear();
1310                        cap_w = fw;
1311                        cap_h = fh;
1312                        for _ in 0..POOL_N {
1313                            surfaces.push(ShmSurface::create(&conn, cap_w, cap_h)?);
1314                        }
1315                        pool.bump_generation();
1316                    }
1317                }
1318            }
1319
1320            // Root geometry is polled on a cadence, not per frame: the reply costs a
1321            // round-trip that would otherwise precede every grab, and external size
1322            // changes are rare (live resizes arrive through region_dirty instead).
1323            // The gate reads the live copy, so a region re-targeted to the root edge
1324            // starts following it from that point on. Grab-failure recovery enters
1325            // regardless of auto_adjust: a fixed-size region has no other re-clamp
1326            // path after an external root shrink, and without one the capture dies at
1327            // the failure cap instead of recovering.
1328            geometry_check = geometry_check.saturating_sub(1);
1329            if (rsettings.auto_adjust_screen_capture_size || grab_failures > 0)
1330                && geometry_check <= 0
1331            {
1332                geometry_check = GEOMETRY_POLL_FRAMES;
1333                if let Some(g) = conn.get_geometry(root).ok().and_then(|c| c.reply().ok()) {
1334                    // A root shrink can strand even a previously-valid offset past
1335                    // the new edge; re-clamp it against the live root.
1336                    root_w = g.width;
1337                    root_h = g.height;
1338                    cap_x = clamp_offset(rsettings.capture_x, root_w);
1339                    cap_y = clamp_offset(rsettings.capture_y, root_h);
1340                    let (nw, nh) = resolve_dims(g.width, g.height, &rsettings);
1341                    if nw != cap_w || nh != cap_h {
1342                        if !pool.drain_for_resize(POOL_N, &controls.stop) {
1343                            break;
1344                        }
1345                        let (fw, fh) = conn
1346                            .get_geometry(root)
1347                            .ok()
1348                            .and_then(|c| c.reply().ok())
1349                            .map(|g| resolve_dims(g.width, g.height, &rsettings))
1350                            .unwrap_or((nw, nh));
1351                        if fw != cap_w || fh != cap_h {
1352                            for s in surfaces.iter_mut() {
1353                                s.destroy(&conn);
1354                            }
1355                            surfaces.clear();
1356                            cap_w = fw;
1357                            cap_h = fh;
1358                            for _ in 0..POOL_N {
1359                                surfaces.push(ShmSurface::create(&conn, cap_w, cap_h)?);
1360                            }
1361                            pool.bump_generation();
1362                        }
1363                    }
1364                }
1365            }
1366
1367            let idx = match pool.acquire(&controls.stop) {
1368                Some(i) => i,
1369                None => break,
1370            };
1371            if let Some(d) = damage.as_ref() {
1372                d.clear(&conn);
1373            }
1374            let grab_result = {
1375                let surface = &mut surfaces[idx];
1376                grab_frame(&conn, root, surface, cap_x, cap_y, cap_w, cap_h)
1377            };
1378            if let Err(e) = grab_result {
1379                // Most likely an external root resize between geometry polls made
1380                // the grab run past the root edge: return the surface, re-poll
1381                // geometry immediately, and only give up if it never recovers.
1382                pool.recycle(idx);
1383                grab_failures += 1;
1384                geometry_check = 0;
1385                if grab_failures > 120 {
1386                    // A burn-out usually means the X connection itself is gone
1387                    // (Xorg restart, VT switch): every request fails until the
1388                    // server is back. Rebuild the channel a few times before
1389                    // declaring the capture dead — a recovered server resumes the
1390                    // stream where a dead thread would leave a black screen until
1391                    // the Python watchdog restarts everything.
1392                    let mut recovered = false;
1393                    for _attempt in 0..5 {
1394                        if controls.stop.load(Ordering::Relaxed) {
1395                            break;
1396                        }
1397                        std::thread::sleep(Duration::from_secs(1));
1398                        match try_rebuild_channel(
1399                            &mut conn,
1400                            &mut root,
1401                            &rsettings,
1402                            &mut cap_x,
1403                            &mut cap_y,
1404                            &mut cap_w,
1405                            &mut cap_h,
1406                            &mut root_w,
1407                            &mut root_h,
1408                            POOL_N,
1409                            &pool,
1410                            &mut surfaces,
1411                            &controls.stop,
1412                        ) {
1413                            Ok(()) => {
1414                                // The reported region belonged to the server that went away.
1415                                damage = RootDamage::create(&conn, root);
1416                                recovered = true;
1417                                break;
1418                            }
1419                            Err(re) => {
1420                                eprintln!("[X11] reconnect attempt failed: {re}");
1421                            }
1422                        }
1423                    }
1424                    if !recovered {
1425                        return Err(format!("capture could not recover: {e}"));
1426                    }
1427                    grab_failures = 0;
1428                    geometry_check = 0;
1429                    reconnect_streak += 1;
1430                    if reconnect_streak >= 4 {
1431                        return Err(
1432                            "capture recovered its channel but the region never grabbed again"
1433                                .to_string(),
1434                        );
1435                    }
1436                }
1437                continue;
1438            }
1439            grab_failures = 0;
1440            reconnect_streak = 0;
1441            let surface = &mut surfaces[idx];
1442
1443            let frame_w = cap_w as i32;
1444            let frame_h = cap_h as i32;
1445            let stride = surface.stride;
1446            let buf = surface.as_mut_slice();
1447
1448            if controls.capture_cursor.load(Ordering::Relaxed)
1449                && let Some(c) = conn
1450                    .xfixes_get_cursor_image()
1451                    .ok()
1452                    .and_then(|c| c.reply().ok())
1453                && c.width > 0
1454                && c.height > 0
1455            {
1456                // Translate from root coordinates using the LIVE grab origin
1457                // (cap_x/cap_y follow region_dirty pans and clamp negatives
1458                // exactly like the grab itself), never the immutable startup
1459                // settings — those go stale on the first live region move.
1460                let (img_x, img_y) =
1461                    cursor_image_origin(c.x, c.y, c.xhot, c.yhot, cap_x as i32, cap_y as i32);
1462                overlay_cursor(
1463                    buf,
1464                    stride,
1465                    frame_w,
1466                    frame_h,
1467                    c.width as i32,
1468                    c.height as i32,
1469                    &c.cursor_image,
1470                    img_x,
1471                    img_y,
1472                );
1473            }
1474
1475            if watermark.is_active() {
1476                watermark.update_position(frame_w, frame_h, settings.watermark_location_enum);
1477                watermark.blend_bgra(buf, stride, frame_w, frame_h);
1478            }
1479
1480            let published = pool.publish(
1481                RawFrame {
1482                    idx,
1483                    ptr: surface.addr,
1484                    len: surface.size,
1485                    width: cap_w,
1486                    height: cap_h,
1487                    stride,
1488                    generation: pool.generation(),
1489                    captured_ns: crate::wayland::host::now_ns(),
1490                },
1491                &controls.stop,
1492            );
1493            if !published {
1494                break;
1495            }
1496        }
1497        Ok(())
1498    })();
1499
1500    pool.shutdown();
1501    let _ = encode_thread.join();
1502    for s in surfaces.iter_mut() {
1503        s.destroy(&conn);
1504    }
1505    match result {
1506        // A real capture error is the more specific diagnosis, so it outranks the panic flag.
1507        Err(e) => Err(e),
1508        Ok(()) if encode_panicked.load(Ordering::Acquire) => Err("encode thread panicked".into()),
1509        Ok(()) => Ok(()),
1510    }
1511}
1512
1513/// Helpers the hardware checks of the zero-copy backends share: a known picture on the root of
1514/// `$DISPLAY`, and the color a decoded frame of it must average to.
1515#[cfg(test)]
1516pub(crate) mod gpu_test_support {
1517    use std::time::Duration;
1518
1519    use crate::RustCaptureSettings;
1520    use crate::encoders::codec::Codec;
1521    use crate::webcam::decode::{Decoder, VideoDecoder};
1522
1523    /// Full-frame capture settings for `codec` at CRF 25, streaming every frame, on the render
1524    /// node `auto_gpu` picks first.
1525    pub(crate) fn settings(codec: Codec) -> RustCaptureSettings {
1526        RustCaptureSettings {
1527            codec,
1528            target_fps: 60.0,
1529            video_crf: 25,
1530            video_streaming_mode: true,
1531            encode_node_index: crate::auto_render_node("true")
1532                .and_then(|node| crate::render_node_index(&node))
1533                .unwrap_or(0),
1534            ..Default::default()
1535        }
1536    }
1537
1538    /// Paint the whole root of `$DISPLAY` one solid color and let the server finish, so the
1539    /// next capture has a known picture in it.
1540    ///
1541    /// The screen saver is turned off first: a test display sees no input, so a server left
1542    /// with the default ten-minute blanking timeout hands the capture a black screen and every
1543    /// color comparison fails for a reason that has nothing to do with the capture.
1544    pub(crate) fn paint_root(rgb: (u8, u8, u8)) -> bool {
1545        let _ = std::process::Command::new("xset")
1546            .args(["s", "off", "s", "noblank"])
1547            .output();
1548        let _ = std::process::Command::new("xset")
1549            .arg("s")
1550            .arg("reset")
1551            .output();
1552        let spec = format!("#{:02x}{:02x}{:02x}", rgb.0, rgb.1, rgb.2);
1553        let out = std::process::Command::new("xsetroot")
1554            .args(["-solid", &spec])
1555            .output();
1556        if !out.map(|o| o.status.success()).unwrap_or(false) {
1557            return false;
1558        }
1559        std::thread::sleep(Duration::from_millis(120));
1560        true
1561    }
1562
1563    /// Limited-range Y/Cb/Cr of an 8-bit RGB triple, what the chroma convert emits for a
1564    /// captured surface.
1565    pub(crate) fn painted_ycbcr(rgb: (u8, u8, u8)) -> [f64; 3] {
1566        use crate::encoders::chroma_siting::{BT709, ycbcr};
1567        ycbcr([rgb.0, rgb.1, rgb.2].map(f64::from), BT709)
1568    }
1569
1570    /// Mean Y/Cb/Cr of a decoded picture.
1571    pub(crate) fn decoded_mean(dec: &mut VideoDecoder, payload: &[u8]) -> [f64; 3] {
1572        assert!(
1573            dec.decode(payload).expect("decode"),
1574            "no picture from this access unit"
1575        );
1576        let v = dec.frame().expect("decoded frame");
1577        let mut acc = [0f64; 3];
1578        let mut n = 0f64;
1579        for y in 0..v.height {
1580            for x in 0..v.width {
1581                acc[0] += v.y[y * v.y_stride + x] as f64;
1582                acc[1] += v.u[(y / 2) * v.uv_stride + x / 2] as f64;
1583                acc[2] += v.v[(y / 2) * v.uv_stride + x / 2] as f64;
1584                n += 1.0;
1585            }
1586        }
1587        [acc[0] / n, acc[1] / n, acc[2] / n]
1588    }
1589}
1590
1591#[cfg(test)]
1592mod pool_tests {
1593    use super::*;
1594
1595    fn dummy(idx: usize) -> RawFrame {
1596        RawFrame {
1597            idx,
1598            ptr: std::ptr::null_mut(),
1599            len: 0,
1600            width: 0,
1601            height: 0,
1602            stride: 0,
1603            generation: 0,
1604            captured_ns: 0,
1605        }
1606    }
1607
1608    /// A full acquire -> publish -> take -> recycle round-trip returns the same surface, and
1609    /// afterwards all three pool surfaces are acquirable again and distinct.
1610    #[test]
1611    fn roundtrip_then_recycle_returns_all_surfaces() {
1612        let p = FramePool::new(3);
1613        let stop = AtomicBool::new(false);
1614        let a = p.acquire(&stop).unwrap();
1615        assert!(p.publish(dummy(a), &stop));
1616        let f = p.take().unwrap();
1617        assert_eq!(f.idx, a);
1618        p.recycle(f.idx);
1619        let (x, y, z) = (
1620            p.acquire(&stop).unwrap(),
1621            p.acquire(&stop).unwrap(),
1622            p.acquire(&stop).unwrap(),
1623        );
1624        assert!(x != y && y != z && x != z);
1625    }
1626
1627    /// With every surface held and `stop` set, `acquire` returns `None` after its bounded wait
1628    /// rather than blocking forever.
1629    #[test]
1630    fn acquire_returns_none_when_exhausted_and_stopped() {
1631        let p = FramePool::new(2);
1632        let stop = AtomicBool::new(false);
1633        let _a = p.acquire(&stop).unwrap();
1634        let _b = p.acquire(&stop).unwrap();
1635        stop.store(true, Ordering::Relaxed);
1636        assert!(p.acquire(&stop).is_none());
1637    }
1638
1639    /// With the handoff slot already occupied and `stop` set, `publish` returns `false` (frame
1640    /// discarded) instead of blocking.
1641    #[test]
1642    fn publish_returns_false_when_slot_full_and_stopped() {
1643        let p = FramePool::new(3);
1644        let stop = AtomicBool::new(false);
1645        let a = p.acquire(&stop).unwrap();
1646        assert!(p.publish(dummy(a), &stop));
1647        let b = p.acquire(&stop).unwrap();
1648        stop.store(true, Ordering::Relaxed);
1649        assert!(!p.publish(dummy(b), &stop));
1650    }
1651
1652    /// `drain_for_resize` blocks until every held surface has been recycled (`free == n`), then
1653    /// reports a full drain and leaves the pool whole; a helper thread recycles the three held surfaces
1654    /// shortly after the drain begins.
1655    #[test]
1656    fn drain_for_resize_waits_until_all_free() {
1657        let p = Arc::new(FramePool::new(3));
1658        let stop = AtomicBool::new(false);
1659        let held = [
1660            p.acquire(&stop).unwrap(),
1661            p.acquire(&stop).unwrap(),
1662            p.acquire(&stop).unwrap(),
1663        ];
1664        let p2 = p.clone();
1665        let t = thread::spawn(move || {
1666            thread::sleep(Duration::from_millis(30));
1667            for idx in held {
1668                p2.recycle(idx);
1669            }
1670        });
1671        assert!(p.drain_for_resize(3, &stop));
1672        t.join().unwrap();
1673        assert!(p.acquire(&stop).is_some());
1674    }
1675
1676    /// A surface is held and never recycled (as a dead encode thread would leave it): setting
1677    /// `stop` unblocks the bounded drain, which reports it did NOT fully drain instead of hanging.
1678    #[test]
1679    fn drain_for_resize_aborts_on_stop() {
1680        let p = FramePool::new(3);
1681        let stop = AtomicBool::new(false);
1682        let _held = p.acquire(&stop).unwrap();
1683        stop.store(true, Ordering::Relaxed);
1684        assert!(!p.drain_for_resize(3, &stop));
1685    }
1686
1687    /// A W1 -> W2 -> W1 resize flap where the encoder never takes the W2 frame:
1688    /// `drain_for_resize` reclaims the un-taken frame from the slot, the surfaces recreate twice, and
1689    /// the next taken frame lands back on the ORIGINAL dimensions — so the bumped generation is the
1690    /// only rebuild signal that invalidates encoder state keyed to the reused surface addresses.
1691    #[test]
1692    fn resize_flap_reclaim_changes_generation_at_identical_dims() {
1693        let p = FramePool::new(3);
1694        let stop = AtomicBool::new(false);
1695        let a = p.acquire(&stop).unwrap();
1696        assert!(p.publish(
1697            RawFrame {
1698                generation: p.generation(),
1699                width: 1920,
1700                height: 1080,
1701                ..dummy(a)
1702            },
1703            &stop
1704        ));
1705        let f = p.take().unwrap();
1706        let (last_gen, last_dims) = (f.generation, (f.width, f.height));
1707        p.recycle(f.idx);
1708        assert!(p.drain_for_resize(3, &stop));
1709        p.bump_generation();
1710        let b = p.acquire(&stop).unwrap();
1711        assert!(p.publish(
1712            RawFrame {
1713                generation: p.generation(),
1714                width: 2560,
1715                height: 1600,
1716                ..dummy(b)
1717            },
1718            &stop
1719        ));
1720        assert!(p.drain_for_resize(3, &stop));
1721        p.bump_generation();
1722        let c = p.acquire(&stop).unwrap();
1723        assert!(p.publish(
1724            RawFrame {
1725                generation: p.generation(),
1726                width: 1920,
1727                height: 1080,
1728                ..dummy(c)
1729            },
1730            &stop
1731        ));
1732        let g = p.take().unwrap();
1733        assert_eq!(
1734            (g.width, g.height),
1735            last_dims,
1736            "flap lands on identical dimensions"
1737        );
1738        assert_eq!(g.generation, 2);
1739        assert_ne!(
1740            g.generation, last_gen,
1741            "generation is the only rebuild signal"
1742        );
1743    }
1744
1745    /// Recreating the surfaces bumps the generation, and frames published afterwards carry the
1746    /// new value — the signal the encode thread uses to trigger a rebuild at identical dimensions.
1747    #[test]
1748    fn generation_bumps_on_recreate_and_rides_published_frames() {
1749        let p = FramePool::new(3);
1750        let stop = AtomicBool::new(false);
1751        assert_eq!(p.generation(), 0);
1752        let a = p.acquire(&stop).unwrap();
1753        assert!(p.publish(
1754            RawFrame {
1755                generation: p.generation(),
1756                ..dummy(a)
1757            },
1758            &stop
1759        ));
1760        let f = p.take().unwrap();
1761        assert_eq!(f.generation, 0);
1762        p.recycle(f.idx);
1763        p.bump_generation();
1764        let b = p.acquire(&stop).unwrap();
1765        assert!(p.publish(
1766            RawFrame {
1767                generation: p.generation(),
1768                ..dummy(b)
1769            },
1770            &stop
1771        ));
1772        assert_eq!(p.take().unwrap().generation, 1);
1773    }
1774}
1775
1776#[cfg(test)]
1777mod cursor_tests {
1778    use super::*;
1779
1780    /// `cursor_image_origin` subtracts both the hotspot and the capture-region offset from the
1781    /// reported pointer position, and goes negative when the hotspot sits near the frame origin (the
1782    /// result is clipped when drawn).
1783    #[test]
1784    fn origin_subtracts_hotspot_and_capture_offset() {
1785        assert_eq!(cursor_image_origin(100, 80, 4, 6, 0, 0), (96, 74));
1786        assert_eq!(cursor_image_origin(100, 80, 4, 6, 10, 20), (86, 54));
1787        assert_eq!(cursor_image_origin(1, 1, 8, 8, 0, 0), (-7, -7));
1788    }
1789
1790    /// `overlay_cursor` blits at the hotspot-offset origin, not the raw pointer position: a
1791    /// 2x2 cursor with hotspot (1,1) at pointer (4,4) lands at (3,3)..(4,4), leaving the un-offset
1792    /// (hotspot) corner untouched.
1793    #[test]
1794    fn overlay_blits_at_hotspot_offset_origin() {
1795        let stride = 8 * 4;
1796        let mut frame = vec![0u8; stride * 8];
1797        let pixels = [0xFFFF_FFFFu32; 4];
1798        let (ox, oy) = cursor_image_origin(4, 4, 1, 1, 0, 0);
1799        overlay_cursor(&mut frame, stride, 8, 8, 2, 2, &pixels, ox, oy);
1800        let px = |x: usize, y: usize| frame[y * stride + x * 4];
1801        assert_eq!(px(3, 3), 255);
1802        assert_eq!(px(4, 4), 255);
1803        assert_eq!(px(2, 2), 0);
1804        assert_eq!(px(5, 5), 0, "no pixel at the un-offset (hotspot) corner");
1805    }
1806
1807    #[test]
1808    fn offset_clamps_into_root() {
1809        assert_eq!(clamp_offset(100, 1920), 100);
1810        assert_eq!(clamp_offset(-50, 1920), 0);
1811        // Past-end offsets keep one root pixel instead of failing forever.
1812        assert_eq!(clamp_offset(100000, 1920), 1919);
1813        // A protocol-max root still maps into i16 X coordinates.
1814        assert_eq!(clamp_offset(40000, u16::MAX), 32766);
1815    }
1816
1817    #[test]
1818    fn resolve_dims_survives_past_end_offset() {
1819        let s = RustCaptureSettings {
1820            capture_x: 100000,
1821            capture_y: 100000,
1822            width: 1920,
1823            height: 1080,
1824            codec: crate::encoders::Codec::H264,
1825            ..Default::default()
1826        };
1827        let (w, h) = resolve_dims(1920, 1080, &s);
1828        assert!(
1829            w >= 2 && h >= 2,
1830            "a past-end offset now degrades to a small grab, not death"
1831        );
1832        assert_eq!(w % 2, 0);
1833        assert_eq!(h % 2, 0);
1834    }
1835
1836    /// `overlay_cursor` treats XFixes pixels as premultiplied: half-alpha at half
1837    /// intensity stays at that intensity over black (the straight-alpha formula
1838    /// would multiply alpha twice and darken it).
1839    #[test]
1840    fn overlay_blends_premultiplied_pixels() {
1841        let stride = 4 * 4;
1842        let mut frame = vec![0u8; stride * 4];
1843        // a=128, g=128 (premultiplied half-intensity), b=64.
1844        let pixels = [0x8000_8040u32; 4];
1845        overlay_cursor(&mut frame, stride, 4, 4, 2, 2, &pixels, 0, 0);
1846        assert_eq!(frame[0], 64, "blue channel: dst = 64 over black");
1847        assert_eq!(
1848            frame[1], 128,
1849            "premultiplied half-alpha stays 128, not darkened to 64"
1850        );
1851    }
1852
1853    /// `overlay_cursor` clips a negative origin at the frame edge: with the origin at (-1,-1)
1854    /// only the in-frame quadrant is written, landing the cursor's bottom-right pixel at (0,0).
1855    #[test]
1856    fn overlay_clips_negative_origin_at_frame_edge() {
1857        let stride = 4 * 4;
1858        let mut frame = vec![0u8; stride * 4];
1859        let pixels = [0xFFFF_FFFFu32; 4];
1860        let (ox, oy) = cursor_image_origin(0, 0, 1, 1, 0, 0);
1861        overlay_cursor(&mut frame, stride, 4, 4, 2, 2, &pixels, ox, oy);
1862        assert_eq!(frame[0], 255);
1863        assert!(
1864            frame[4..].iter().all(|&b| b == 0),
1865            "no writes outside (0,0)"
1866        );
1867    }
1868}
1869
1870#[cfg(test)]
1871mod damage_tests {
1872    //! The `x11_damage` tests need an X server of their own, quiet enough that nothing else
1873    //! repaints it: `DISPLAY=:N cargo test x11_damage -- --ignored --nocapture`.
1874
1875    use super::*;
1876    use crate::pipeline::Damage as Changed;
1877    use x11rb::protocol::xproto::CreateGCAux;
1878
1879    const PERIOD: Duration = Duration::from_millis(400);
1880
1881    fn repaint(color: u32) {
1882        fill(color, 0, 0, 64, 64);
1883    }
1884
1885    fn fill(color: u32, x: i16, y: i16, width: u16, height: u16) {
1886        let (conn, screen) = x11rb::connect(None).expect("connect");
1887        let root = conn.setup().roots[screen].root;
1888        let gc = conn.generate_id().expect("id");
1889        conn.create_gc(gc, root, &CreateGCAux::new().foreground(color))
1890            .expect("gc");
1891        conn.poly_fill_rectangle(
1892            root,
1893            gc,
1894            &[Rectangle {
1895                x,
1896                y,
1897                width,
1898                height,
1899            }],
1900        )
1901        .expect("fill");
1902        conn.flush().expect("flush");
1903    }
1904
1905    /// The root's damaged rectangles read in the captured area's own coordinates, clipped to it.
1906    #[test]
1907    fn the_root_damage_reads_as_the_captured_share() {
1908        let caret = [Rectangle {
1909            x: 110,
1910            y: 60,
1911            width: 2,
1912            height: 20,
1913        }];
1914        assert_eq!(
1915            captured_damage(&caret, 100, 50, 200, 100),
1916            Changed::Area(0.002)
1917        );
1918        assert_eq!(
1919            captured_damage(&caret, 120, 50, 200, 100),
1920            Changed::None,
1921            "left of the captured area"
1922        );
1923        let wide = [Rectangle {
1924            x: 0,
1925            y: 0,
1926            width: 1000,
1927            height: 1000,
1928        }];
1929        assert_eq!(
1930            captured_damage(&wide, 100, 50, 200, 100),
1931            Changed::Area(1.0)
1932        );
1933        assert_eq!(captured_damage(&[], 0, 0, 200, 100), Changed::None);
1934    }
1935
1936    /// A caret-sized repaint reads as that small a share of the screen, and one outside the
1937    /// captured area as none of it.
1938    #[test]
1939    #[ignore]
1940    fn x11_damage_says_where_the_root_changed() {
1941        let (conn, screen) = x11rb::connect(None).expect("connect");
1942        let root = conn.setup().roots[screen].root;
1943        let (w, h) = (
1944            i32::from(conn.setup().roots[screen].width_in_pixels),
1945            i32::from(conn.setup().roots[screen].height_in_pixels),
1946        );
1947        let damage = RootDamage::create(&conn, root).expect("the server carries DAMAGE");
1948        damage.clear(&conn);
1949        assert!(
1950            damage.take(&conn).is_some_and(|rects| rects.is_empty()),
1951            "nothing reported yet"
1952        );
1953        let mut pace = FramePace::default();
1954        pace.ticked(TickTrigger::Timer, PERIOD, Instant::now(), false);
1955        fill(0x00ff_ffff, 300, 200, 2, 20);
1956        assert_eq!(
1957            wait_for_frame(&conn, Some(&damage), &pace, PERIOD),
1958            TickTrigger::Damage
1959        );
1960        let asked = Instant::now();
1961        let rects = damage.take(&conn).expect("the server says where");
1962        let took = asked.elapsed();
1963        let caret = captured_damage(&rects, 0, 0, w, h);
1964        assert_eq!(
1965            caret,
1966            Changed::Area((40.0 / (w * h) as f64) as f32),
1967            "{rects:?}"
1968        );
1969        assert_eq!(
1970            captured_damage(&rects, 400, 0, w - 400, h),
1971            Changed::None,
1972            "right of the caret"
1973        );
1974        println!("a 2x20 caret on a {w}x{h} root reads as {caret:?}, read in {took:?}");
1975    }
1976
1977    /// A screen that changes mid-period publishes the change then, while a screen that does not
1978    /// keeps the cadence: the latency of a change is its own, not the frame rate's.
1979    #[test]
1980    #[ignore]
1981    fn x11_damage_publishes_a_change_before_the_next_tick() {
1982        let (conn, screen) = x11rb::connect(None).expect("connect");
1983        let root = conn.setup().roots[screen].root;
1984        let damage = RootDamage::create(&conn, root).expect("the server carries DAMAGE");
1985        let mut pace = FramePace::default();
1986
1987        pace.ticked(TickTrigger::Timer, PERIOD, Instant::now(), false);
1988        damage.clear(&conn);
1989        let start = Instant::now();
1990        let quiet = wait_for_frame(&conn, Some(&damage), &pace, PERIOD);
1991        let waited = start.elapsed();
1992        assert_eq!(
1993            quiet,
1994            TickTrigger::Timer,
1995            "a quiet screen is the timer's to pace"
1996        );
1997        assert!(
1998            waited >= PERIOD.mul_f64(0.9),
1999            "the timer's own tick came after {waited:?}"
2000        );
2001
2002        pace.ticked(TickTrigger::Timer, PERIOD, Instant::now(), false);
2003        damage.clear(&conn);
2004        let painter = thread::spawn(|| {
2005            thread::sleep(PERIOD.mul_f64(0.6));
2006            repaint(0x0020_4060);
2007        });
2008        let start = Instant::now();
2009        let pulled = wait_for_frame(&conn, Some(&damage), &pace, PERIOD);
2010        let waited = start.elapsed();
2011        painter.join().expect("painter");
2012        assert_eq!(
2013            pulled,
2014            TickTrigger::Damage,
2015            "the repaint is what asked for the frame"
2016        );
2017        assert!(
2018            waited < PERIOD.mul_f64(0.9),
2019            "the repaint was published after {waited:?}"
2020        );
2021        println!(
2022            "repaint at 0.6 of a period published after {:.0}ms",
2023            waited.as_secs_f64() * 1000.0
2024        );
2025    }
2026}
2027
2028#[cfg(test)]
2029mod vblank_tests {
2030    //! Needs an X server of its own: `DISPLAY=:N cargo test x11_vblank -- --ignored`.
2031
2032    use super::*;
2033
2034    fn capture_at(fps: f64) -> Arc<Controls> {
2035        Arc::new(Controls::new(&RustCaptureSettings {
2036            target_fps: fps,
2037            ..Default::default()
2038        }))
2039    }
2040
2041    /// The root window's `_FAKE_SCREEN_FPS` and `_FAKE_SCREEN_MILLIHZ`.
2042    fn published() -> (Option<u32>, Option<u32>) {
2043        (
2044            property(b"_FAKE_SCREEN_FPS"),
2045            property(b"_FAKE_SCREEN_MILLIHZ"),
2046        )
2047    }
2048
2049    fn property(name: &[u8]) -> Option<u32> {
2050        let (conn, screen) = x11rb::connect(None).expect("connect");
2051        let root = conn.setup().roots[screen].root;
2052        let atom = conn
2053            .intern_atom(false, name)
2054            .expect("intern")
2055            .reply()
2056            .expect("atom")
2057            .atom;
2058        let reply = conn
2059            .get_property(false, root, atom, AtomEnum::CARDINAL, 0, 1)
2060            .expect("get_property")
2061            .reply()
2062            .expect("reply");
2063        reply.value32().and_then(|mut v| v.next())
2064    }
2065
2066    /// The fastest running capture sets the rate, a change of rate follows, a fractional one to the
2067    /// millihertz, and the last capture to end takes the properties away.
2068    #[test]
2069    #[ignore]
2070    fn x11_vblank_rate_follows_the_fastest_capture() {
2071        let fast = capture_at(144.0);
2072        let slow = capture_at(90.0);
2073        let fast_claim = VblankClaim::new(&fast);
2074        let slow_claim = VblankClaim::new(&slow);
2075        assert_eq!(published(), (Some(144), Some(144_000)));
2076        drop(fast_claim);
2077        assert_eq!(published(), (Some(90), Some(90_000)));
2078        slow.fps_bits.store(119.6f64.to_bits(), Ordering::Relaxed);
2079        follow_frame_rate();
2080        assert_eq!(published(), (Some(120), Some(119_600)));
2081        slow.fps_bits
2082            .store((60000.0f64 / 1001.0).to_bits(), Ordering::Relaxed);
2083        follow_frame_rate();
2084        assert_eq!(published(), (Some(60), Some(59_940)));
2085        drop(slow_claim);
2086        assert_eq!(published(), (None, None));
2087    }
2088}