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}