Skip to main content

pixelflux/
lib.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/*
8  ▘    ▜ ▐▘▜
9▛▌▌▚▘█▌▐ ▜▘▐ ▌▌▚▘
10▙▌▌▞▖▙▖▐▖▐ ▐▖▙▌▞▖
1112*/
13
14//! # pixelflux
15//!
16//! A high-performance screen capture and encoding pipeline exposed as a Python extension via
17//! PyO3. It supports two independent backends — **X11** (XShm + XFixes) and **Wayland**
18//! (a headless [Smithay](https://github.com/Smithay/smithay) compositor) — and a shared
19//! encoding layer that dispatches to software (striped JPEG, and H.264 through the build's
20//! software encoder: libx264 with the `gpl` feature, OpenH264 without) or hardware (NVENC,
21//! VA-API) encoders based on the available GPU and operator settings.
22//!
23//! ## Crate structure
24//!
25//! | Module | Purpose |
26//! |--------|---------|
27//! | [`encoders`] | Encoder backends: software H.264 (libx264 or OpenH264) / JPEG, NVENC, VA-API, watermark overlay |
28//! | [`wayland`] | Headless Smithay compositor, cursor rendering |
29//! | [`x11`] | X11/XShm capture loop, XFixes out-of-band cursor monitor, and stripe dispatch |
30//! | [`pipeline`] | Frame-processing policy shared by both backends (send/QP/keyframe decisions) |
31//! | [`recording_sink`] | Unix-socket H.264 fan-out for external recording |
32//! | [`recorder`] | Built-in MP4 recorder (fMP4 muxer + Python/env/REST control surfaces) |
33//! | [`computer_use`] | HTTP API for AI-agent desktop control (screenshots, input injection) |
34//! | [`nvgpufilter`] | Multi-GPU NVENC device filtering via ioctl |
35//! | [`webcam`] | Virtual camera: client webcam uplink decoded into a V4L2 device (interposer ring, v4l2loopback, PipeWire node) |
36//!
37//! ## Data flow
38//!
39//! ```text
40//! Python  ──►  CaptureSettings  ──►  X11 / Wayland backend
41//!                                         │
42//!                                    frame pixels
43//!                                         │
44//!                                    ┌────┴────┐
45//!                                    │ Encoder │  (NVENC / VAAPI / x264 or OpenH264 / JPEG)
46//!                                    └────┬────┘
47//!                                         │
48//!                                   EncodedStripe(s)
49//!                                         │
50//!                                    Python callback
51//! ```
52
53#![allow(dead_code)]
54
55use std::fs::File;
56use std::sync::Arc;
57use std::thread;
58use std::time::{Duration, Instant};
59
60use gbm::{BufferObject, BufferObjectFlags, Device as RawGbmDevice, Format as GbmFormat};
61use pyo3::prelude::*;
62use pyo3::types::{PyAny, PyModule};
63use yuv::{
64    BufferStoreMut, YuvBiPlanarImageMut, YuvConversionMode, YuvRange, YuvStandardMatrix,
65};
66
67use smithay::wayland::single_pixel_buffer::SinglePixelBufferState;
68use smithay::wayland::viewporter::ViewporterState;
69use smithay::wayland::presentation::{PresentationState, Refresh};
70use smithay::wayland::image_capture_source::{ImageCaptureSourceState, OutputCaptureSourceState};
71use smithay::wayland::image_copy_capture::{CaptureFailureReason, ImageCopyCaptureState};
72use smithay::desktop::utils::{send_frames_surface_tree, OutputPresentationFeedback};
73use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
74use smithay::wayland::selection::wlr_data_control::DataControlState;
75use smithay::wayland::selection::ext_data_control::DataControlState as ExtDataControlState;
76use smithay::wayland::cursor_shape::CursorShapeManagerState;
77use smithay::{
78    backend::{
79        allocator::{
80            dmabuf::{Dmabuf, DmabufFlags},
81            gbm::GbmDevice,
82            Fourcc, Modifier,
83        },
84        drm::DrmNode,
85        egl::{EGLContext, EGLDisplay},
86        input::{Axis, AxisSource, KeyState, Keycode},
87        renderer::{
88            damage::OutputDamageTracker,
89            element::{
90                memory::MemoryRenderBufferRenderElement,
91                surface::WaylandSurfaceRenderElement,
92                AsRenderElements, Element, RenderElement, Wrap,
93            },
94            gles::GlesRenderer,
95            pixman::PixmanRenderer,
96            sync::SyncPoint,
97            Bind, ExportMem, Frame as _, ImportAll, ImportDma, ImportEgl, ImportMem,
98            Renderer as _,
99        },
100    },
101    desktop::{space::SpaceRenderElements, Space},
102    input::{
103        keyboard::{FilterResult, XkbConfig},
104        pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent, RelativeMotionEvent},
105        SeatState,
106    },
107    output::{Mode as OutputMode, Output, PhysicalProperties, Scale as OutputScale, Subpixel},
108    reexports::{
109        calloop::{
110            generic::Generic, timer::{TimeoutAction, Timer},
111            EventLoop, Interest, Mode, PostAction,
112        },
113        pixman,
114        wayland_server::{Display, DisplayHandle},
115    },
116    utils::{Clock, Physical, Point, Rectangle, Scale, Transform},
117    wayland::{
118        compositor::{with_states, CompositorState},
119        dmabuf::{DmabufFeedbackBuilder, DmabufState},
120        fractional_scale::FractionalScaleManagerState,
121        output::OutputManagerState,
122        selection::data_device::DataDeviceState,
123        seat::WaylandFocus,
124        shell::xdg::XdgShellState,
125        shm::ShmState,
126        socket::ListeningSocketSource,
127        pointer_warp::PointerWarpManager,
128        relative_pointer::RelativePointerManagerState,
129        pointer_constraints::PointerConstraintsState,
130        foreign_toplevel_list::ForeignToplevelListState,
131        shell::xdg::decoration::XdgDecorationState,
132    },
133    desktop::{layer_map_for_output, PopupManager},
134    wayland::shell::wlr_layer::WlrLayerShellState,
135    wayland::xdg_activation::XdgActivationState,
136    wayland::selection::primary_selection::PrimarySelectionState,
137};
138
139pub mod encoders {
140    /// NVIDIA NVENC hardware H.264 encoder loaded via runtime `libcuda` / `libnvidia-encode`.
141    pub mod nvenc;
142    /// Cisco OpenH264 software H.264 encoder (BSD-licensed): the software encoder of a build
143    /// without `gpl`, and always built for the test suite.
144    #[cfg(any(feature = "openh264", test))]
145    pub mod oh264;
146    /// PNG watermark overlay composited onto frames before encoding.
147    pub mod overlay;
148    /// CPU-based striped H.264 (libx264 or OpenH264, by build) / JPEG encoder with per-stripe
149    /// change detection.
150    pub mod software;
151    /// VA-API hardware H.264 encoder for Intel / AMD GPUs via FFmpeg.
152    pub mod vaapi;
153
154    #[cfg(not(any(feature = "gpl", feature = "openh264")))]
155    compile_error!(
156        "pixelflux needs a software H.264 encoder: enable the `gpl` feature (libx264, the default) or `openh264`."
157    );
158
159    /// The software H.264 encoder this build resolved to, fixed by the crate features: `"x264"`
160    /// whenever `gpl` is on (libx264 wins even if `openh264` is also enabled), `"openh264"` for
161    /// a GPL-free build. It is what the striped software path and the full-frame software fallback
162    /// under NVENC/VA-API both encode with; exposed to Python as `pixelflux.SOFTWARE_H264_ENCODER`.
163    #[cfg(feature = "gpl")]
164    pub const SOFTWARE_H264_ENCODER: &str = "x264";
165    #[cfg(not(feature = "gpl"))]
166    pub const SOFTWARE_H264_ENCODER: &str = "openh264";
167
168    /// Whether the build's software H.264 encoder carries a 4:4:4 (`video_fullcolor`) request:
169    /// libx264 does (High 4:4:4, full range); OpenH264 is 4:2:0-only and encodes such a request
170    /// 4:2:0. Every "is this software stream 4:4:4" decision reads it from here.
171    pub const SOFTWARE_H264_FULLCOLOR: bool = cfg!(feature = "gpl");
172
173    /// Damps visible quality "blinking": the number of consecutive frames a QP *increase* (a
174    /// quality drop under sustained motion) must be requested before a fixed-QP encoder commits
175    /// it. Moving the quantizer costs a codec re-open (VA-API CQP) or a full encoder rebuild
176    /// (OpenH264), and either forces an IDR, so acting on every transient increase — and then
177    /// reversing it as motion settles — would make the picture pulse. Quality *increases* (a
178    /// lower QP, e.g. a paint-over refresh) apply at once and never wait. Shared so the two
179    /// fixed-QP encoders cannot disagree about how long a drop must persist.
180    pub(crate) const QP_HYSTERESIS_LIMIT: u32 = 60;
181
182    /// Lowest H.264 level whose Annex-A Table A-1 limits admit a `width` x `height` stream at
183    /// `fps`, as level_idc (41 = 4.1, 52 = 5.2, 62 = 6.2).
184    ///
185    /// A frame is charged two ways: **MaxFS**, its size in macroblocks, and **MaxMBPS**, that
186    /// size times the frame rate. Advertising the lowest fitting level asks the least of a
187    /// decoder, so clients that gate hardware decode on the level accept the widest range of
188    /// streams. One shared ladder keeps two encoders from disagreeing on the same geometry;
189    /// it starts at 4.1 and a caller needing more raises the floor itself. Above 6.2's limits
190    /// there is no higher level, so it returns 62 as a best effort.
191    pub(crate) fn min_h264_level(width: u32, height: u32, fps: u32) -> u32 {
192        let mbs = (width as u64).div_ceil(16) * (height as u64).div_ceil(16);
193        let mbps = mbs * fps.max(1) as u64;
194        const LEVELS: [(u32, u64, u64); 8] = [
195            (41, 8192, 245760),
196            (42, 8704, 522240),
197            (50, 22080, 589824),
198            (51, 36864, 983040),
199            (52, 36864, 2073600),
200            (60, 139264, 4177920),
201            (61, 139264, 8355840),
202            (62, 139264, 16711680),
203        ];
204        for &(level, max_fs, max_mbps) in &LEVELS {
205            if mbs <= max_fs && mbps <= max_mbps {
206                return level;
207            }
208        }
209        62
210    }
211
212    /// Size the CBR VBV/HRD buffer so rate control has enough slack to hold quality steady
213    /// without letting end-to-end latency drift upward.
214    ///
215    /// The size is expressed as a multiple of one frame's bit budget (`bitrate_bps / fps`) rather
216    /// than a fixed byte count so a live bitrate or framerate change rescales the buffer with it,
217    /// preserving the same latency behavior at every operating point.
218    ///
219    /// # Arguments
220    ///
221    /// * `bitrate_bps` - Target bitrate in bits per second.
222    /// * `fps` - Target frames per second.
223    /// * `keyframe_interval_s` - Seconds between scheduled keyframes; `<= 0` for infinite GOP.
224    /// * `multiplier` - Explicit buffer multiplier; `<= 0` selects the policy default (1.5 on
225    ///   infinite GOP, 3 when keyframe interval is active).
226    ///
227    /// # Returns
228    ///
229    /// VBV buffer size in bits, clamped to `[1, u32::MAX]`.
230    pub fn vbv_bits(bitrate_bps: u32, fps: f64, keyframe_interval_s: f64, multiplier: f64) -> u32 {
231        let frame_bits = bitrate_bps as f64 / fps.max(1.0);
232        let mult = if multiplier > 0.0 {
233            multiplier
234        } else if keyframe_interval_s > 0.0 {
235            3.0
236        } else {
237            1.5
238        };
239        (frame_bits * mult).round().max(1.0).min(u32::MAX as f64) as u32
240    }
241
242    /// The `Colorspace:` field of a stream log line, from what the session negotiated rather than
243    /// what was asked for: a hardware encoder can refuse 4:4:4, and only the software encoder
244    /// carries it at full range. Shared so the X11 and Wayland logs describe an identical session
245    /// identically.
246    pub fn colorspace_desc(fullcolor: bool, software: bool) -> &'static str {
247        match (fullcolor, software) {
248            (true, true) => "I444 (Full Range)",
249            (true, false) => "I444 (Limited Range)",
250            _ => "I420 (Limited Range)",
251        }
252    }
253}
254
255/// Headless Wayland compositor and cursor rendering.
256pub mod wayland;
257/// Unix-socket H.264 recording fan-out for external capture tools.
258pub mod recording_sink;
259/// Built-in MP4 recorder: independent capture-to-file with Python/env/REST control.
260pub mod recorder;
261/// HTTP server implementing the Anthropic Computer Use spec for AI agent desktop control.
262pub mod computer_use;
263/// Frame-processing policy shared by the X11 and Wayland backends.
264pub mod pipeline;
265/// X11/XShm capture loop, stripe dispatch, and per-stripe change detection.
266pub mod x11;
267/// Multi-GPU NVENC device filtering via kernel ioctl.
268pub mod nvgpufilter;
269
270pub mod webcam;
271
272pub use encoders::nvenc;
273pub use encoders::software::StripeState;
274pub use encoders::vaapi;
275
276fn get_process_rss_bytes() -> usize {
277    if let Ok(contents) = std::fs::read_to_string("/proc/self/statm")
278        && let Some(rss_pages) = contents.split_whitespace().nth(1)
279        && let Ok(pages) = rss_pages.parse::<usize>() {
280                return pages * 4096;
281            }
282    0
283}
284
285/// Blocks actually allocated, not apparent size, so a sparse file is not reported as
286/// memory the process took. Feeds the debug log line only.
287fn shm_usage_in(dir: &str) -> u64 {
288    use std::os::unix::fs::MetadataExt;
289    let mut total_size = 0;
290    if let Ok(entries) = std::fs::read_dir(dir) {
291        for entry in entries.flatten() {
292            if let Ok(metadata) = entry.metadata() {
293                total_size += metadata.blocks() * 512;
294            }
295        }
296    }
297    total_size
298}
299
300fn get_shm_usage_bytes() -> u64 {
301    shm_usage_in("/dev/shm")
302}
303
304use encoders::nvenc::NvencEncoder;
305use encoders::overlay::OverlayState;
306use encoders::software::MAX_STRIPE_CAPACITY;
307use encoders::vaapi::VaapiEncoder;
308
309use smithay::reexports::wayland_protocols_misc::zwp_virtual_keyboard_v1::server::zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1;
310
311use wayland::cursor::{Cursor, CursorJob};
312use wayland::frontend::{AppState, ClientState, FocusTarget, GpuEncoder, next_serial, wayland_time, wayland_utime};
313
314smithay::backend::renderer::element::render_elements! {
315    pub CompositionElements<R, E> where R: ImportAll + ImportMem;
316    Space=SpaceRenderElements<R, E>,
317    Window=Wrap<E>,
318    Cursor=MemoryRenderBufferRenderElement<R>,
319    Surface=WaylandSurfaceRenderElement<R>,
320}
321
322/// Export the offscreen GBM render target as a Dmabuf so the very same GPU pixels can be both
323/// rendered into and encoded with no intervening copy — the linchpin of the zero-copy capture path.
324/// The returned dmabuf is the one handle the GLES renderer binds as its framebuffer AND a hardware
325/// encoder (NVENC through CUDA, or VAAPI) imports to read those pixels directly, which only works if
326/// the buffer is described precisely enough (fd, stride, DRM modifier) for the importer to interpret
327/// it. One ARGB8888 plane is all that is carried because the compositor's offscreen target is exactly
328/// that single-plane format.
329pub(crate) fn create_dmabuf_from_bo(bo: &BufferObject<()>) -> Dmabuf {
330    let fd = bo.fd().expect("Failed to get FD from GBM BO");
331    let modifier = bo.modifier();
332    let stride = bo.stride();
333    let width = bo.width();
334    let height = bo.height();
335
336    let drm_modifier = Modifier::from(Into::<u64>::into(modifier));
337
338    let mut builder = Dmabuf::builder(
339        (width as i32, height as i32),
340        Fourcc::Argb8888,
341        drm_modifier,
342        DmabufFlags::empty(),
343    );
344
345    builder.add_plane(fd, 0, 0, stride);
346    builder.build().expect("Failed to build Dmabuf from GBM BO")
347}
348
349/// The full set of capture + encode parameters the Python layer hands to the Rust backend.
350///
351/// A single value configures a capture session end to end: capture geometry and frame rate, the
352/// output mode (striped JPEG/x264 vs full-frame H.264), the H.264 quality and rate-control knobs,
353/// cursor and watermark options, the encode-device selection, and the optional recording socket.
354/// It derives `PartialEq` so the backend can detect when a live setting actually changed, and
355/// `Clone` so each capture pipeline can own its own copy.
356#[derive(Clone, Debug, PartialEq)]
357pub struct RustCaptureSettings {
358    pub width: i32,
359    pub height: i32,
360    pub scale: f64,
361    pub capture_x: i32,
362    pub capture_y: i32,
363    pub target_fps: f64,
364    pub jpeg_quality: i32,
365    pub paint_over_jpeg_quality: i32,
366    pub use_paint_over_quality: bool,
367    pub paint_over_trigger_frames: u32,
368    pub damage_block_threshold: u32,
369    pub damage_block_duration: u32,
370    pub output_mode: i32,
371    pub video_crf: i32,
372    pub video_paintover_crf: i32,
373    pub video_paintover_burst_frames: i32,
374    pub video_fullcolor: bool,
375    pub video_fullframe: bool,
376    pub video_streaming_mode: bool,
377    pub capture_cursor: bool,
378    /// Longest cursor edge the out-of-band cursor callback delivers; larger images are
379    /// downscaled (`<= 0` = uncapped). Compositing via `capture_cursor` is unaffected.
380    pub cursor_size_cap: i32,
381    pub watermark_path: String,
382    pub watermark_location_enum: i32,
383    pub encode_node_index: i32,
384    pub use_cpu: bool,
385    pub debug_logging: bool,
386    pub auto_adjust_screen_capture_size: bool,
387    pub recording_socket: String,
388    /// Wayland display of an EXTERNAL compositor to capture (host-capture mode);
389    /// empty composites own clients as usual.
390    pub wayland_host_display: String,
391    /// When true, encoders emit the raw payload without the per-stripe header byte block;
392    /// stripe metadata is then carried only on the frame attributes.
393    pub omit_stripe_headers: bool,
394    pub video_cbr_mode: bool,
395    pub video_bitrate_kbps: i32,
396    /// CBR VBV/HRD size as a multiple of one frame's bit budget (bitrate/framerate), so it
397    /// rescales with live bitrate/fps changes. `<= 0` selects the policy default: 1.5 on an
398    /// infinite GOP, 3 when scheduled keyframes are enabled.
399    pub video_vbv_multiplier: f64,
400    /// Seconds between scheduled recovery keyframes; `<= 0` keeps the GOP infinite
401    /// (IDRs only on demand: client join / reset, recorder connect).
402    pub keyframe_interval_s: f64,
403    /// Rate-controlled (CBR) QP clamp: `video_max_qp` bounds the quality FLOOR (screen text stays
404    /// legible under motion at the cost of overshooting impossible targets), `video_min_qp` bounds
405    /// bit WASTE on easy content. 0 keeps the encoder's own default; CRF/CQP modes pin their QP
406    /// directly and ignore these.
407    pub video_min_qp: i32,
408    pub video_max_qp: i32,
409}
410
411/// The per-frame decision/quality knobs every encoder re-reads from the settings on each
412/// tick, so they retune a running capture with no encoder re-init: x264 reconfigures, NVENC
413/// CQP retargets, VAAPI re-opens only its codec ctx, JPEG is stateless. Applied on the
414/// thread that owns the settings copy. Structural switches (encoder, chroma, RC mode,
415/// device) still need a capture restart.
416#[derive(Clone, Copy, Debug)]
417pub struct LiveTunables {
418    pub jpeg_quality: i32,
419    pub paint_over_jpeg_quality: i32,
420    pub use_paint_over_quality: bool,
421    pub paint_over_trigger_frames: u32,
422    pub video_crf: i32,
423    pub video_paintover_crf: i32,
424    pub video_paintover_burst_frames: i32,
425    pub video_streaming_mode: bool,
426    pub keyframe_interval_s: f64,
427    pub capture_cursor: bool,
428    pub cursor_size_cap: i32,
429}
430
431impl LiveTunables {
432    /// Snapshot the live-tunable subset out of a full settings value.
433    pub fn from_settings(s: &RustCaptureSettings) -> Self {
434        Self {
435            jpeg_quality: s.jpeg_quality,
436            paint_over_jpeg_quality: s.paint_over_jpeg_quality,
437            use_paint_over_quality: s.use_paint_over_quality,
438            paint_over_trigger_frames: s.paint_over_trigger_frames,
439            video_crf: s.video_crf,
440            video_paintover_crf: s.video_paintover_crf,
441            video_paintover_burst_frames: s.video_paintover_burst_frames,
442            video_streaming_mode: s.video_streaming_mode,
443            keyframe_interval_s: s.keyframe_interval_s,
444            capture_cursor: s.capture_cursor,
445            cursor_size_cap: s.cursor_size_cap,
446        }
447    }
448
449    /// Write these live tunables back into a full settings value in place.
450    pub fn apply_to(&self, s: &mut RustCaptureSettings) {
451        s.jpeg_quality = self.jpeg_quality;
452        s.paint_over_jpeg_quality = self.paint_over_jpeg_quality;
453        s.use_paint_over_quality = self.use_paint_over_quality;
454        s.paint_over_trigger_frames = self.paint_over_trigger_frames;
455        s.video_crf = self.video_crf;
456        s.video_paintover_crf = self.video_paintover_crf;
457        s.video_paintover_burst_frames = self.video_paintover_burst_frames;
458        s.video_streaming_mode = self.video_streaming_mode;
459        s.keyframe_interval_s = self.keyframe_interval_s;
460        s.cursor_size_cap = self.cursor_size_cap;
461        s.capture_cursor = self.capture_cursor;
462    }
463}
464
465impl Default for RustCaptureSettings {
466    fn default() -> Self {
467        Self {
468            width: 1024,
469            height: 768,
470            scale: 1.0,
471            capture_x: 0,
472            capture_y: 0,
473            target_fps: 60.0,
474            jpeg_quality: 75,
475            paint_over_jpeg_quality: 95,
476            use_paint_over_quality: true,
477            paint_over_trigger_frames: 15,
478            damage_block_threshold: 10,
479            damage_block_duration: 30,
480            output_mode: 0,
481            video_crf: 25,
482            video_paintover_crf: 18,
483            video_paintover_burst_frames: 5,
484            video_fullcolor: false,
485            video_fullframe: false,
486            video_streaming_mode: false,
487            capture_cursor: false,
488            cursor_size_cap: 32,
489            watermark_path: String::new(),
490            watermark_location_enum: 0,
491            encode_node_index: -2,
492            use_cpu: false,
493            debug_logging: false,
494            auto_adjust_screen_capture_size: false,
495            recording_socket: String::new(),
496            wayland_host_display: String::new(),
497            omit_stripe_headers: false,
498            video_cbr_mode: false,
499            video_bitrate_kbps: 4000,
500            video_vbv_multiplier: 0.0,
501            keyframe_interval_s: 0.0,
502            video_min_qp: 0,
503            video_max_qp: 0,
504        }
505    }
506}
507
508/// Marshal a Python settings object into the plain owned Rust value both capture backends run on.
509///
510/// Fields are read by attribute name (`getattr`), not by position, so a caller can pass any object
511/// exposing the `CaptureSettings` attributes — including a subclass carrying extras. Newer/optional
512/// fields fall back to a default when absent rather than erroring, so a caller built against an
513/// older schema still starts. Both the Wayland and X11 entry points route through this one reader
514/// to prevent drift.
515///
516/// # Arguments
517///
518/// * `settings` - A Python object exposing `CaptureSettings` attributes (`capture_width`,
519///   `capture_height`, `target_fps`, `jpeg_quality`, `video_crf`, etc.).
520///
521/// # Returns
522///
523/// An owned [`RustCaptureSettings`] on success, or a Python exception if a required field is
524/// missing or has the wrong type.
525pub(crate) fn extract_settings(settings: &Bound<'_, PyAny>) -> PyResult<RustCaptureSettings> {
526    let watermark_path_obj = settings.getattr("watermark_path")?;
527    let watermark_path = if let Ok(s) = watermark_path_obj.extract::<String>() {
528        s
529    } else if let Ok(b) = watermark_path_obj.extract::<Vec<u8>>() {
530        String::from_utf8_lossy(&b).into_owned()
531    } else {
532        String::new()
533    };
534
535    let scale = settings
536        .getattr("scale")
537        .ok()
538        .and_then(|x| x.extract().ok())
539        .unwrap_or(1.0);
540
541    // These fields are public attributes any caller can set to anything an i32 or f64
542    // holds. Clamping here keeps a dimension from sizing an absurd frame buffer, and a
543    // quality outside turbojpeg's 1..=100 from failing set_quality on every stripe, which
544    // would emit no JPEG at all.
545    let sanitize_dim = |v: i32| -> i32 {
546        if v <= 0 { 0 } else { v.min(MAX_CAPTURE_DIM) }
547    };
548    let sanitize_fps = |v: f64| -> f64 {
549        if v.is_finite() && v > 0.0 { v.min(MAX_FPS) } else { DEFAULT_FPS }
550    };
551    let sanitize_scale = |v: f64| -> f64 {
552        if v.is_finite() && v > 0.0 { v.min(MAX_SCALE) } else { 1.0 }
553    };
554
555    Ok(RustCaptureSettings {
556        width: sanitize_dim(settings.getattr("capture_width")?.extract()?),
557        height: sanitize_dim(settings.getattr("capture_height")?.extract()?),
558        scale: sanitize_scale(scale),
559        capture_x: settings.getattr("capture_x")?.extract()?,
560        capture_y: settings.getattr("capture_y")?.extract()?,
561        target_fps: sanitize_fps(settings.getattr("target_fps")?.extract()?),
562        jpeg_quality: settings.getattr("jpeg_quality")?.extract::<i32>()?.clamp(1, 100),
563        paint_over_jpeg_quality: settings.getattr("paint_over_jpeg_quality")?.extract::<i32>()?.clamp(1, 100),
564        use_paint_over_quality: settings.getattr("use_paint_over_quality")?.extract()?,
565        paint_over_trigger_frames: settings.getattr("paint_over_trigger_frames")?.extract()?,
566        damage_block_threshold: settings.getattr("damage_block_threshold")?.extract()?,
567        damage_block_duration: settings.getattr("damage_block_duration")?.extract()?,
568        output_mode: settings.getattr("output_mode")?.extract()?,
569        video_crf: settings.getattr("video_crf")?.extract()?,
570        video_paintover_crf: settings.getattr("video_paintover_crf")?.extract()?,
571        video_paintover_burst_frames: settings.getattr("video_paintover_burst_frames")?.extract()?,
572        video_fullcolor: settings.getattr("video_fullcolor")?.extract()?,
573        video_fullframe: settings.getattr("video_fullframe")?.extract()?,
574        video_streaming_mode: settings.getattr("video_streaming_mode")?.extract()?,
575        capture_cursor: settings.getattr("capture_cursor")?.extract()?,
576        cursor_size_cap: settings
577            .getattr("cursor_size_cap")
578            .ok()
579            .and_then(|v| v.extract::<i32>().ok())
580            .unwrap_or(32),
581        watermark_path,
582        watermark_location_enum: settings.getattr("watermark_location_enum")?.extract()?,
583        encode_node_index: settings.getattr("encode_node_index")?.extract()?,
584        use_cpu: settings.getattr("use_cpu")?.extract()?,
585        debug_logging: settings.getattr("debug_logging")?.extract()?,
586        auto_adjust_screen_capture_size: settings
587            .getattr("auto_adjust_screen_capture_size")
588            .ok()
589            .and_then(|v| v.extract::<bool>().ok())
590            .unwrap_or(false),
591        recording_socket: settings
592            .getattr("recording_socket")
593            .ok()
594            .and_then(|v| v.extract::<String>().ok())
595            .unwrap_or_default(),
596        wayland_host_display: settings
597            .getattr("wayland_host_display")
598            .ok()
599            .and_then(|v| v.extract::<String>().ok())
600            .unwrap_or_default(),
601        omit_stripe_headers: settings
602            .getattr("omit_stripe_headers")
603            .ok()
604            .and_then(|v| v.extract::<bool>().ok())
605            .unwrap_or(false),
606        video_cbr_mode: settings.getattr("video_cbr_mode")?.extract()?,
607        video_bitrate_kbps: settings.getattr("video_bitrate_kbps")?.extract()?,
608        video_vbv_multiplier: settings
609            .getattr("video_vbv_multiplier")
610            .ok()
611            .and_then(|v| v.extract::<f64>().ok())
612            .unwrap_or(0.0),
613        keyframe_interval_s: settings
614            .getattr("keyframe_interval_s")
615            .ok()
616            .and_then(|v| v.extract::<f64>().ok())
617            .unwrap_or(0.0),
618        video_min_qp: settings
619            .getattr("video_min_qp")
620            .ok()
621            .and_then(|v| v.extract::<i32>().ok())
622            .unwrap_or(0),
623        video_max_qp: settings
624            .getattr("video_max_qp")
625            .ok()
626            .and_then(|v| v.extract::<i32>().ok())
627            .unwrap_or(0),
628    })
629}
630
631/// One live output as `(id, x, y, width, height, scale, capturing)`. Sizes are physical
632/// pixels and `(x, y)` is the layout offset.
633pub type OutputDesc = (u32, i32, i32, i32, i32, f64, bool);
634
635/// One mapped window as `(window_id, title, app_id, output_id, parked)`. A parked window
636/// has no output yet — a nested session's spare screens.
637pub type WindowDesc = (u32, String, String, u32, bool);
638
639/// Control messages sent from the Python-facing methods to the capture thread.
640///
641/// Every interaction with a running capture crosses the thread boundary as one of these variants
642/// over the command channel: starting and stopping, injecting keyboard / pointer input, swapping
643/// the xkb keymap, serving the clipboard, changing live rate and per-frame tunables, and the
644/// computer-use queries that read back the screen, cursor, and geometry.
645pub enum ThreadCommand {
646    /// Start (or in-place reconfigure) the capture bound to output `display_id`.
647    /// `callback` is the Python per-frame delivery target; `None` starts an internal
648    /// capture with no Python consumer (the built-in recorder taps the delivery layer).
649    StartCapture { display_id: u32, callback: Option<Py<PyAny>>, settings: RustCaptureSettings },
650    /// Stop the capture bound to output `display_id` (other displays keep running).
651    StopCapture { display_id: u32 },
652    /// Create an additional output: `WxH` physical pixels at fractional `scale`, mapped
653    /// into the layout at offset `(x, y)`. Replies false when the id is taken/reserved or
654    /// the GPU render target cannot be allocated.
655    CreateOutput {
656        id: u32,
657        width: i32,
658        height: i32,
659        x: i32,
660        y: i32,
661        scale: f64,
662        reply: std::sync::mpsc::Sender<bool>,
663    },
664    /// Destroy a secondary output: its capture ends, its windows relocate to the primary
665    /// output. Replies false for the primary (id 0) or an unknown id.
666    DestroyOutput { id: u32, reply: std::sync::mpsc::Sender<bool> },
667    /// Remap an existing output (the primary included) to layout offset `(x, y)`: the
668    /// Space mapping, the offsets used for absolute input injection and cursor
669    /// compositing, and the windows placed on it all follow, and the output is damaged so
670    /// the next frames render correctly. Replies false for an unknown id.
671    RepositionOutput { id: u32, x: i32, y: i32, reply: std::sync::mpsc::Sender<bool> },
672    /// Reply with every live output as `(id, x, y, width, height, scale, capturing)`.
673    ListOutputs { reply: std::sync::mpsc::Sender<Vec<OutputDesc>> },
674    /// Reply with how many displays this backend can back with real content: -1 when
675    /// self-compositing (outputs are created on demand), the host compositor's output
676    /// count in host-capture mode.
677    OutputCapacity { reply: std::sync::mpsc::Sender<i64> },
678    /// Move the window with the given id onto output `output_id` (fullscreened there).
679    MoveWindowToOutput { window_id: u32, output_id: u32, reply: std::sync::mpsc::Sender<bool> },
680    /// Reply with every mapped window as `(window_id, title, app_id, output_id)`.
681    ListWindows { reply: std::sync::mpsc::Sender<Vec<WindowDesc>> },
682    SetCursorCallback(Py<PyAny>),
683    SetClipboardCallback(Py<PyAny>),
684    /// Server-side clipboard offer: the compositor owns the selection and serves `data` as
685    /// `mime` (plus text aliases) to pasting clients.
686    SetClipboard { mime: String, data: Vec<u8> },
687    KeyboardKey { scancode: u32, state: u32 },
688    /// A whole ordered run of key events in one message. Typing a paste one event at a
689    /// time costs a channel send and a calloop wake per event, which competes with the
690    /// render loop on the same thread; the caller still decides the sequence.
691    KeyboardKeys { events: Vec<(u32, u32)> },
692    /// Set the seat's BASE keymap from a full XKB_KEYMAP_FORMAT_TEXT_V1 string. The
693    /// compositor's keymap policy rebuilds on top: overlay binds are re-spliced onto the new
694    /// base (same keycodes) and the combined keymap is applied in one swap.
695    SetKeymapString(String),
696    /// Set the seat's BASE layout from RMLVO names (empty strings = xkbcommon defaults);
697    /// replies whether compilation succeeded. Overlay binds rebuild on top as for
698    /// `SetKeymapString`.
699    SetXkbLayout {
700        rules: String,
701        model: String,
702        layout: String,
703        variant: String,
704        options: String,
705        reply: std::sync::mpsc::Sender<bool>,
706    },
707    /// Resolve keysyms to `(keycode, level)` against the seat keymap, overlay-binding every
708    /// keysym the base cannot produce — ONE keymap swap for the whole batch, and a keycode that
709    /// is currently pressed is never recycled. `(0, 0)` marks an unbindable keysym. Serves
710    /// computer-use only; selkies resolves its own keysyms and injects plain keycodes.
711    BindKeysyms {
712        keysyms: Vec<u32>,
713        reply: std::sync::mpsc::Sender<Vec<(u32, u32)>>,
714    },
715    /// Bind explicit `(keycode, keysym)` pairs onto the CURRENT base keymap and deliver it,
716    /// in one swap. The caller decides the assignment; this only assembles and applies, so the
717    /// base is neither re-sent nor recompiled and its reverse map is not rebuilt. Ordered with
718    /// key events on the one command channel, so the keys that need the new binds cannot
719    /// overtake it — no reply is awaited, and the caller's loop is never blocked on the swap.
720    SetKeymapOverlay { binds: Vec<(u32, u32)> },
721    /// Debug/verification readback: currently pressed xkb keycodes plus the modifier state
722    /// bitmask (1 ctrl, 2 shift, 4 alt, 8 logo, 16 caps, 32 num, 64 altgr, 128 level5).
723    GetKeyboardState {
724        reply: std::sync::mpsc::Sender<(Vec<u32>, u32)>,
725    },
726    /// Reply with the smithay keyboard's keymap as an XKB_KEYMAP_FORMAT_TEXT_V1 string so a
727    /// consumer (selkies) can build its reverse keysym map from the IDENTICAL keymap.
728    GetXkbKeymap { reply: std::sync::mpsc::Sender<String> },
729    /// Ack once every previously queued command has been fully processed (the channel is
730    /// FIFO). The atexit sweep sends StopCapture + Barrier and waits, so the interpreter never
731    /// exits while the calloop thread is still mid-teardown (an NVENC/CUDA session drop racing
732    /// process exit segfaults).
733    Barrier { reply: std::sync::mpsc::Sender<()> },
734    PointerMotion { x: f64, y: f64 },
735    PointerRelativeMotion { dx: f64, dy: f64 },
736    /// `btn` is an evdev `BTN_` code by contract (e.g. 272 = BTN_LEFT, 273 = BTN_RIGHT,
737    /// 274 = BTN_MIDDLE, 0x113 = BTN_SIDE / 0x114 = BTN_EXTRA for back/forward) and is passed
738    /// straight through to smithay's pointer.
739    PointerButton { btn: u32, state: u32 },
740    PointerAxis { x: f64, y: f64 },
741    UpdateCursorConfig { render_on_framebuffer: bool },
742    /// Recreate the cursor theme handles at a new pixel size — the calloop's compositing
743    /// helper (the burned-in cursor) and, through its job channel, the `wl-cursor` worker's
744    /// (named-cursor PNG delivery). Replies false for a non-positive size.
745    SetCursorSize { size: i32, reply: std::sync::mpsc::Sender<bool> },
746    /// On-demand keyframe request (client reconnect / decoder reset) for one display's
747    /// capture: forces a send and an IDR even on a static screen.
748    RequestIdr { display_id: u32 },
749    /// Live rate-control change for one display's capture (parity with the X11 `rate_dirty`
750    /// path). Each field is `None` when that dimension is unchanged.
751    UpdateRate {
752        display_id: u32,
753        bitrate_kbps: Option<i32>,
754        vbv_multiplier: Option<f64>,
755        fps: Option<f64>,
756    },
757    /// Live per-frame tunables (quality / paint-over / streaming / cursor) for one
758    /// display's capture, mirrored to its readback encode thread — no restart.
759    UpdateTunables { display_id: u32, tunables: LiveTunables },
760    /// One-shot PNG of one output's next rendered frame (0 = primary); an unknown
761    /// display id replies with an error immediately.
762    CuScreenshot { display_id: u32, resp: std::sync::mpsc::Sender<Result<Vec<u8>, String>> },
763    CuCursorPosition { resp: std::sync::mpsc::Sender<(f64, f64)> },
764    CuGetInfo { display_id: u32, resp: std::sync::mpsc::Sender<(i32, i32, f64)> },
765}
766
767/// Read the kernel driver bound to a render node for encoder routing.
768///
769/// An `nvidia` driver name routes to NVENC; anything else routes to VA-API. The name is
770/// lowercased for case-insensitive substring matching, and is empty when the node has no driver
771/// link (treated as "no detectable GPU" by the selection logic).
772///
773/// # Arguments
774///
775/// * `card_index` - DRM card index (maps to `/sys/class/drm/renderD{128 + card_index}`).
776///
777/// # Returns
778///
779/// Lowercased driver name, or an empty string if the node has no driver link.
780pub(crate) fn get_gpu_driver(card_index: i32) -> String {
781    let path = format!("/sys/class/drm/renderD{}/device/driver", 128 + card_index);
782    match std::fs::read_link(&path) {
783        Ok(link_path) => link_path.to_string_lossy().to_lowercase(),
784        Err(_) => String::new(),
785    }
786}
787
788/// Whether an encode node's driver name routes to NVENC: NVIDIA, or unknown (no readable
789/// render-node sysfs entry, the usual shape of an NVIDIA container without /dev/dri), so the
790/// NVENC attempt runs before the CPU fallback. Any other named driver routes to VA-API. Shared
791/// by the X11 and Wayland paths so both pick the same encoder for the same node.
792pub(crate) fn driver_selects_nvenc(encode_driver: &str) -> bool {
793    encode_driver.is_empty() || encode_driver.contains("nvidia")
794}
795
796/// A DRM card's identity as the kernel reports it, read from the device's
797/// sysfs `uevent` (DRIVER=, PCI_ID=, OF_COMPATIBLE_n=) with per-file fallbacks
798/// (`vendor`, `modalias`, the `driver` symlink). uevent is uniform across buses
799/// (PCI, platform/devicetree, USB) and readable in unprivileged containers.
800struct CardIdentity {
801    driver: String,
802    pci_vendor: Option<u32>,
803    compatibles: Vec<String>,
804}
805
806/// Read a DRM card's `CardIdentity` from sysfs.
807///
808/// The device's `uevent` file is the primary source (`DRIVER=`, `PCI_ID=`, `OF_COMPATIBLE_n=`)
809/// because it is uniform across buses and readable in unprivileged containers. Each field has a
810/// fallback for cards whose `uevent` omits it: the `vendor` file for the PCI vendor, `modalias`
811/// (`of:...C<compatible>`) for the devicetree compatibles, and the `driver` symlink for the name.
812fn read_card_identity(device: &std::path::Path) -> CardIdentity {
813    let mut id = CardIdentity { driver: String::new(), pci_vendor: None, compatibles: Vec::new() };
814    if let Ok(uevent) = std::fs::read_to_string(device.join("uevent")) {
815        for line in uevent.lines() {
816            if let Some(v) = line.strip_prefix("DRIVER=") {
817                id.driver = v.trim().to_lowercase();
818            } else if let Some(v) = line.strip_prefix("PCI_ID=") {
819                id.pci_vendor = v.split(':').next().and_then(|h| u32::from_str_radix(h, 16).ok());
820            } else if line.starts_with("OF_COMPATIBLE_") && !line.starts_with("OF_COMPATIBLE_N")
821                && let Some(v) = line.split_once('=').map(|x| x.1) {
822                    id.compatibles.push(v.trim().to_lowercase());
823                }
824        }
825    }
826    if id.pci_vendor.is_none() {
827        id.pci_vendor = std::fs::read_to_string(device.join("vendor"))
828            .ok()
829            .and_then(|v| u32::from_str_radix(v.trim().trim_start_matches("0x"), 16).ok());
830    }
831    if id.compatibles.is_empty()
832        && let Ok(modalias) = std::fs::read_to_string(device.join("modalias")) {
833            let modalias = modalias.trim();
834            if let Some(rest) = modalias.strip_prefix("of:") {
835                id.compatibles
836                    .extend(rest.split('C').skip(1).map(|c| c.to_lowercase()));
837            }
838        }
839    if id.driver.is_empty() {
840        id.driver = std::fs::read_link(device.join("driver"))
841            .map(|p| p.file_name().map(|n| n.to_string_lossy().to_lowercase()).unwrap_or_default())
842            .unwrap_or_default();
843    }
844    id
845}
846
847/// Human vendor name -> PCI vendor IDs. The kernel has no such table (the
848/// grouping is conventional), and pci.ids/hwdata is often absent in containers,
849/// so this is the one mapping that must be embedded — kept to the names only.
850const VENDOR_PCI_IDS: &[(&str, &[u32])] = &[
851    ("nvidia", &[0x10de, 0x12d2]),
852    ("amd", &[0x1002, 0x1022]),
853    ("ati", &[0x1002]),
854    ("intel", &[0x8086, 0x8087]),
855    ("arm", &[0x13b5]),
856    ("qualcomm", &[0x5143, 0x17cb]),
857    ("broadcom", &[0x14e4]),
858    ("apple", &[0x106b]),
859    ("mediatek", &[0x14c3]),
860    ("samsung", &[0x144d]),
861    ("vmware", &[0x15ad]),
862    ("microsoft", &[0x1414]),
863    ("virtio", &[0x1af4]),
864];
865
866/// Human name -> devicetree vendor prefix, only where they differ (a token
867/// equal to the prefix itself, e.g. "qcom" or "rockchip", matches directly).
868const OF_PREFIX_ALIASES: &[(&str, &str)] = &[
869    ("mali", "arm"),
870    ("qualcomm", "qcom"),
871    ("adreno", "qcom"),
872    ("broadcom", "brcm"),
873    ("videocore", "brcm"),
874    ("imagination", "img"),
875    ("powervr", "img"),
876];
877
878/// Does a card match the requested token? Accepted token forms, checked against
879/// the identity the kernel itself reports: a kernel DRIVER name (exact, no
880/// table), a raw PCI vendor ID ("0x10de"/"10de"), a devicetree vendor prefix
881/// (literal first segment of any compatible), or a human vendor name resolved
882/// through the small embedded alias maps above.
883fn card_matches_token(token: &str, id: &CardIdentity) -> bool {
884    if !id.driver.is_empty() && token == id.driver {
885        return true;
886    }
887    if let Some(vid) = id.pci_vendor {
888        if u32::from_str_radix(token.trim_start_matches("0x"), 16) == Ok(vid) {
889            return true;
890        }
891        if let Some((_, ids)) = VENDOR_PCI_IDS.iter().find(|(n, _)| *n == token)
892            && ids.contains(&vid) {
893                return true;
894            }
895    }
896    if !id.compatibles.is_empty() {
897        let prefix = OF_PREFIX_ALIASES
898            .iter()
899            .find(|(n, _)| *n == token)
900            .map(|(_, p)| *p)
901            .unwrap_or(token);
902        let want = format!("{prefix},");
903        if id.compatibles.iter().any(|c| c.starts_with(&want)) {
904            return true;
905        }
906    }
907    false
908}
909
910/// Parse an auto-GPU request (the CaptureSettings `auto_gpu` field, which selkies
911/// fills from --auto-gpu / SELKIES_AUTO_GPU). `None` = disabled; `Some(None)` =
912/// pick the first GPU overall ("true"); `Some(Some(token))` = pick the first GPU
913/// whose kernel identity matches the case-insensitive token (vendor name, kernel
914/// driver name, devicetree vendor prefix, or raw PCI vendor id).
915fn parse_auto_gpu(value: &str) -> Option<Option<String>> {
916    let value = value.to_lowercase();
917    match value.as_str() {
918        "" | "false" | "0" | "off" | "no" => None,
919        "true" | "1" | "on" | "yes" => Some(None),
920        token => Some(Some(token.to_string())),
921    }
922}
923
924/// Resolve a usable `/dev/dri/renderD*` node, optionally matching a vendor/driver token.
925///
926/// Cards under `/sys/class/drm` are walked in numeric order, skipping cards with no render node
927/// (e.g. IPMI/VGA). When `/sys/class/drm` is unreadable (container without `/sys`), falls
928/// through to scanning `/dev/dri` directly — that fallback has no device identity so a `token`
929/// request cannot be satisfied there.
930///
931/// # Arguments
932///
933/// * `token` - Optional vendor/driver filter: a kernel driver name, PCI vendor ID (`"0x10de"`),
934///   devicetree vendor prefix, or human vendor name (`"nvidia"`, `"intel"`). `None` picks the
935///   first available node.
936///
937/// # Returns
938///
939/// A `/dev/dri/renderD*` path, or `None` if no matching node exists.
940fn auto_select_render_node(token: Option<&str>) -> Option<String> {
941    let mut cards: Vec<(u32, std::path::PathBuf)> = std::fs::read_dir("/sys/class/drm")
942        .into_iter()
943        .flatten()
944        .flatten()
945        .filter_map(|e| {
946            let num = e.file_name().into_string().ok()?.strip_prefix("card")?.parse::<u32>().ok()?;
947            Some((num, e.path()))
948        })
949        .collect();
950    cards.sort_by_key(|(n, _)| *n);
951    for (_, path) in &cards {
952        if let Some(t) = token
953            && !card_matches_token(t, &read_card_identity(&path.join("device"))) {
954                continue;
955            }
956        if let Ok(drm_entries) = std::fs::read_dir(path.join("device/drm")) {
957            for de in drm_entries.flatten() {
958                let name = de.file_name().into_string().unwrap_or_default();
959                if name.starts_with("renderD") {
960                    let dev = format!("/dev/dri/{}", name);
961                    if std::path::Path::new(&dev).exists() {
962                        return Some(dev);
963                    }
964                }
965            }
966        }
967    }
968    if token.is_some() {
969        return None;
970    }
971    let mut nodes: Vec<String> = std::fs::read_dir("/dev/dri")
972        .ok()?
973        .flatten()
974        .filter_map(|e| e.file_name().into_string().ok())
975        .filter(|n| n.starts_with("renderD"))
976        .collect();
977    nodes.sort();
978    nodes.first().map(|n| format!("/dev/dri/{}", n))
979}
980
981
982/// One captured host-pixel frame in flight from the calloop (render/readback) to the Wayland
983/// encode thread: the pixels plus the per-frame inputs of the encode dispatch (damage,
984/// overlay animation); the IDR request travels separately via the controls atomic.
985pub struct WlFrame {
986    /// Pool slot id; travels with the buffer so recycle returns it to the right slot.
987    id: usize,
988    buf: Vec<u8>,
989    frame_id: u16,
990    damage: Vec<Rectangle<i32, Physical>>,
991    is_animated: bool,
992}
993
994/// Interior state of `WlFramePool`: the free-buffer list plus the single publish slot.
995struct WlPoolInner {
996    free: Vec<(usize, Vec<u8>)>,
997    slot: Option<WlFrame>,
998}
999
1000/// Render->encode handoff for the Wayland readback paths, mirroring the X11 FramePool's
1001/// single-slot non-dropping design with one deliberate difference: the calloop thread is also
1002/// the compositor + input dispatcher, so it must NEVER block on the pool. `try_begin` hands
1003/// out a buffer only while the publish slot is empty, so `publish` cannot block, and a
1004/// saturated encoder throttles capture by SKIPPING ticks (compositor damage accumulates via
1005/// buffer age, so nothing is lost). Every published frame is encoded, in order: the H.264
1006/// reference chain stays contiguous exactly as on X11.
1007pub struct WlFramePool {
1008    inner: Mutex<WlPoolInner>,
1009    cv: Condvar,
1010    stop: AtomicBool,
1011}
1012
1013impl WlFramePool {
1014    /// Pre-allocate all `n` capture buffers up front (each `buf_len` bytes, all initially
1015    /// free) so the steady-state render/encode loop hands buffers around without ever allocating on
1016    /// the hot path.
1017    fn new(n: usize, buf_len: usize) -> Self {
1018        Self {
1019            inner: Mutex::new(WlPoolInner {
1020                free: (0..n).map(|i| (i, vec![0u8; buf_len])).collect(),
1021                slot: None,
1022            }),
1023            cv: Condvar::new(),
1024            stop: AtomicBool::new(false),
1025        }
1026    }
1027
1028    /// Calloop: reserve a buffer for the next render/readback, NON-blocking. None means the
1029    /// encoder is still behind (slot full or every buffer in flight) -- skip this tick.
1030    /// Because the calloop is the only producer, a successful reservation guarantees the
1031    /// following `publish` finds the slot empty (the consumer only ever drains it).
1032    fn try_begin(&self) -> Option<(usize, Vec<u8>)> {
1033        let mut g = self.inner.lock().unwrap();
1034        if g.slot.is_some() {
1035            return None;
1036        }
1037        g.free.pop()
1038    }
1039
1040    /// Calloop: hand the filled buffer to the encode thread. Never blocks (see `try_begin`).
1041    fn publish(&self, frame: WlFrame) {
1042        let mut g = self.inner.lock().unwrap();
1043        debug_assert!(g.slot.is_none());
1044        g.slot = Some(frame);
1045        drop(g);
1046        self.cv.notify_all();
1047    }
1048
1049    /// Calloop: return an unused reservation (render failed, readback skipped).
1050    fn cancel(&self, id: usize, buf: Vec<u8>) {
1051        self.inner.lock().unwrap().free.push((id, buf));
1052    }
1053
1054    /// Encode: block until a frame is published (Some) or the pool shuts down (None). The
1055    /// wait is bounded, re-checking `stop` as defense-in-depth against a lost wakeup.
1056    fn take(&self) -> Option<WlFrame> {
1057        let mut g = self.inner.lock().unwrap();
1058        loop {
1059            if let Some(f) = g.slot.take() {
1060                return Some(f);
1061            }
1062            if self.stop.load(Ordering::Acquire) {
1063                return None;
1064            }
1065            let (gg, _) = self.cv.wait_timeout(g, WL_POOL_WAKE_QUANTUM).unwrap();
1066            g = gg;
1067        }
1068    }
1069
1070    /// Encode: return an encoded frame's buffer so the calloop can capture into it again.
1071    /// No notify: the only waiter (take) waits on the slot, and try_begin never blocks.
1072    fn recycle(&self, id: usize, buf: Vec<u8>) {
1073        self.inner.lock().unwrap().free.push((id, buf));
1074    }
1075
1076    /// Store stop under the lock so take() can't check stop==false and then park after the
1077    /// notify already fired (lost wakeup); notify after unlocking.
1078    fn shutdown(&self) {
1079        let g = self.inner.lock().unwrap();
1080        self.stop.store(true, Ordering::Release);
1081        drop(g);
1082        self.cv.notify_all();
1083    }
1084}
1085
1086/// Cross-thread controls for the Wayland encode thread, the X11 `Controls` scheme: the
1087/// UpdateRate handler stores the current values then flips `rate_dirty` with Release; the
1088/// encode thread swaps it with Acquire and re-reads the payload, never seeing it half-applied.
1089/// `force_idr` is swapped just before each encode, so an on-demand keyframe lands on the
1090/// frame ALREADY in flight instead of waiting one pipeline stage for the next publish.
1091pub struct WlEncodeControls {
1092    rate_dirty: AtomicBool,
1093    bitrate_kbps: AtomicI32,
1094    vbv_mult_milli: AtomicI32,
1095    fps_milli: AtomicU64,
1096    force_idr: AtomicBool,
1097    /// Pending per-frame tunables for the encode thread (mutex, not atomics: one struct, set
1098    /// rarely, read only when the dirty flag says so).
1099    tunables_dirty: AtomicBool,
1100    tunables: Mutex<Option<LiveTunables>>,
1101}
1102
1103impl WlEncodeControls {
1104    fn new() -> Self {
1105        Self {
1106            rate_dirty: AtomicBool::new(false),
1107            bitrate_kbps: AtomicI32::new(0),
1108            vbv_mult_milli: AtomicI32::new(0),
1109            fps_milli: AtomicU64::new(0),
1110            force_idr: AtomicBool::new(false),
1111            tunables_dirty: AtomicBool::new(false),
1112            tunables: Mutex::new(None),
1113        }
1114    }
1115}
1116
1117/// Two buffers: one being encoded while the calloop fills the other. try_begin gates on the
1118/// publish slot, so a deeper pool would only add latency (staler frames), never overlap.
1119const WL_POOL_SURFACES: usize = 2;
1120/// The encode thread's bounded idle wait in `WlFramePool::take`.
1121const WL_POOL_WAKE_QUANTUM: Duration = Duration::from_millis(20);
1122/// How long a reconfigured output holds its frames while its clients answer the new size.
1123/// It is measured from the start, which rebuilds the encode path before there is any frame
1124/// to hold: the first one arrives roughly 300 ms later, so a window shorter than that
1125/// expires before it can suppress anything and the hold does nothing at all. This one
1126/// outlasts the rebuild and a client's own repaint, and is the whole delay an output that
1127/// no client ever draws on pays before its content reaches the viewer regardless.
1128const WL_CONTENT_HOLD: Duration = Duration::from_millis(500);
1129
1130/// Upper bounds for Python-supplied capture geometry: keeps a hostile or buggy setting from
1131/// turning into a multi-GB `vec![]` allocation that would abort the process. 16384 covers every
1132/// real display wall; the fps/scale bounds keep timing math finite and sane.
1133const MAX_CAPTURE_DIM: i32 = 16384;
1134const MAX_FPS: f64 = 1000.0;
1135/// How often frame callbacks go out while nothing is being captured. Fast enough that a
1136/// client blocked on one keeps making progress, slow enough that an unwatched session is
1137/// not drawing frames nobody asked for.
1138const IDLE_FRAME_INTERVAL: Duration = Duration::from_millis(250);
1139const DEFAULT_FPS: f64 = 60.0;
1140const MAX_SCALE: f64 = 8.0;
1141/// Wheel v120 units per unit of injected scroll value: selkies sends 10 per notch, and one
1142/// notch is 120, so both the seat's v120 and the host virtual pointer's discrete steps derive
1143/// from the same value.
1144pub(crate) const SCROLL_V120_PER_UNIT: f64 = 12.0;
1145
1146/// Shared capture stats: whichever thread owns the encoders counts frames/stripes and
1147/// composes `desc` + `n_stripes` (the encoder half of the 1 s debug log line); the calloop
1148/// log loads, prints and resets the counters.
1149pub struct WlEncodeStats {
1150    frames: AtomicU32,
1151    stripes: AtomicU32,
1152    n_stripes: AtomicU32,
1153    desc: Mutex<String>,
1154}
1155
1156impl WlEncodeStats {
1157    fn new() -> Self {
1158        Self {
1159            frames: AtomicU32::new(0),
1160            stripes: AtomicU32::new(0),
1161            n_stripes: AtomicU32::new(1),
1162            desc: Mutex::new(String::new()),
1163        }
1164    }
1165}
1166
1167/// Everything the Wayland encode thread needs, fixed for the life of one capture (a
1168/// StartCapture reconfigure tears the thread down and spawns a fresh one). Rate changes
1169/// flow through `controls`; damage and the IDR request arrive per-frame in `WlFrame`.
1170struct WlEncodeConfig {
1171    settings: RustCaptureSettings,
1172    /// Output/display id this encode loop serves; keys the recorder's delivery-layer tap.
1173    display_id: u32,
1174    /// GLES readback is RGBA; the pixman framebuffer is BGRA. Selects CSC + encoder input kind.
1175    use_gpu: bool,
1176    /// Attempt a HW (NVENC/VAAPI) readback session before falling back to the CPU encoders.
1177    try_gpu: bool,
1178    /// HW session handed back by the previous encode thread; reconfigured in place and
1179    /// reused when still compatible, sparing the stream a session rebuild.
1180    prior: Option<GpuEncoder>,
1181    /// The outgoing encode thread of a capture being restarted, joined here — off the
1182    /// calloop — before this thread builds anything. Everything the predecessor did
1183    /// therefore precedes everything this thread does, and its readback hardware session is
1184    /// inherited rather than rebuilt.
1185    predecessor: Option<std::thread::JoinHandle<Option<GpuEncoder>>>,
1186    /// Weak, so the calloop's teardown owns the sink's lifetime: a strong handle held here
1187    /// could outlive the capture and unlink a successor's freshly bound socket path.
1188    recording_sink: Option<std::sync::Weak<crate::recording_sink::RecordingSink>>,
1189    deliver_tx: std::sync::mpsc::SyncSender<Vec<EncodedStripe>>,
1190    controls: Arc<WlEncodeControls>,
1191    stats: Arc<WlEncodeStats>,
1192}
1193
1194/// Build the readback-mode hardware encoder on the thread that will own and drive it.
1195///
1196/// The result is a hardware `GpuEncoder` (NVENC or VAAPI) consuming host NV12/YUV444 via
1197/// `encode_raw`, or `None` for the software path — striped or full-frame JPEG/H.264 through the
1198/// build's software encoder (`SOFTWARE_H264_ENCODER`), where `encode_cpu` builds its own per-stripe
1199/// state. Selection follows the settings and the effective encode device — an auto index below
1200/// zero resolves to device 0: NVENC on an NVIDIA driver, VAAPI on any other GPU, except a 4:4:4
1201/// full-color request which VAAPI cannot do reliably and so falls through to the CPU. When a
1202/// compatible NVENC session is handed over from the previous encode thread it is reconfigured in
1203/// place (milliseconds) rather than rebuilt, which would stall the stream. The EGL display is
1204/// always null here: readback mode never imports dmabufs.
1205fn build_readback_encoders(
1206    settings: &RustCaptureSettings,
1207    try_gpu: bool,
1208    prior: Option<GpuEncoder>,
1209) -> Option<GpuEncoder> {
1210    if !try_gpu {
1211        return None;
1212    }
1213    let encode_driver = get_gpu_driver(settings.encode_node_index.max(0));
1214    println!(
1215        "[Wayland] Encode Node Index: {} | Driver: {}",
1216        settings.encode_node_index.max(0), encode_driver
1217    );
1218    if driver_selects_nvenc(&encode_driver) {
1219        if let Some(GpuEncoder::Nvenc(mut enc)) = prior {
1220            match enc.reconfigure_resolution(settings) {
1221                Ok(()) => {
1222                    println!("[Wayland] NVENC session reconfigured in place.");
1223                    return Some(GpuEncoder::Nvenc(enc));
1224                }
1225                Err(e) => eprintln!(
1226                    "[Wayland] NVENC in-place reconfigure unavailable ({e}); rebuilding."
1227                ),
1228            }
1229        }
1230        println!("[Wayland] Nvidia Encoder detected. Initializing NVENC...");
1231        match NvencEncoder::new(settings, std::ptr::null()) {
1232            Ok(e) => {
1233                println!("[Wayland] NVENC Encoder initialized successfully.");
1234                return Some(GpuEncoder::Nvenc(e));
1235            }
1236            Err(e) => eprintln!(
1237                "[Wayland] Failed to init NVENC: {}. Falling back to CPU ({}).",
1238                e,
1239                encoders::SOFTWARE_H264_ENCODER
1240            ),
1241        }
1242    } else {
1243        println!("[Wayland] Initializing Unified VAAPI Encoder...");
1244        match VaapiEncoder::new(settings) {
1245            Ok(e) => {
1246                println!(
1247                    "[Wayland] VAAPI Encoder initialized successfully ({}).",
1248                    if e.is_fullcolor() { "4:4:4" } else { "4:2:0" }
1249                );
1250                return Some(GpuEncoder::Vaapi(e));
1251            }
1252            Err(e) => eprintln!(
1253                "[Wayland] Failed to init VAAPI: {}. Falling back to CPU ({}).",
1254                e,
1255                encoders::SOFTWARE_H264_ENCODER
1256            ),
1257        }
1258    }
1259    None
1260}
1261
1262/// Planar CSC target for a readback VA-API session: YUV444 needs three full planes, NV12 one
1263/// plus a half-height interleaved chroma plane. Empty when no such session consumes it — NVENC
1264/// takes the packed readback rows and converts in hardware.
1265fn hw_plane_buffer(width: i32, height: i32, fullcolor: bool, planar: bool) -> Vec<u8> {
1266    if !planar {
1267        return Vec::new();
1268    }
1269    let n = (width * height) as usize;
1270    vec![0u8; if fullcolor { n * 3 } else { n * 3 / 2 }]
1271}
1272
1273/// Encode-thread body for the Wayland readback paths: drain published frames and run the
1274/// full encode dispatch, owning the encoders for the life of the capture.
1275///
1276/// Owning the encoders on this one thread (created, driven, and dropped here) is what lets the
1277/// calloop overlap the next render/readback with this encode — the same capture‖encode split used
1278/// on X11, minus the renderer, which is genuinely calloop-affine (EGL/GBM/dmabuf and the pixman
1279/// targets). Each published frame is processed in order so the H.264 reference chain stays
1280/// contiguous:
1281///
1282/// 1. **Apply cross-thread changes**: live tunables and rate/VBV/fps updates are read here, on the
1283///    thread that owns the encoders (each `Acquire` swap pairs with the command handler's `Release`
1284///    store, so a payload is never seen half-applied). The IDR request is swapped as late as
1285///    possible, so a request that arrived while this frame was in flight is honored one pipeline
1286///    stage earlier than the next publish.
1287/// 2. **Dispatch by encoder**: a hardware session runs `decide_hw_fullframe`, then hands only the
1288///    frames actually being encoded to the session — NVENC takes the packed rows as they are (RGBA
1289///    vs BGRA source chosen by the renderer) through `encode_cpu_packed` and converts in hardware,
1290///    VA-API gets them colorspace-converted into the reused NV12/YUV444 buffer and `encode_raw`;
1291///    otherwise `encode_cpu` runs the
1292///    software path with compositor damage — JPEG, or H.264 through the build's software encoder,
1293///    striped or full-frame. The software H.264 path keeps an infinite GOP, forcing an IDR only on
1294///    an explicit request or the configured interval, and an explicit request also forces a full
1295///    JPEG resend for joiners.
1296/// 3. **Recycle then deliver**: the capture buffer is recycled BEFORE delivery so a slow consumer
1297///    never pins one, then the stripes go to the delivery thread through a single-slot `send` whose
1298///    blocking is the backpressure that overlaps delivery with the next render + encode.
1299///
1300/// On exit the hardware session is handed back to the calloop so a restart can reuse it in place
1301/// when the new settings stay compatible (a plain `StopCapture` just drops it).
1302fn wayland_encode_loop(pool: &WlFramePool, cfg: WlEncodeConfig) -> Option<GpuEncoder> {
1303    crate::boost_thread_priority(-10);
1304    let mut settings = cfg.settings;
1305    let inherited = cfg.predecessor.and_then(|h| h.join().ok().flatten());
1306    let mut video_encoder =
1307        build_readback_encoders(&settings, cfg.try_gpu, cfg.prior.or(inherited));
1308    if cfg.try_gpu && video_encoder.is_none() {
1309        println!(
1310            "[Wayland] Decision: No GPU Encoder available -> Using CPU Software Encoding ({}).",
1311            encoders::SOFTWARE_H264_ENCODER
1312        );
1313    }
1314    let n_stripes = wayland_stripe_count(&settings, video_encoder.is_some());
1315    cfg.stats.n_stripes.store(n_stripes as u32, Ordering::Relaxed);
1316    *cfg.stats.desc.lock().unwrap() = encoder_desc(&settings, video_encoder.as_ref(), false);
1317    log_stream_settings(&settings, n_stripes, video_encoder.as_ref());
1318
1319    let width = settings.width;
1320    let height = settings.height;
1321    let mut stripes: Vec<StripeState> = Vec::with_capacity(MAX_STRIPE_CAPACITY);
1322    // Smoothed number of stripes carrying the encode budget (see stripe_rate_control).
1323    let mut stripes_carrying: f32 = 1.0;
1324    let mut hw_state = StripeState::default();
1325    // The CSC target follows the session's own chroma, not the request: a VA-API device that
1326    // refused 4:4:4 encodes 4:2:0, and sizing this from the request would hand it a buffer laid
1327    // out for planes it does not read.
1328    let mut hw_fullcolor = encoder_fullcolor(video_encoder.as_ref(), &settings);
1329    let mut nv12_buffer: Vec<u8> = hw_plane_buffer(
1330        width,
1331        height,
1332        hw_fullcolor,
1333        matches!(video_encoder, Some(GpuEncoder::Vaapi(_))),
1334    );
1335    // Mid-stream recovery state for the readback hardware session, mirroring the zero-copy tick.
1336    let mut hw_error_streak: u32 = 0;
1337    let mut hw_rebuilt = false;
1338
1339    while let Some(mut f) = pool.take() {
1340        if cfg.controls.tunables_dirty.swap(false, Ordering::Acquire)
1341            && let Some(t) = cfg.controls.tunables.lock().unwrap().take() {
1342                t.apply_to(&mut settings);
1343            }
1344        if cfg.controls.rate_dirty.swap(false, Ordering::Acquire) {
1345            settings.video_bitrate_kbps = cfg.controls.bitrate_kbps.load(Ordering::Relaxed);
1346            settings.video_vbv_multiplier =
1347                cfg.controls.vbv_mult_milli.load(Ordering::Relaxed) as f64 / 1000.0;
1348            let fps = (cfg.controls.fps_milli.load(Ordering::Relaxed) as f64) / 1000.0;
1349            if fps > 0.0 {
1350                settings.target_fps = fps;
1351            }
1352            match video_encoder.as_mut() {
1353                Some(GpuEncoder::Nvenc(enc)) => enc.reconfigure_rate(&settings),
1354                Some(GpuEncoder::Vaapi(enc)) => {
1355                    if let Err(e) = enc.reconfigure_rate(&settings) {
1356                        // The failed re-open left no codec context: the next encode fails,
1357                        // and a full streak makes that failure run the recovery ladder at
1358                        // once instead of after a window of dead frames.
1359                        eprintln!("[wl-encode] VAAPI rate reconfigure failed: {e}");
1360                        hw_error_streak = HW_ERROR_RECOVERY_THRESHOLD - 1;
1361                    }
1362                }
1363                None => {}
1364            }
1365        }
1366
1367        // A recorder connecting counts as a request, so the decision layer sends a
1368        // decodable frame even when the screen is static. A sink whose capture has been
1369        // torn down is already gone, and its last frames are not recorded.
1370        let recording_sink = cfg.recording_sink.as_ref().and_then(|w| w.upgrade());
1371        let requested_idr = cfg.controls.force_idr.swap(false, Ordering::Relaxed)
1372            || recording_sink.as_ref().is_some_and(|s| s.should_force_idr());
1373
1374        let mut out: Vec<EncodedStripe> = Vec::new();
1375        if let Some(ref mut encoder) = video_encoder {
1376            let decision = crate::pipeline::decide_hw_fullframe(
1377                &mut hw_state,
1378                &settings,
1379                f.frame_id,
1380                !f.damage.is_empty(),
1381                f.is_animated,
1382                requested_idr,
1383            );
1384            if decision.send {
1385                let w = width as u32;
1386                let h = height as u32;
1387                let force_idr = decision.force_idr;
1388                let outcome = match encoder {
1389                    // NVENC takes the readback rows as they are — BGRA from the pixman
1390                    // framebuffer, RGBA from a GLES readback — through a pinned upload and
1391                    // its hardware CSC, the same hand-over the X11 capture uses, so no
1392                    // colour conversion runs on this thread.
1393                    GpuEncoder::Nvenc(enc) => enc.encode_cpu_packed(
1394                        &f.buf,
1395                        (w * 4) as usize,
1396                        cfg.use_gpu,
1397                        f.frame_id as u64,
1398                        decision.target_qp,
1399                        force_idr,
1400                    ),
1401                    GpuEncoder::Vaapi(enc) => {
1402                        // VA-API's raw entry point takes planar YUV, converted here with the
1403                        // matrix and range its session declares: BT.709, limited (the range
1404                        // its own convert targets as well). A failed conversion leaves the
1405                        // planes holding the previous frame, so the encode is skipped rather
1406                        // than submitting stale content, and the failure feeds the same
1407                        // recovery ladder as an encode failure: both leave the session
1408                        // delivering nothing, and only the rebuild or the demote below can get
1409                        // the stream moving again.
1410                        let matrix = YuvStandardMatrix::Bt709;
1411                        let range = YuvRange::Limited;
1412                        let y_size = (w * h) as usize;
1413                        let csc = if hw_fullcolor {
1414                            let (y_plane, rest) = nv12_buffer.split_at_mut(y_size);
1415                            let (u_plane, v_plane) = rest.split_at_mut(y_size);
1416                            let mut planar_image = yuv::YuvPlanarImageMut {
1417                                y_plane: BufferStoreMut::Borrowed(y_plane),
1418                                y_stride: w,
1419                                u_plane: BufferStoreMut::Borrowed(u_plane),
1420                                u_stride: w,
1421                                v_plane: BufferStoreMut::Borrowed(v_plane),
1422                                v_stride: w,
1423                                width: w,
1424                                height: h,
1425                            };
1426                            if cfg.use_gpu {
1427                                yuv::rgba_to_yuv444(&mut planar_image, &f.buf, w * 4, range, matrix, YuvConversionMode::Fast)
1428                            } else {
1429                                yuv::bgra_to_yuv444(&mut planar_image, &f.buf, w * 4, range, matrix, YuvConversionMode::Fast)
1430                            }
1431                        } else {
1432                            let (y_plane, uv_plane) = nv12_buffer.split_at_mut(y_size);
1433                            let mut planar_image = YuvBiPlanarImageMut {
1434                                y_plane: BufferStoreMut::Borrowed(y_plane),
1435                                y_stride: w,
1436                                uv_plane: BufferStoreMut::Borrowed(uv_plane),
1437                                uv_stride: w,
1438                                width: w,
1439                                height: h,
1440                            };
1441                            if cfg.use_gpu {
1442                                yuv::rgba_to_yuv_nv12(&mut planar_image, &f.buf, w * 4, range, matrix, YuvConversionMode::Fast)
1443                            } else {
1444                                yuv::bgra_to_yuv_nv12(&mut planar_image, &f.buf, w * 4, range, matrix, YuvConversionMode::Fast)
1445                            }
1446                        };
1447                        match csc {
1448                            Err(e) => Err(format!(
1449                                "{} CSC failed: {e:?}",
1450                                if hw_fullcolor { "YUV444" } else { "NV12" }
1451                            )),
1452                            Ok(()) => enc.encode_raw(
1453                                &nv12_buffer,
1454                                f.frame_id as u64,
1455                                decision.target_qp,
1456                                force_idr,
1457                            ),
1458                        }
1459                    }
1460                };
1461                match outcome {
1462                    Ok(data) => {
1463                        hw_error_streak = 0;
1464                        hw_rebuilt = false;
1465                        if !data.is_empty() {
1466                            out.push(EncodedStripe {
1467                                data: Arc::new(data),
1468                                data_type: 2,
1469                                stripe_y_start: 0,
1470                                stripe_height: height,
1471                                frame_id: f.frame_id as i32,
1472                            });
1473                        }
1474                    }
1475                    Err(e) => {
1476                        // One line per recovery window: a session failing at frame rate would
1477                        // otherwise write a line per frame for the life of the capture.
1478                        if hw_error_streak % HW_ERROR_RECOVERY_THRESHOLD == 0 {
1479                            eprintln!("[wl-encode] HW encode error: {e}");
1480                        }
1481                        hw_error_streak = hw_error_streak.saturating_add(1);
1482                        if hw_error_streak >= HW_ERROR_RECOVERY_THRESHOLD {
1483                            // The readback session persistently fails after having worked:
1484                            // rebuild it once with the startup selection, then demote to the
1485                            // software encoders. A session whose encodes keep failing still
1486                            // constructs, so the rebuild only counts as recovery until the
1487                            // next streak; otherwise the stream would rebuild in a loop.
1488                            hw_error_streak = 0;
1489                            let try_gpu = !hw_rebuilt;
1490                            if try_gpu {
1491                                eprintln!("[wl-encode] rebuilding readback HW encoder after repeated encode errors.");
1492                            } else {
1493                                eprintln!(
1494                                    "[wl-encode] readback HW encoder unrecoverable; demoting to software encoding ({}).",
1495                                    encoders::SOFTWARE_H264_ENCODER
1496                                );
1497                            }
1498                            // The broken session is released before its replacement is opened:
1499                            // the failure it recovers from is usually device memory pressure,
1500                            // and holding both at once is what would make the rebuild fail too.
1501                            drop(video_encoder.take());
1502                            video_encoder = build_readback_encoders(&settings, try_gpu, None);
1503                            hw_rebuilt = try_gpu && video_encoder.is_some();
1504                            hw_fullcolor = encoder_fullcolor(video_encoder.as_ref(), &settings);
1505                            nv12_buffer = hw_plane_buffer(
1506                                width,
1507                                height,
1508                                hw_fullcolor,
1509                                matches!(video_encoder, Some(GpuEncoder::Vaapi(_))),
1510                            );
1511                            cfg.controls.force_idr.store(true, Ordering::Relaxed);
1512                            let n = wayland_stripe_count(&settings, video_encoder.is_some());
1513                            cfg.stats.n_stripes.store(n as u32, Ordering::Relaxed);
1514                            *cfg.stats.desc.lock().unwrap() =
1515                                encoder_desc(&settings, video_encoder.as_ref(), false);
1516                            log_stream_settings(&settings, n, video_encoder.as_ref());
1517                        }
1518                    }
1519                }
1520            }
1521        } else {
1522            let mut damage = std::mem::take(&mut f.damage);
1523            if f.is_animated {
1524                damage.push(Rectangle::new((0, 0).into(), (width, height).into()));
1525            }
1526            let force_idr_all = requested_idr
1527                || (settings.output_mode == 1
1528                    && crate::pipeline::periodic_idr_due(&settings, f.frame_id));
1529            out = encoders::software::encode_cpu(
1530                &mut stripes,
1531                &mut stripes_carrying,
1532                &f.buf,
1533                width,
1534                height,
1535                &damage,
1536                &settings,
1537                f.frame_id,
1538                cfg.use_gpu,
1539                false,
1540                force_idr_all,
1541            );
1542        }
1543
1544        let WlFrame { id, buf, .. } = f;
1545        pool.recycle(id, buf);
1546        // An unserved request stays armed: on an infinite GOP an IDR lost to an encode
1547        // error or skip would never self-heal.
1548        if requested_idr && out.is_empty() {
1549            cfg.controls.force_idr.store(true, Ordering::Relaxed);
1550        }
1551        if !out.is_empty() {
1552            cfg.stats.frames.fetch_add(1, Ordering::Relaxed);
1553            cfg.stats.stripes.fetch_add(out.len() as u32, Ordering::Relaxed);
1554            if let Some(ref socket) = recording_sink {
1555                socket.write_frame(&out, settings.height);
1556            }
1557            crate::recorder::wayland_tap(cfg.display_id, &out);
1558            let _ = cfg.deliver_tx.send(out);
1559        }
1560    }
1561    if settings.debug_logging {
1562        println!(
1563            "[Wayland] Encode thread exiting (hw={}, stripes={}).",
1564            video_encoder.is_some(),
1565            stripes.len()
1566        );
1567    }
1568    video_encoder
1569}
1570
1571/// The chroma format a session actually carries, which is not always the one requested: NVENC
1572/// takes 4:4:4 whenever asked, VA-API only when the driver and FFmpeg build both carry it, and the
1573/// software path (`None`) only when the build's software encoder does (`SOFTWARE_H264_FULLCOLOR`:
1574/// libx264 yes, OpenH264 no). Every consumer of "is this stream 4:4:4" — the readback buffer
1575/// sizing, the CSC branch, and both log lines — reads it from here so they cannot disagree.
1576fn encoder_fullcolor(video_encoder: Option<&GpuEncoder>, settings: &RustCaptureSettings) -> bool {
1577    match video_encoder {
1578        Some(GpuEncoder::Vaapi(enc)) => enc.is_fullcolor(),
1579        Some(GpuEncoder::Nvenc(_)) => settings.video_fullcolor,
1580        None => settings.video_fullcolor && encoders::SOFTWARE_H264_FULLCOLOR,
1581    }
1582}
1583
1584/// Compose the encoder half of the 1 s debug log line (backend, colorspace, frame mode) for
1585/// whichever thread owns the encoders.
1586fn encoder_desc(
1587    settings: &RustCaptureSettings,
1588    video_encoder: Option<&GpuEncoder>,
1589    zero_copy: bool,
1590) -> String {
1591    if settings.output_mode == 0 {
1592        return format!("JPEG Q:{}", settings.jpeg_quality);
1593    }
1594    let copy_mode = if zero_copy { "ZeroCopy" } else { "Readback" };
1595    let backend = match video_encoder {
1596        Some(GpuEncoder::Nvenc(_)) => format!("NVENC ({})", copy_mode),
1597        Some(GpuEncoder::Vaapi(_)) => format!("VAAPI ({})", copy_mode),
1598        None => format!("CPU {}", encoders::SOFTWARE_H264_ENCODER),
1599    };
1600    let is_444 = encoder_fullcolor(video_encoder, settings);
1601    let cs_str = if is_444 { "CS_IN:I444" } else { "CS_IN:I420" };
1602    // Only the software encoder carries 4:4:4 at full range; NVENC's hardware CSC is
1603    // limited-range whatever the chroma format.
1604    let range_str = if is_444 && video_encoder.is_none() { "FR" } else { "LR" };
1605    let frame_str = if video_encoder.is_some() || settings.video_fullframe {
1606        "FF"
1607    } else {
1608        "Striped"
1609    };
1610    format!("H264 ({}) {} {} {} CRF:{}", backend, cs_str, range_str, frame_str, settings.video_crf)
1611}
1612
1613/// How many horizontal stripes a frame is split into, for the settings line and the stats.
1614///
1615/// A full-frame session (`fullframe_encoder`: a HW encoder, or a forced `video_fullframe`) is
1616/// one contiguous H.264 stream and so a single stripe. Everything else asks the encoder's own
1617/// rule, so what is reported is what is encoded.
1618fn wayland_stripe_count(settings: &RustCaptureSettings, fullframe_encoder: bool) -> usize {
1619    crate::encoders::software::stripe_count(
1620        settings.height,
1621        settings.output_mode,
1622        fullframe_encoder || settings.video_fullframe,
1623    )
1624}
1625
1626/// One-shot "Stream settings active" line, printed by the thread that owns the encoders
1627/// once the selection is final (calloop for zero-copy, encode thread for readback).
1628fn log_stream_settings(
1629    settings: &RustCaptureSettings,
1630    n_stripes: usize,
1631    video_encoder: Option<&GpuEncoder>,
1632) {
1633    let mut log_msg = format!(
1634        "Stream settings active -> Res: {}x{} | FPS: {:.1} | Stripes: {}",
1635        settings.width, settings.height, settings.target_fps, n_stripes
1636    );
1637
1638    if settings.output_mode == 0 {
1639        log_msg.push_str(&format!(" | Mode: JPEG | Quality: {}", settings.jpeg_quality));
1640        if settings.use_paint_over_quality {
1641            log_msg.push_str(&format!(
1642                " | PaintOver Q: {} (Trigger: {}f)",
1643                settings.paint_over_jpeg_quality, settings.paint_over_trigger_frames
1644            ));
1645        }
1646    } else {
1647        let encoder_type = match video_encoder {
1648            Some(GpuEncoder::Nvenc(_)) => "NVENC",
1649            Some(GpuEncoder::Vaapi(_)) => "VAAPI",
1650            None => encoders::SOFTWARE_H264_ENCODER,
1651        };
1652        log_msg.push_str(&format!(" | Mode: H264 ({})", encoder_type));
1653
1654        if video_encoder.is_some() || settings.video_fullframe {
1655            log_msg.push_str(" FullFrame");
1656        } else {
1657            log_msg.push_str(" Striped");
1658        }
1659
1660        if settings.video_streaming_mode {
1661            log_msg.push_str(" Streaming");
1662        }
1663
1664        if settings.video_cbr_mode {
1665            log_msg.push_str(&format!(" | CBR {}", settings.video_bitrate_kbps));
1666        } else {
1667            log_msg.push_str(&format!(" | CRF: {}", settings.video_crf));
1668            if settings.video_bitrate_kbps > 0 {
1669                log_msg.push_str(&format!(" | VBV: {} kbps", settings.video_bitrate_kbps));
1670            }
1671        }
1672
1673        if settings.use_paint_over_quality {
1674            log_msg.push_str(&format!(
1675                " | PaintOver CRF: {} (Burst: {}f)",
1676                settings.video_paintover_crf, settings.video_paintover_burst_frames
1677            ));
1678        }
1679
1680        let is_actually_444 = encoder_fullcolor(video_encoder, settings);
1681        log_msg.push_str(&format!(
1682            " | Colorspace: {}",
1683            encoders::colorspace_desc(is_actually_444, video_encoder.is_none())
1684        ));
1685    }
1686
1687    log_msg.push_str(&format!(
1688        " | Damage Thresh: {}f | Damage Dur: {}f",
1689        settings.damage_block_threshold, settings.damage_block_duration
1690    ));
1691
1692    println!("{}", log_msg);
1693}
1694
1695/// Tear down a capture's encode/delivery threads and pools, returning both join handles as
1696/// `(deliver, encode)` for the caller to place. NEITHER is joined here: this runs on the
1697/// calloop thread, which also dispatches Wayland clients and input, and either thread can be
1698/// parked behind a consumer still inside Python. A restart hands each handle to its
1699/// successor thread, which joins it off the event loop — the encode successor inheriting the
1700/// readback hardware session, the deliver successor keeping stripes in capture order across
1701/// the restart — and a plain stop hands both to the reaper. The zero-copy session (if any)
1702/// is left on the capture for the caller to reuse or drop.
1703fn teardown_capture(
1704    cap: &mut wayland::frontend::WlCapture,
1705) -> (
1706    Option<std::thread::JoinHandle<()>>,
1707    Option<std::thread::JoinHandle<Option<GpuEncoder>>>,
1708) {
1709    if let Some(p) = cap.encode_pool.take() {
1710        p.shutdown();
1711    }
1712    if let Some(flag) = cap.deliver_discard.take() {
1713        flag.store(true, Ordering::Relaxed);
1714    }
1715    let encode_join = cap.encode_join.take();
1716    if let Some(tx) = cap.deliver_tx.take() {
1717        drop(tx);
1718    }
1719    let join = cap.deliver_join.take();
1720    cap.pending_hw_delivery = None;
1721    cap.pending_hw_damage = false;
1722    // The last strong reference: dropping it here, before a following start binds its own
1723    // sink, is what keeps the outgoing sink's unlink from stripping the successor's socket
1724    // path. The encode thread holds only a Weak.
1725    cap.recording_sink = None;
1726    (join, encode_join)
1727}
1728
1729/// Drop a host-capture session whose compositor connection has ended, stopping every
1730/// capture it fed: their displays leave the alive set, so `is_capturing` turns false and the
1731/// consumer rebuilds them (the next start reconnects), and no input is written to a dead
1732/// connection. The same signal X11 gives when its capture thread ends.
1733fn reap_dead_host(state: &mut AppState) {
1734    if !state.host.as_ref().is_some_and(|h| !h.alive()) {
1735        return;
1736    }
1737    eprintln!("[HostCapture] host compositor connection lost; captures stop until restarted.");
1738    let ids: Vec<u32> = state
1739        .output_nodes
1740        .iter()
1741        .filter(|n| n.capture.is_some())
1742        .map(|n| n.id)
1743        .collect();
1744    for id in ids {
1745        stop_capture_on_display(state, id);
1746        // stop_capture_on_display cleared this display's outcome; overwrite it with the
1747        // host-death reason so capture_state reports why the pipeline went down (input is
1748        // black-holed to a dead connection until a fresh start rebuilds against a new one).
1749        set_wayland_capture_err(
1750            id,
1751            Some("host compositor connection lost; capture stopped".to_string()),
1752        );
1753    }
1754    state.host = None;
1755    // Nothing will answer the requests still in flight: their geometry readers get
1756    // what the outputs show now.
1757    let pending: Vec<_> = state.host_layout_pending.drain().collect();
1758    for (id, p) in pending {
1759        answer_geometry_waiters(state, id, p.geometry_waiters);
1760    }
1761}
1762
1763/// Stop the capture bound to `display_id`, leaving the output (and every other display's
1764/// capture) running.
1765fn stop_capture_on_display(state: &mut AppState, display_id: u32) {
1766    let Some(idx) = state.node_idx_for_id(display_id) else { return };
1767    if let Some(mut cap) = state.output_nodes[idx].capture.take() {
1768        println!("[Wayland] Capture loop stopped (display {display_id}).");
1769        cap.video_encoder = None;
1770        let (join, encode_join) = teardown_capture(&mut cap);
1771        state.deliver_reaper.extend(join);
1772        state.encode_reaper.extend(encode_join);
1773    }
1774    wayland_alive().lock().unwrap().remove(&display_id);
1775    set_wayland_capture_err(display_id, None);
1776    if let Some(p) = state.host_layout_pending.remove(&display_id) {
1777        answer_geometry_waiters(state, display_id, p.geometry_waiters);
1778    }
1779}
1780
1781/// The geometry `get_realized_geometry` reports for `display_id`: the live capture's size
1782/// and scale, else the output's current mode, else zeros for an unknown display.
1783fn realized_geometry(state: &AppState, display_id: u32) -> (i32, i32, f64) {
1784    state
1785        .node_idx_for_id(display_id)
1786        .map(|idx| {
1787            let node = &state.output_nodes[idx];
1788            match node.capture.as_ref() {
1789                Some(c) => (c.settings.width, c.settings.height, c.settings.scale),
1790                None => node
1791                    .output
1792                    .current_mode()
1793                    .map(|m| {
1794                        (
1795                            m.size.w,
1796                            m.size.h,
1797                            node.output.current_scale().fractional_scale(),
1798                        )
1799                    })
1800                    .unwrap_or((0, 0, 0.0)),
1801            }
1802        })
1803        .unwrap_or((0, 0, 0.0))
1804}
1805
1806/// Answer geometry readers parked behind a host layout request with what `display_id`
1807/// captures now.
1808fn answer_geometry_waiters(
1809    state: &AppState,
1810    display_id: u32,
1811    waiters: Vec<std::sync::mpsc::Sender<(i32, i32, f64)>>,
1812) {
1813    if waiters.is_empty() {
1814        return;
1815    }
1816    let info = realized_geometry(state, display_id);
1817    for w in waiters {
1818        let _ = w.send(info);
1819    }
1820}
1821
1822/// What the host's verdict on a layout request means for a capture configured at `want`:
1823/// `Some(size)` when it has to be re-sized to the mode the host kept (the request was
1824/// declined and the host runs a different size), `None` when nothing changes — the host
1825/// applied the request, already runs that size, or its mode is unknown (the capture keeps
1826/// gating on the size it asked for).
1827fn host_layout_resolution(
1828    realized: bool,
1829    want: (i32, i32),
1830    current: Option<(i32, i32)>,
1831) -> Option<(i32, i32)> {
1832    if realized {
1833        return None;
1834    }
1835    current.filter(|&c| c != want)
1836}
1837
1838/// Settle the host layout requests answered since the last tick. One the host applied
1839/// needs nothing: the capture was configured for that size and its frames flow as the
1840/// host switches. One the host kept its own mode against (declined, no layout
1841/// management, no answer by the deadline) re-sizes the capture to that mode through the
1842/// same in-place reconfigure a resize takes, so it never gates on a size the host will
1843/// not produce, and the refusal becomes the capture's caveat. Geometry readers parked
1844/// behind a request answer once it is settled, with the size actually captured.
1845fn reconcile_host_layouts(state: &mut AppState) {
1846    if state.host_layout_pending.is_empty() {
1847        return;
1848    }
1849    let ids: Vec<u32> = state.host_layout_pending.keys().copied().collect();
1850    for id in ids {
1851        // An earlier iteration's restart may have reaped the host (and every request).
1852        let Some(pending) = state.host_layout_pending.get(&id) else { continue };
1853        let verdict = match state.host.as_ref() {
1854            Some(host) => match host.layout_outcome(pending.epoch) {
1855                None => continue,
1856                Some(realized) => {
1857                    host_layout_resolution(realized, pending.want, host.current_output_size(id))
1858                }
1859            },
1860            None => None,
1861        };
1862        let Some(pending) = state.host_layout_pending.remove(&id) else { continue };
1863        if let Some((rw, rh)) = verdict {
1864            let (w, h) = pending.want;
1865            let restart = state
1866                .node_idx_for_id(id)
1867                .and_then(|idx| state.output_nodes[idx].capture.as_ref())
1868                .map(|cap| (cap.callback.clone(), cap.settings.clone()));
1869            if let Some((cb, mut settings)) = restart {
1870                settings.width = rw;
1871                settings.height = rh;
1872                // H.264 even-masks its dimensions, so an odd host mode is followed as closely
1873                // as the encoder can; asking again would only loop.
1874                let followed = if settings.output_mode == 1 { (rw & !1, rh & !1) } else { (rw, rh) };
1875                if followed != (w, h) {
1876                    eprintln!(
1877                        "[HostCapture] host kept {rw}x{rh} for display {id} ({w}x{h} declined); capturing at that size."
1878                    );
1879                    start_capture_on_display(state, id, cb, settings);
1880                    let refusal = format!("host kept {rw}x{rh} ({w}x{h} declined)");
1881                    let own = wayland_capture_err().lock().unwrap().get(&id).cloned();
1882                    set_wayland_capture_err(
1883                        id,
1884                        Some(match own {
1885                            Some(e) => format!("{refusal}; {e}"),
1886                            None => refusal,
1887                        }),
1888                    );
1889                }
1890            }
1891        }
1892        // Settled either way: the restart, if any, runs at the size the host has, so the
1893        // readers need not wait for the host to acknowledge it again.
1894        answer_geometry_waiters(state, id, pending.geometry_waiters);
1895    }
1896}
1897
1898/// Start (or in-place reconfigure) the capture bound to output `display_id`: reprogram the
1899/// output's mode/scale/refresh, size the render targets, fullscreen the display's windows at
1900/// the new logical size, resolve the encode path (zero-copy vs readback), and spawn the
1901/// delivery (and readback-mode encode) threads. The single-display behavior of the former
1902/// global StartCapture is preserved exactly for display 0.
1903/// Bring up the readback encode path (pixman readback → pool → encode thread) for a
1904/// capture whose zero-copy session is absent or just died. Mirrors the start-capture
1905/// bootstrap: u64::MAX content generations mark every pool slot stale so each one is
1906/// read back before its first publish, whatever the damage says.
1907fn bootstrap_readback_pool(
1908    cap: &mut wayland::frontend::WlCapture,
1909    display_id: u32,
1910    use_gpu: bool,
1911    try_gpu: bool,
1912    prior: Option<GpuEncoder>,
1913    predecessor: Option<std::thread::JoinHandle<Option<GpuEncoder>>>,
1914) {
1915    let Some(deliver_tx) = cap.deliver_tx.clone() else {
1916        return;
1917    };
1918    let settings = cap.settings.clone();
1919    let pool = Arc::new(WlFramePool::new(
1920        WL_POOL_SURFACES,
1921        (settings.width.max(0) as usize) * (settings.height.max(0) as usize) * 4,
1922    ));
1923    cap.pool_last_render = vec![0; WL_POOL_SURFACES];
1924    cap.render_seq = 0;
1925    cap.pool_content_gen = vec![u64::MAX; WL_POOL_SURFACES];
1926    cap.content_gen = 0;
1927    let c = &cap.encode_controls;
1928    c.bitrate_kbps.store(settings.video_bitrate_kbps, Ordering::Relaxed);
1929    c.vbv_mult_milli.store(
1930        (settings.video_vbv_multiplier * 1000.0).round() as i32,
1931        Ordering::Relaxed,
1932    );
1933    c.fps_milli.store(
1934        (settings.target_fps.max(1.0) * 1000.0) as u64,
1935        Ordering::Relaxed,
1936    );
1937    let cfg = WlEncodeConfig {
1938        settings: settings.clone(),
1939        display_id,
1940        use_gpu,
1941        try_gpu,
1942        prior,
1943        predecessor,
1944        recording_sink: cap.recording_sink.as_ref().map(Arc::downgrade),
1945        deliver_tx,
1946        controls: cap.encode_controls.clone(),
1947        stats: cap.encode_stats.clone(),
1948    };
1949    let pool2 = pool.clone();
1950    cap.encode_join = Some(
1951        thread::Builder::new()
1952            .name(format!("wl-encode-{display_id}"))
1953            .spawn(move || wayland_encode_loop(&pool2, cfg))
1954            .expect("failed to spawn wl-encode thread"),
1955    );
1956    cap.encode_pool = Some(pool);
1957}
1958
1959/// Rebuild a broken zero-copy hardware session with the startup construction (driver
1960/// match, EGL display hand-over, the same chroma negotiation). `None` means unrecoverable —
1961/// the caller demotes to readback.
1962fn rebuild_zerocopy_encoder(
1963    cap: &wayland::frontend::WlCapture,
1964    state: &mut AppState,
1965) -> Option<GpuEncoder> {
1966    let settings = &cap.settings;
1967    let encode_driver = get_gpu_driver(settings.encode_node_index.max(0));
1968    if driver_selects_nvenc(&encode_driver) {
1969        let egl_display = state
1970            .gles_renderer
1971            .as_ref()
1972            .map(|r| r.egl_context().display().get_display_handle().handle)
1973            .unwrap_or(std::ptr::null());
1974        NvencEncoder::new(settings, egl_display)
1975            .ok()
1976            .map(GpuEncoder::Nvenc)
1977    } else {
1978        VaapiEncoder::new(settings).ok().map(GpuEncoder::Vaapi)
1979    }
1980}
1981
1982/// Consecutive encode failures before a hardware path recovers (~0.5s at 60fps): a hiccup
1983/// outlasts it, anything longer starts to look like a dead session. Shared by every hardware
1984/// encode path (Wayland zero-copy, Wayland readback, X11) so recovery timing matches.
1985pub(crate) const HW_ERROR_RECOVERY_THRESHOLD: u32 = 30;
1986
1987fn start_capture_on_display(
1988    state: &mut AppState,
1989    display_id: u32,
1990    cb: Option<Arc<Py<PyAny>>>,
1991    mut settings: RustCaptureSettings,
1992) {
1993    use smithay::wayland::fractional_scale::with_fractional_scale;
1994
1995    // The cursor worker outlives individual captures, so the starting settings have to
1996    // reach it here — same point X11 applies the cap — or it keeps the previous capture's.
1997    let _ = state.cursor_tx.send(CursorJob::SetSizeCap(settings.cursor_size_cap));
1998
1999    // Fresh attempt: drop any prior outcome so a stale caveat cannot read as this start's.
2000    set_wayland_capture_err(display_id, None);
2001
2002    let Some(node_idx) = state.node_idx_for_id(display_id) else {
2003        eprintln!("[Wayland] StartCapture: no output with display id {display_id}.");
2004        set_wayland_capture_err(
2005            display_id,
2006            Some(format!("no output with display id {display_id}")),
2007        );
2008        return;
2009    };
2010    let mut node = state.output_nodes.remove(node_idx);
2011    // Geometry readers parked behind a layout request this start supersedes: they ride
2012    // on to this start's request, or answer at its end if no host is involved.
2013    let mut geometry_waiters = state
2014        .host_layout_pending
2015        .remove(&display_id)
2016        .map(|p| p.geometry_waiters)
2017        .unwrap_or_default();
2018
2019    if state.auto_gpu_selected && settings.encode_node_index < -1
2020        && let Some(idx_str) = state.render_node_path.strip_prefix("/dev/dri/renderD")
2021        && let Ok(idx) = idx_str.parse::<i32>() {
2022                settings.encode_node_index = idx - 128;
2023            }
2024
2025    if settings.output_mode == 1 {
2026        settings.width &= !1;
2027        settings.height &= !1;
2028    }
2029
2030    // Tear down this display's previous capture first; its hardware sessions are the
2031    // reuse candidates below (zero-copy inline, readback via the encode config).
2032    let mut prior_zero_copy: Option<GpuEncoder> = None;
2033    let mut prior_encode_join: Option<std::thread::JoinHandle<Option<GpuEncoder>>> = None;
2034    let mut prior_deliver_join: Option<std::thread::JoinHandle<()>> = None;
2035    if let Some(mut old) = node.capture.take() {
2036        prior_zero_copy = old.video_encoder.take();
2037        (prior_deliver_join, prior_encode_join) = teardown_capture(&mut old);
2038    }
2039
2040    // Bind only after the old capture is gone: its sink unlinks the socket path in
2041    // Drop, which would strip a fresh bind's filesystem name and leave every later
2042    // recorder connect with ENOENT.
2043    let recording_sink = crate::recording_sink::RecordingSink::try_bind(&settings.recording_socket);
2044
2045    // Host-capture mode: connect on first use. The display's mode is requested from
2046    // the host further down, without waiting for an answer; a host that keeps its own
2047    // mode (refusal, or no layout management — KWin) has this capture re-sized to it
2048    // when the verdict arrives (`reconcile_host_layouts`), the same way a failed GBM
2049    // resize falls back to the live mode.
2050    let host_capture = !settings.wayland_host_display.is_empty();
2051    reap_dead_host(state);
2052    if host_capture && state.host.is_none() {
2053        // Capture buffers come from the same render node the encoder imports
2054        // from, resolved via its live fd (the path string is not retained in
2055        // auto mode); each capture thread opens its own device handle.
2056        let gbm_path = if state.use_gpu {
2057            state.gbm_device.as_ref().and_then(|dev| {
2058                use std::os::fd::{AsFd as _, AsRawFd as _};
2059                let fd = dev.as_fd().as_raw_fd();
2060                std::fs::read_link(format!("/proc/self/fd/{fd}")).ok()
2061            })
2062        } else {
2063            None
2064        };
2065        match crate::wayland::host::HostSession::connect(&settings.wayland_host_display, gbm_path) {
2066            Ok(h) => {
2067                println!(
2068                    "[HostCapture] capturing host compositor '{}' ({} outputs).",
2069                    settings.wayland_host_display,
2070                    h.output_count()
2071                );
2072                state.host = Some(h);
2073            }
2074            Err(e) => {
2075                eprintln!(
2076                    "[HostCapture] connect '{}' failed: {e}",
2077                    settings.wayland_host_display
2078                );
2079                set_wayland_capture_err(
2080                    display_id,
2081                    Some(format!(
2082                        "host compositor '{}' connect failed ({e}); capturing locally",
2083                        settings.wayland_host_display
2084                    )),
2085                );
2086            }
2087        }
2088    }
2089    // A failed connect leaves this display compositing locally, so its frames follow the
2090    // local renderer again.
2091    let host_capture = host_capture && state.host.is_some();
2092    if host_capture && let Some(host) = &state.host {
2093        // The node's layout offset rides along so the host's heads mirror
2094        // selkies' union layout (input coordinates already assume it).
2095        host.set_layout(display_id, node.pos.0, node.pos.1);
2096    }
2097
2098    {
2099        // Never panic the compositor thread: an output momentarily without a current
2100        // mode falls back to the requested geometry so the reconfigure below is a
2101        // no-op for size/refresh instead of unwrap-panicking.
2102        let target_refresh = (settings.target_fps * 1000.0).round() as i32;
2103        let (current_w, current_h, current_refresh) = match node.output.current_mode() {
2104            Some(m) => (m.size.w, m.size.h, m.refresh),
2105            None => (settings.width, settings.height, target_refresh),
2106        };
2107        let current_scale = node.output.current_scale().fractional_scale();
2108
2109        if current_w != settings.width
2110            || current_h != settings.height
2111            || (current_scale - settings.scale).abs() > 0.001
2112            || current_refresh != target_refresh
2113        {
2114            // Allocate the GPU backing for the new dimensions BEFORE committing
2115            // anything: if the driver refuses (VRAM exhaustion, dimensions it will
2116            // not back), the whole reconfigure is skipped and the previous mode +
2117            // buffers stay live. A failed resize must degrade to "no resize", never
2118            // panic the compositor thread.
2119            let mut new_offscreen = None;
2120            let mut gbm_resize_failed = false;
2121            if state.use_gpu
2122                && let Some(gbm) = state.gbm_device.as_mut() {
2123                    match gbm.create_buffer_object(
2124                        settings.width as u32,
2125                        settings.height as u32,
2126                        GbmFormat::Argb8888,
2127                        BufferObjectFlags::RENDERING,
2128                    ) {
2129                        Ok(bo) => {
2130                            let dmabuf = create_dmabuf_from_bo(&bo);
2131                            new_offscreen = Some((bo, dmabuf));
2132                        }
2133                        Err(e) => {
2134                            eprintln!(
2135                                "[Wayland] GBM buffer resize to {}x{} failed ({:?}); keeping previous output mode.",
2136                                settings.width, settings.height, e
2137                            );
2138                            gbm_resize_failed = true;
2139                        }
2140                    }
2141                }
2142            if gbm_resize_failed {
2143                // The mode commit below is skipped wholesale, so the rest of this
2144                // StartCapture (encoder setup, stored settings) must see the
2145                // dimensions actually live.
2146                set_wayland_capture_err(
2147                    display_id,
2148                    Some(format!(
2149                        "GPU buffer resize to {}x{} refused; kept {current_w}x{current_h}",
2150                        settings.width, settings.height
2151                    )),
2152                );
2153                settings.width = current_w;
2154                settings.height = current_h;
2155                settings.scale = current_scale;
2156                settings.target_fps = current_refresh as f64 / 1000.0;
2157            } else {
2158                println!(
2159                    "[Wayland] Configuring Output {} ({}): {}x{} @ {:.2} FPS (Scale {:.2})",
2160                    display_id, node.output.name(),
2161                    settings.width, settings.height, settings.target_fps, settings.scale
2162                );
2163                let new_mode = OutputMode {
2164                    size: (settings.width, settings.height).into(),
2165                    refresh: target_refresh,
2166                };
2167                node.output.change_current_state(
2168                    Some(new_mode),
2169                    Some(Transform::Normal),
2170                    Some(OutputScale::Fractional(settings.scale)),
2171                    Some(Point::from(node.pos)),
2172                );
2173                node.output.set_preferred(new_mode);
2174                // Capture clients allocate to our announced size; a stale size means
2175                // every frame they submit from now on fails buffer validation.
2176                for cs in state
2177                    .copy_sessions
2178                    .iter()
2179                    .filter(|cs| cs.output.upgrade().as_ref() == Some(&node.output))
2180                {
2181                    if let Some(c) = wayland::frontend::output_capture_constraints(
2182                        &node.output,
2183                        state.gles_renderer.as_ref(),
2184                        &state.render_node_path,
2185                    ) {
2186                        cs.session.update_constraints(c);
2187                    }
2188                }
2189
2190                let pixel_count =
2191                    (settings.width.max(0) as usize) * (settings.height.max(0) as usize);
2192                node.frame_buffer = vec![0u8; pixel_count * 4];
2193                node.target_seeded = false;
2194
2195                if let Some(off) = new_offscreen.take() {
2196                    node.offscreen_buffer = Some(off);
2197                }
2198            }
2199        }
2200
2201        let scale = settings.scale.max(0.1);
2202        let logical_width = (settings.width as f64 / scale).round() as i32;
2203        let logical_height = (settings.height as f64 / scale).round() as i32;
2204
2205        for window in state.space.elements() {
2206            if wayland::frontend::window_output_id(window) != display_id {
2207                continue;
2208            }
2209            // A parked screen is tagged for this display but composited on none: it
2210            // holds PARKED_LOGICAL_SIZE until `create_output` gives it one. Resizing it
2211            // here would double the session's coordinate space onto a screen nobody
2212            // watches, which is where a window that centres itself then lands.
2213            if wayland::frontend::window_meta(window)
2214                .is_some_and(|meta| meta.parked.load(Ordering::Relaxed))
2215            {
2216                continue;
2217            }
2218            if let Some(surface) = window.wl_surface() {
2219                node.output.enter(&surface);
2220                with_states(&surface, |states| {
2221                    smithay::wayland::compositor::send_surface_state(
2222                        &surface, states, scale.ceil() as i32, Transform::Normal,
2223                    );
2224                    with_fractional_scale(states, |fs| {
2225                        fs.set_preferred_scale(scale);
2226                    });
2227                });
2228            }
2229            if let Some(toplevel) = window.toplevel() {
2230                toplevel.with_pending_state(|state| {
2231                    use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State;
2232                    state.states.set(State::Fullscreen);
2233                    state.states.set(State::Activated);
2234                    state.size = Some((logical_width, logical_height).into());
2235                });
2236                toplevel.send_configure();
2237            }
2238        }
2239    }
2240
2241    let use_cpu_explicit = settings.use_cpu || settings.encode_node_index == -1;
2242    let gpu_intent = settings.output_mode == 1 && !use_cpu_explicit;
2243    if use_cpu_explicit {
2244        println!("[Wayland] CPU encoding selected (use_cpu=true or encode_node_index=-1).");
2245    }
2246
2247    let mut different_gpu = false;
2248    if gpu_intent {
2249        let encode_node_idx = settings.encode_node_index.max(0);
2250        if !state.render_node_path.is_empty()
2251            && !state.render_node_path.contains(&format!("renderD{}", 128 + encode_node_idx))
2252        {
2253            different_gpu = true;
2254        }
2255    }
2256
2257    let mut video_encoder: Option<GpuEncoder> = None;
2258    if gpu_intent && state.use_gpu && !different_gpu {
2259        let encode_driver = get_gpu_driver(settings.encode_node_index.max(0));
2260        println!(
2261            "[Wayland] Encode Node Index: {} | Driver: {}",
2262            settings.encode_node_index.max(0), encode_driver
2263        );
2264
2265        if driver_selects_nvenc(&encode_driver) {
2266            let reused = match prior_zero_copy.as_mut() {
2267                Some(GpuEncoder::Nvenc(enc)) => match enc.reconfigure_resolution(&settings) {
2268                    Ok(()) => {
2269                        println!("[Wayland] NVENC session reconfigured in place.");
2270                        true
2271                    }
2272                    Err(e) => {
2273                        eprintln!("[Wayland] NVENC in-place reconfigure unavailable ({e}); rebuilding.");
2274                        false
2275                    }
2276                },
2277                _ => false,
2278            };
2279            if reused {
2280                video_encoder = prior_zero_copy.take();
2281            } else {
2282                prior_zero_copy = None;
2283                println!("[Wayland] Nvidia Encoder detected. Initializing NVENC...");
2284                let egl_display = if let Some(renderer) = state.gles_renderer.as_ref() {
2285                    renderer.egl_context().display().get_display_handle().handle
2286                } else {
2287                    std::ptr::null()
2288                };
2289
2290                match NvencEncoder::new(&settings, egl_display) {
2291                    Ok(encoder) => {
2292                        video_encoder = Some(GpuEncoder::Nvenc(encoder));
2293                        println!("[Wayland] NVENC Encoder initialized successfully.");
2294                    }
2295                    Err(e) => {
2296                        eprintln!(
2297                            "[Wayland] Failed to init NVENC: {}. Falling back to CPU.",
2298                            e
2299                        );
2300                        set_wayland_capture_err(
2301                            display_id,
2302                            Some(format!("NVENC init failed ({e}); using CPU encode")),
2303                        );
2304                    }
2305                }
2306            }
2307        } else {
2308            prior_zero_copy = None;
2309            println!("[Wayland] Initializing Unified VAAPI Encoder...");
2310            match VaapiEncoder::new(&settings) {
2311                Ok(encoder) => {
2312                    println!(
2313                        "[Wayland] VAAPI Encoder initialized successfully ({}).",
2314                        if encoder.is_fullcolor() { "4:4:4" } else { "4:2:0" }
2315                    );
2316                    video_encoder = Some(GpuEncoder::Vaapi(encoder));
2317                }
2318                Err(e) => {
2319                    eprintln!(
2320                        "[Wayland] Failed to init VAAPI: {}. Falling back to CPU.",
2321                        e
2322                    );
2323                    set_wayland_capture_err(
2324                        display_id,
2325                        Some(format!("VAAPI init failed ({e}); using CPU encode")),
2326                    );
2327                }
2328            }
2329        }
2330    }
2331    drop(prior_zero_copy);
2332
2333    if different_gpu {
2334        println!("[Wayland] Decision: Rendering and Encoding GPUs differ -> Forcing Readback (CPU path for pixels).");
2335    }
2336    if video_encoder.is_none() {
2337        println!("[Wayland] Decision: Readback path (encode thread) active.");
2338    } else if !different_gpu {
2339        println!("[Wayland] Decision: Zero-Copy path active.");
2340    }
2341
2342    // Point this display's host capture thread at the size the encoder was just
2343    // configured for and ask the host for that mode. The compositor keeps running (CU,
2344    // clipboard callbacks, input fallbacks) but its renderer is bypassed. The buffer
2345    // type follows the consumer settled on above: dmabufs only for a zero-copy encoder,
2346    // shm frames for every CPU path (use_cpu, JPEG, readback). The host's
2347    // answer is not waited for here — this thread carries input and every other
2348    // display — but polled by the render tick, which re-sizes this capture should the
2349    // host keep a different mode; frames gate until the sizes agree.
2350    if host_capture && let Some(host) = &state.host {
2351        let epoch = host.start_capture(
2352            display_id,
2353            settings.width,
2354            settings.height,
2355            video_encoder.is_some(),
2356            settings.capture_cursor,
2357        );
2358        state.host_layout_pending.insert(
2359            display_id,
2360            wayland::frontend::PendingHostLayout {
2361                epoch,
2362                want: (settings.width, settings.height),
2363                geometry_waiters: std::mem::take(&mut geometry_waiters),
2364            },
2365        );
2366    }
2367
2368    if recording_sink.is_some() && settings.output_mode == 0 {
2369        eprintln!(
2370            "[recording_sink] WARNING: recording_socket is set but output_mode is JPEG (0). \
2371             The recording socket requires a single H.264 stream. Please set output_mode=1 \
2372             on the Python CaptureSettings to produce a recordable output."
2373        );
2374    }
2375
2376    // Every display's capture composites its own watermark, uploaded at this output's
2377    // scale and placed against this output's frame dimensions.
2378    let watermark_output_scale = node.output.current_scale().fractional_scale();
2379    node.overlay_state
2380        .load_watermark(&settings.watermark_path, watermark_output_scale);
2381    if display_id == 0 {
2382        state.settings = settings.clone();
2383        if state.cursor_callback_set
2384            && let Some(icon) = state.current_cursor_icon.clone() {
2385                state.send_cursor_image(&icon);
2386            }
2387    }
2388    state.render_cursor_on_framebuffer = settings.capture_cursor;
2389
2390    let mut cap = wayland::frontend::WlCapture {
2391        settings: settings.clone(),
2392        callback: cb.clone(),
2393        video_encoder,
2394        vaapi_state: StripeState::default(),
2395        recording_sink,
2396        deliver_tx: None,
2397        deliver_join: None,
2398        deliver_discard: None,
2399        pending_hw_delivery: None,
2400        pending_hw_damage: false,
2401        encode_pool: None,
2402        encode_join: None,
2403        encode_controls: Arc::new(WlEncodeControls::new()),
2404        encode_stats: Arc::new(WlEncodeStats::new()),
2405        pool_last_render: Vec::new(),
2406        render_seq: 0,
2407        pool_content_gen: Vec::new(),
2408        content_gen: 0,
2409        frame_counter: 0,
2410        pending_force_idr: false,
2411        needs_full_render: true,
2412        last_tick: None,
2413        hw_error_streak: 0,
2414        hw_rebuilt: false,
2415    };
2416
2417    {
2418        let (tx, rx) = std::sync::mpsc::sync_channel::<Vec<EncodedStripe>>(1);
2419        let discard = Arc::new(AtomicBool::new(false));
2420        let thread_discard = discard.clone();
2421        let predecessor = prior_deliver_join;
2422        // With no Python callback (internal recorder-owned capture) the delivery thread
2423        // only drains the channel: the recorder already consumed the frames at the
2424        // delivery-layer tap, upstream of this per-consumer handoff.
2425        let join = thread::spawn(move || {
2426            // The predecessor capture's deliver thread finishes first, off the
2427            // event loop: encoded stripes reach Python in capture order across
2428            // a reconfigure, and a stale pre-teardown stripe can never land
2429            // after this capture's first frame.
2430            if let Some(handle) = predecessor {
2431                let _ = handle.join();
2432            }
2433            match cb {
2434                Some(cb) => {
2435                    crate::boost_thread_priority(-10);
2436                    while let Ok(stripes) = rx.recv() {
2437                        if thread_discard.load(Ordering::Relaxed)
2438                            || PY_SHUTDOWN.load(Ordering::Relaxed) { continue; }
2439                        Python::attach(|py| {
2440                            for s in stripes {
2441                                match Py::new(py, StripeFrame::new_owned_meta(
2442                                    s.data, s.data_type, s.stripe_y_start,
2443                                    s.stripe_height, s.frame_id,
2444                                )) {
2445                                    Ok(f) => { if let Err(e) = cb.call1(py, (f,)) { e.print(py); } }
2446                                    Err(e) => eprintln!("[wayland] frame alloc error: {e:?}"),
2447                                }
2448                            }
2449                        });
2450                    }
2451                }
2452                None => while rx.recv().is_ok() {},
2453            }
2454        });
2455        cap.deliver_tx = Some(tx);
2456        cap.deliver_discard = Some(discard);
2457        cap.deliver_join = Some(join);
2458    }
2459
2460    if cap.video_encoder.is_none() {
2461        bootstrap_readback_pool(
2462            &mut cap,
2463            display_id,
2464            // Host frames land in the pool as BGRA whatever the local renderer is; only
2465            // a GLES readback of our own compositing produces RGBA.
2466            state.use_gpu && !host_capture,
2467            gpu_intent && (!state.use_gpu || different_gpu),
2468            None,
2469            prior_encode_join.take(),
2470        );
2471    } else {
2472        cap.encode_stats.n_stripes.store(1, Ordering::Relaxed);
2473        *cap.encode_stats.desc.lock().unwrap() =
2474            encoder_desc(&settings, cap.video_encoder.as_ref(), true);
2475        log_stream_settings(&settings, 1, cap.video_encoder.as_ref());
2476    }
2477    // A zero-copy start has no successor encode thread to inherit the outgoing readback
2478    // session, so the outgoing thread is reaped instead.
2479    state.encode_reaper.extend(prior_encode_join);
2480    // Force the keyframe unconditionally: the damage tracker and offscreen buffer
2481    // stay warm across stop/start, so a restarted capture on a static screen
2482    // otherwise produces no damage, no first frame, and no IDR in either path.
2483    cap.request_idr();
2484
2485    node.capture = Some(cap);
2486    // The start reprogrammed this output, and until a client answers at the new size the
2487    // compositor paints its clear colour over whatever the client does not cover — a
2488    // freshly created output, whose session window is still parked at a placeholder size,
2489    // is covered by none of it. Those frames are held rather than streamed as a blank
2490    // screen; the deadline releases an output no client ever draws on.
2491    node.content_hold_until = Some(Instant::now() + WL_CONTENT_HOLD);
2492    state.output_nodes.insert(node_idx, node);
2493    wayland_alive().lock().unwrap().insert(display_id);
2494    answer_geometry_waiters(state, display_id, geometry_waiters);
2495}
2496
2497/// One output's render + capture tick: composite the elements overlapping this output
2498/// (positions made output-local by subtracting its layout origin), track damage, feed the
2499/// display's own encode path, and answer a pending screenshot on the primary. Returns true
2500/// when the tick was skipped because this display's encode pool was still busy (the caller
2501/// then retries shortly instead of waiting a full frame interval).
2502/// Stamp the watermark element onto `target` in place — no clear, so the
2503/// captured content underneath stays — returning the draw's sync point for the
2504/// encoder to wait on.
2505fn draw_host_watermark(
2506    renderer: &mut GlesRenderer,
2507    overlay: &crate::encoders::overlay::OverlayState,
2508    target: &mut Dmabuf,
2509    size: (i32, i32),
2510) -> Result<SyncPoint, String> {
2511    let elem = overlay
2512        .get_watermark_element(renderer)
2513        .ok_or("watermark element unavailable")?;
2514    let mut fb = renderer.bind(target).map_err(|e| format!("bind: {e:?}"))?;
2515    let mut frame = renderer
2516        .render(&mut fb, (size.0, size.1).into(), Transform::Normal)
2517        .map_err(|e| format!("render: {e:?}"))?;
2518    let dst = elem.geometry(1.0.into());
2519    let local = Rectangle::from_size(dst.size);
2520    elem.draw(&mut frame, elem.src(), dst, &[local], &[], None)
2521        .map_err(|e| format!("draw: {e:?}"))?;
2522    frame.finish().map_err(|e| format!("finish: {e:?}"))
2523}
2524
2525/// Re-compose `src` (the retained host frame) plus the watermark into `target`:
2526/// the path a moving watermark needs, since re-drawing over the same retained
2527/// buffer would leave trails.
2528fn compose_host_watermark(
2529    renderer: &mut GlesRenderer,
2530    overlay: &crate::encoders::overlay::OverlayState,
2531    src: &Dmabuf,
2532    target: &mut Dmabuf,
2533    size: (i32, i32),
2534) -> Result<SyncPoint, String> {
2535    let elem = overlay
2536        .get_watermark_element(renderer)
2537        .ok_or("watermark element unavailable")?;
2538    let tex = renderer
2539        .import_dmabuf(src, None)
2540        .map_err(|e| format!("import: {e:?}"))?;
2541    let mut fb = renderer.bind(target).map_err(|e| format!("bind: {e:?}"))?;
2542    let full: Rectangle<i32, Physical> = Rectangle::from_size((size.0, size.1).into());
2543    let mut frame = renderer
2544        .render(&mut fb, (size.0, size.1).into(), Transform::Normal)
2545        .map_err(|e| format!("render: {e:?}"))?;
2546    frame
2547        .render_texture_from_to(
2548            &tex,
2549            Rectangle::from_size((size.0 as f64, size.1 as f64).into()),
2550            full,
2551            &[full],
2552            // Opaque: the capture format's undefined alpha must not blend, it
2553            // would leave the target's previous content (and stamped
2554            // watermarks) underneath.
2555            &[full],
2556            Transform::Normal,
2557            1.0,
2558            None,
2559            &[],
2560        )
2561        .map_err(|e| format!("texture: {e:?}"))?;
2562    let dst = elem.geometry(1.0.into());
2563    let local = Rectangle::from_size(dst.size);
2564    elem.draw(&mut frame, elem.src(), dst, &[local], &[], None)
2565        .map_err(|e| format!("draw: {e:?}"))?;
2566    frame.finish().map_err(|e| format!("finish: {e:?}"))
2567}
2568
2569fn warn_once_host_watermark(e: &str) {
2570    static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
2571    if !WARNED.swap(true, Ordering::Relaxed) {
2572        eprintln!("[HostCapture] watermark compositing failed: {e}");
2573    }
2574}
2575
2576/// Copy full rows from `src` (tight `width*4` stride) into an Argb8888/Xrgb8888 shm
2577/// buffer, honoring the destination's own offset and stride.
2578fn copy_rows_into_shm(
2579    buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer,
2580    src: &[u8],
2581    width: i32,
2582    height: i32,
2583) -> Result<(), String> {
2584    use smithay::wayland::shm::with_buffer_contents_mut;
2585    let src_stride = (width.max(0) as usize) * 4;
2586    if src.len() < src_stride * (height.max(0) as usize) {
2587        return Err("source smaller than advertised".into());
2588    }
2589    with_buffer_contents_mut(buffer, |ptr, len, spec| {
2590        if spec.width < width || spec.height < height {
2591            return Err("shm buffer smaller than the output".to_string());
2592        }
2593        let dst_stride = spec.stride as usize;
2594        let offset = spec.offset as usize;
2595        if dst_stride < src_stride || len < offset + dst_stride * (height as usize) {
2596            return Err("shm stride or length mismatch".to_string());
2597        }
2598        for y in 0..height as usize {
2599            unsafe {
2600                std::ptr::copy_nonoverlapping(
2601                    src.as_ptr().add(y * src_stride),
2602                    ptr.add(offset + y * dst_stride),
2603                    src_stride,
2604                );
2605            }
2606        }
2607        Ok(())
2608    })
2609    .map_err(|e| format!("{e:?}"))?
2610}
2611
2612/// Complete parked ext-image-copy-capture frames for this output from the frame the
2613/// render pass just produced. A dmabuf target is filled by one GPU blit from the
2614/// output's composited buffer; an shm target by one readback (GLES) or one row copy
2615/// (pixman). With no damage, a session that has already received content keeps its
2616/// frame parked, so a static screen costs capture clients nothing.
2617fn service_copy_frames(
2618    state: &mut AppState,
2619    node: &mut wayland::frontend::OutputNode,
2620    width: i32,
2621    height: i32,
2622    damage_rects: &[Rectangle<i32, Physical>],
2623) {
2624    use smithay::backend::renderer::{buffer_type, Blit, BufferType, ExportMem, TextureFilter};
2625    use smithay::utils::Buffer as BufferCoords;
2626
2627    if state.copy_sessions.is_empty() {
2628        return;
2629    }
2630    let time = state.clock.now();
2631    for i in 0..state.copy_sessions.len() {
2632        {
2633            let cs = &state.copy_sessions[i];
2634            if cs.output.upgrade().as_ref() != Some(&node.output)
2635                || cs.pending.is_none()
2636                || (damage_rects.is_empty() && cs.delivered_once)
2637            {
2638                continue;
2639            }
2640        }
2641        let first = !state.copy_sessions[i].delivered_once;
2642        let frame = state.copy_sessions[i].pending.take().unwrap();
2643        let buffer = frame.buffer();
2644        let full = Rectangle::<i32, Physical>::new((0, 0).into(), (width, height).into());
2645        let result: Result<(), String> = match buffer_type(&buffer) {
2646            Some(BufferType::Dma) => (|| {
2647                let renderer = state.gles_renderer.as_mut().ok_or("no GLES renderer")?;
2648                let mut client = smithay::wayland::dmabuf::get_dmabuf(&buffer)
2649                    .map_err(|e| e.to_string())?
2650                    .clone();
2651                let (_bo, offscreen) = node
2652                    .offscreen_buffer
2653                    .as_mut()
2654                    .ok_or("no composited buffer")?;
2655                let src = renderer.bind(offscreen).map_err(|e| format!("{e:?}"))?;
2656                let mut dst = renderer.bind(&mut client).map_err(|e| format!("{e:?}"))?;
2657                // ready() promises readable contents, and implicit dmabuf sync cannot
2658                // be relied on cross-process on every driver: wait out the blit fence
2659                // (microseconds) before declaring the frame done.
2660                renderer
2661                    .blit(&src, &mut dst, full, full, TextureFilter::Linear)
2662                    .map_err(|e| format!("{e:?}"))?
2663                    .wait()
2664                    .map_err(|_| "blit fence interrupted".to_string())?;
2665                Ok(())
2666            })(),
2667            Some(BufferType::Shm) => (|| {
2668                if let Some(renderer) = state.gles_renderer.as_mut() {
2669                    let (_bo, offscreen) = node
2670                        .offscreen_buffer
2671                        .as_mut()
2672                        .ok_or("no composited buffer")?;
2673                    let fb = renderer.bind(offscreen).map_err(|e| format!("{e:?}"))?;
2674                    let mapping = renderer
2675                        .copy_framebuffer(
2676                            &fb,
2677                            Rectangle::new((0, 0).into(), (width, height).into()),
2678                            Fourcc::Argb8888,
2679                        )
2680                        .map_err(|e| format!("{e:?}"))?;
2681                    let data = renderer.map_texture(&mapping).map_err(|e| format!("{e:?}"))?;
2682                    copy_rows_into_shm(&buffer, data, width, height)
2683                } else {
2684                    copy_rows_into_shm(&buffer, &node.frame_buffer, width, height)
2685                }
2686            })(),
2687            _ => Err("unsupported buffer type".into()),
2688        };
2689        match result {
2690            Ok(()) => {
2691                let damage: Option<Vec<Rectangle<i32, BufferCoords>>> = if first {
2692                    None
2693                } else {
2694                    Some(
2695                        damage_rects
2696                            .iter()
2697                            .map(|r| {
2698                                Rectangle::new(
2699                                    (r.loc.x, r.loc.y).into(),
2700                                    (r.size.w, r.size.h).into(),
2701                                )
2702                            })
2703                            .collect(),
2704                    )
2705                };
2706                frame.success(Transform::Normal, damage, time);
2707                state.copy_sessions[i].delivered_once = true;
2708            }
2709            Err(e) => {
2710                eprintln!("[Wayland] copy-capture frame failed: {e}");
2711                frame.fail(CaptureFailureReason::Unknown);
2712            }
2713        }
2714    }
2715}
2716
2717/// Answer the frame callbacks of a surface-backed cursor. Xwayland and libwayland-cursor
2718/// clients wait for the cursor surface's frame callback before attaching their next
2719/// sprite, so a cursor surface that never gets one stops updating after its first sprite.
2720fn send_cursor_frame(state: &AppState, output: &Output, time: impl Into<Duration>) {
2721    if let Some(CursorImageStatus::Surface(surface)) = &state.current_cursor_icon {
2722        send_frames_surface_tree(surface, output, time, Some(Duration::ZERO), |_, _| Some(output.clone()));
2723    }
2724}
2725
2726/// Hotspot of a surface-backed cursor, in the surface's logical coordinates (zero when the
2727/// client set none). The sprite is placed at pointer - hotspot, like named and X11 cursors.
2728fn cursor_surface_hotspot(
2729    surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
2730) -> Point<i32, smithay::utils::Logical> {
2731    with_states(surface, |states| {
2732        states
2733            .data_map
2734            .get::<std::sync::Mutex<smithay::input::pointer::CursorImageAttributes>>()
2735            .and_then(|attrs| attrs.lock().ok().map(|guard| guard.hotspot))
2736            .unwrap_or_default()
2737    })
2738}
2739
2740fn render_node_tick(
2741    state: &mut AppState,
2742    node: &mut wayland::frontend::OutputNode,
2743) -> bool {
2744    let take_screenshot = state
2745        .pending_screenshot
2746        .as_ref()
2747        .is_some_and(|(id, _)| *id == node.id);
2748    let copy_frame_wanted = state.copy_frame_pending_for(&node.output);
2749    if node.capture.is_none() && !take_screenshot && !copy_frame_wanted {
2750        return false;
2751    }
2752
2753    // Per-display frame pacing under the one shared timer (which fires at the fastest
2754    // active capture's rate).
2755    if let Some(cap) = node.capture.as_ref()
2756        && !take_screenshot {
2757            let fps = cap.settings.target_fps.max(1.0);
2758            if let Some(last) = cap.last_tick
2759                && last.elapsed().as_secs_f64() < (1.0 / fps) * 0.9 {
2760                    return false;
2761                }
2762        }
2763
2764    let output = node.output.clone();
2765    let origin: Point<i32, smithay::utils::Logical> = node.pos.into();
2766    let output_scale_val = output.current_scale().fractional_scale();
2767    let (width, height) = match node.capture.as_ref() {
2768        Some(c) => (c.settings.width, c.settings.height),
2769        None => output
2770            .current_mode()
2771            .map(|m| (m.size.w, m.size.h))
2772            .unwrap_or((0, 0)),
2773    };
2774    if width <= 0 || height <= 0 {
2775        return false;
2776    }
2777    if node.frame_buffer.len() < (width as usize) * (height as usize) * 4 {
2778        node.frame_buffer = vec![0u8; (width as usize) * (height as usize) * 4];
2779    }
2780    let logical_w = (width as f64 / output_scale_val).round();
2781    let logical_h = (height as f64 / output_scale_val).round();
2782
2783    // A reconfigured or freshly created output composites its clear colour wherever a client
2784    // has not yet answered the new size; publishing is held until one covers the output so
2785    // that grey never reaches the stream. Compositing continues, so the frame callbacks the
2786    // waited-on clients redraw on keep flowing and the hold cannot deadlock on itself. Host
2787    // capture streams the host compositor's own frames and has no such gap.
2788    let hold_frame = match node.content_hold_until {
2789        Some(deadline)
2790            if state.host.is_none()
2791                && Instant::now() < deadline
2792                && !wayland::frontend::output_content_covers(
2793                    &state.space,
2794                    node.id,
2795                    logical_w,
2796                    logical_h,
2797                ) =>
2798        {
2799            true
2800        }
2801        Some(_) => {
2802            node.content_hold_until = None;
2803            false
2804        }
2805        None => false,
2806    };
2807
2808    // A recorder connecting counts as an IDR request, kept armed across skipped ticks.
2809    if let Some(cap) = node.capture.as_mut()
2810        && cap
2811            .recording_sink
2812            .as_ref()
2813            .map(|s| s.should_force_idr())
2814            .unwrap_or(false)
2815        {
2816            cap.request_idr();
2817        }
2818    let requested_idr = node.capture.as_ref().map(|c| c.pending_force_idr).unwrap_or(false);
2819    // A client keyframe request lands on the hardware path's atomic (RequestIdr sets
2820    // it whenever an encode pool exists); host mode consults it — without consuming —
2821    // to decide whether a static screen must re-encode its retained frame.
2822    let hw_idr_pending = node
2823        .capture
2824        .as_ref()
2825        .map(|c| c.encode_controls.force_idr.load(Ordering::Relaxed))
2826        .unwrap_or(false);
2827    let want_idr_for_host = requested_idr || hw_idr_pending;
2828
2829    let mut pool_slot: Option<(usize, Vec<u8>)> = None;
2830    if !hold_frame
2831        && let Some(cap) = node.capture.as_ref()
2832        && let Some(ref pool) = cap.encode_pool {
2833            pool_slot = pool.try_begin();
2834            if pool_slot.is_none() {
2835                return true;
2836            }
2837        }
2838
2839    let loc_enum = node
2840        .capture
2841        .as_ref()
2842        .map(|c| c.settings.watermark_location_enum)
2843        .unwrap_or(state.settings.watermark_location_enum);
2844    node.overlay_state.update_position(width, height, loc_enum);
2845
2846    if let Some(cap) = node.capture.as_mut() {
2847        cap.last_tick = Some(Instant::now());
2848    }
2849
2850    // The cursor is composited only on the output the pointer is on, at that output's
2851    // scale; its position is output-local.
2852    let pointer_local: Option<Point<f64, smithay::utils::Logical>> = state
2853        .seat
2854        .get_pointer()
2855        .map(|p| p.current_location())
2856        .and_then(|pos| {
2857            let rect = Rectangle::<f64, smithay::utils::Logical>::new(
2858                origin.to_f64(),
2859                (logical_w, logical_h).into(),
2860            );
2861            if rect.contains(pos) {
2862                Some(pos - origin.to_f64())
2863            } else {
2864                None
2865            }
2866        });
2867
2868    let mut render_success = false;
2869    let mut render_sync = None;
2870    let mut damage_rects: Vec<Rectangle<i32, Physical>> = Vec::new();
2871    let needs_full = node.capture.as_ref().map(|c| c.needs_full_render).unwrap_or(!node.target_seeded);
2872
2873    // Host-capture mode: the host compositor already blitted this display's frame
2874    // into one of our buffers (screencopy); adopt it in place of compositing.
2875    let host_mode = state.host.as_ref().map(|h| h.has_output_for(node.id)).unwrap_or(false);
2876    if state.host.is_some() && !host_mode {
2877        // No host output backs this display (start_capture already warned):
2878        // produce nothing rather than the compositor's own empty content.
2879        if let Some((id, buf)) = pool_slot.take()
2880            && let Some(cap) = node.capture.as_ref()
2881            && let Some(ref pool) = cap.encode_pool {
2882                    pool.cancel(id, buf);
2883                }
2884        return false;
2885    }
2886    // Dmabuf handed to the GPU encoder in host mode (from the new or retained frame).
2887    let mut host_enc_dmabuf: Option<Dmabuf> = None;
2888    // Host software frames arrive BGRA, so anything reading this display's frame buffer
2889    // back has to know it is not the GLES readback's RGBA.
2890    let mut host_cpu_frame = false;
2891    if host_mode {
2892        const RETAINED_OK: u8 = 0;
2893        const RETAINED_NONE: u8 = 1;
2894        // The host produced the buffer type this display's consumer cannot take; each
2895        // direction has its own recovery below.
2896        const RETAINED_CPU_FRAME: u8 = 2;
2897        const RETAINED_GPU_FRAME: u8 = 3;
2898        let host_idx = node.id;
2899        let gpu_encoder = node
2900            .capture
2901            .as_ref()
2902            .map(|c| c.video_encoder.is_some())
2903            .unwrap_or(false);
2904        // The session steps out of `state` while frames are adopted so the
2905        // renderer can composite the watermark / serve screenshot readbacks.
2906        let host = state.host.take().unwrap();
2907        // Stale-geometry rejection: a frame captured before a mode change is
2908        // useless at the new size (the CPU path would blit old-pitch rows into
2909        // a new-size buffer, and HW encoders cannot take a mismatched dmabuf),
2910        // so it goes back to the pool instead of being retained or encoded.
2911        let expect = node
2912            .capture
2913            .as_ref()
2914            .map(|c| (c.settings.width, c.settings.height));
2915        let new_frame = match (host.try_take_frame(host_idx), expect) {
2916            (Some(f), Some((w, h))) if f.width != w || f.height != h => {
2917                host.release_frame(host_idx, f);
2918                None
2919            }
2920            (f, _) => f,
2921        };
2922        let have_new = new_frame.is_some();
2923        // Streaming mode wants a constant-rate stream (the client's decoder pipeline
2924        // is built for it), so re-encode the retained frame every tick like the
2925        // compositor path does. Outside streaming mode, stay damage-driven, waking
2926        // only for a pending IDR (a viewer opening its keyframe gate), a screenshot
2927        // request, or a bouncing watermark that must keep moving.
2928        let streaming = node
2929            .capture
2930            .as_ref()
2931            .map(|c| c.settings.video_streaming_mode)
2932            .unwrap_or(false);
2933        let wm_active = node.overlay_state.is_active();
2934        let wm_animated = wm_active && node.overlay_state.is_animated();
2935        if !have_new && !want_idr_for_host && !streaming && !take_screenshot && !wm_animated {
2936            if let Some((id, buf)) = pool_slot.take()
2937                && let Some(cap) = node.capture.as_ref()
2938                && let Some(ref pool) = cap.encode_pool {
2939                        pool.cancel(id, buf);
2940                    }
2941            state.host = Some(host);
2942            return false;
2943        }
2944        if let Some(f) = new_frame {
2945            host.retain_frame(host_idx, f);
2946        }
2947        // Consume the (new or prior) retained frame's content into the pool slot /
2948        // GPU dmabuf. The frame stays retained for the next IDR. The CPU path
2949        // blends the watermark in place; the GPU path composites it below. Damage
2950        // is the fresh blit's; a re-encode of retained content has none of its own.
2951        let mut wm_drawn = false;
2952        let outcome = host.with_retained(host_idx, |r| {
2953            let Some(f) = r else { return RETAINED_NONE };
2954            damage_rects = if have_new { f.damage.clone() } else { Vec::new() };
2955            if let Some(cpu) = f.cpu.as_ref() {
2956                if gpu_encoder {
2957                    return RETAINED_CPU_FRAME;
2958                }
2959                host_cpu_frame = true;
2960                if let Some((_, ref mut buf)) = pool_slot {
2961                    cpu.write_bgra(f.width, f.height, buf);
2962                    if wm_active {
2963                        node.overlay_state.blend_bgra(buf, (f.width as usize) * 4, f.width, f.height);
2964                        wm_drawn = true;
2965                    }
2966                }
2967                cpu.write_bgra(f.width, f.height, &mut node.frame_buffer);
2968                if wm_active {
2969                    node.overlay_state
2970                        .blend_bgra(&mut node.frame_buffer, (f.width as usize) * 4, f.width, f.height);
2971                }
2972            } else if let Some(dmabuf) = f.dmabuf.as_ref() {
2973                if !gpu_encoder {
2974                    return RETAINED_GPU_FRAME;
2975                }
2976                host_enc_dmabuf = Some(dmabuf.clone());
2977            }
2978            RETAINED_OK
2979        });
2980        // GPU path: composite the watermark and serve screenshot readbacks with
2981        // the renderer. A bouncing watermark re-composes retained content into
2982        // this display's offscreen target every tick (drawing in place would
2983        // trail); anchored watermarks are stamped once onto each fresh blit and
2984        // ride along with retained re-encodes.
2985        if let Some(src) = host_enc_dmabuf.clone() {
2986            if wm_active
2987                && let Some(renderer) = state.gles_renderer.as_mut() {
2988                    if wm_animated {
2989                        if let Some((_, target)) = node.offscreen_buffer.as_mut() {
2990                            match compose_host_watermark(
2991                                renderer,
2992                                &node.overlay_state,
2993                                &src,
2994                                target,
2995                                (width, height),
2996                            ) {
2997                                Ok(sync) => {
2998                                    render_sync = Some(sync);
2999                                    host_enc_dmabuf = Some(target.clone());
3000                                    wm_drawn = true;
3001                                }
3002                                Err(e) => warn_once_host_watermark(&e),
3003                            }
3004                        }
3005                    } else if have_new {
3006                        let mut target = src.clone();
3007                        match draw_host_watermark(
3008                            renderer,
3009                            &node.overlay_state,
3010                            &mut target,
3011                            (width, height),
3012                        ) {
3013                            Ok(sync) => {
3014                                render_sync = Some(sync);
3015                                wm_drawn = true;
3016                            }
3017                            Err(e) => warn_once_host_watermark(&e),
3018                        }
3019                    }
3020                }
3021            if take_screenshot
3022                && let Some(renderer) = state.gles_renderer.as_mut() {
3023                    let mut shot = host_enc_dmabuf.clone().unwrap_or(src);
3024                    match renderer.bind(&mut shot) {
3025                        Ok(fb) => {
3026                            let rect = Rectangle::new((0, 0).into(), (width, height).into());
3027                            match renderer.copy_framebuffer(&fb, rect, Fourcc::Abgr8888) {
3028                                Ok(mapping) => match renderer.map_texture(&mapping) {
3029                                    Ok(data) => {
3030                                        let n = data.len().min(node.frame_buffer.len());
3031                                        node.frame_buffer[..n].copy_from_slice(&data[..n]);
3032                                    }
3033                                    Err(e) => eprintln!("[HostCapture] screenshot map: {e:?}"),
3034                                },
3035                                Err(e) => eprintln!("[HostCapture] screenshot copy: {e:?}"),
3036                            }
3037                        }
3038                        Err(e) => eprintln!("[HostCapture] screenshot bind: {e:?}"),
3039                    };
3040                }
3041        }
3042        if wm_drawn
3043            && let Some(rect) = node.overlay_state.damage_rect(width, height) {
3044                damage_rects.push(rect);
3045            }
3046        state.host = Some(host);
3047        if outcome != RETAINED_OK {
3048            if let Some((id, buf)) = pool_slot.take()
3049                && let Some(cap) = node.capture.as_ref()
3050                && let Some(ref pool) = cap.encode_pool {
3051                        pool.cancel(id, buf);
3052                    }
3053            // A frame type this display's consumer cannot take is recovered from rather
3054            // than warned about: either side of the mismatch would otherwise stream
3055            // nothing for the life of the capture.
3056            if outcome == RETAINED_GPU_FRAME {
3057                // The host is blitting into dmabufs a CPU encoder cannot read; ask it
3058                // for shm frames. Frames resume once the capture thread reallocates,
3059                // and the request is idempotent, so repeating it per tick costs nothing.
3060                if let Some(h) = state.host.as_ref() {
3061                    h.set_buffer_type(node.id, false);
3062                }
3063            } else if outcome == RETAINED_CPU_FRAME
3064                && let Some(cap) = node.capture.as_mut() {
3065                    // The host hands out software frames only (no zwp_linux_dmabuf v3),
3066                    // so the zero-copy session has nothing to import: demote it to the
3067                    // readback path, which encodes those frames as they arrive.
3068                    eprintln!(
3069                        "[HostCapture] host delivers software frames; demoting the zero-copy encoder to readback encode."
3070                    );
3071                    cap.video_encoder = None;
3072                    let s = &cap.settings;
3073                    let try_gpu = s.output_mode == 1
3074                        && !(s.use_cpu || s.encode_node_index == -1);
3075                    bootstrap_readback_pool(cap, node.id, false, try_gpu, None, None);
3076                    cap.request_idr();
3077                    // The consumer is now a CPU one, so the host stops preferring GPU slots.
3078                    if let Some(h) = state.host.as_ref() {
3079                        h.set_buffer_type(node.id, false);
3080                    }
3081                }
3082            return false;
3083        }
3084        render_success = true;
3085    }
3086
3087    if !host_mode && state.use_gpu {
3088        if let Some(renderer) = state.gles_renderer.as_mut() {
3089            let mut cap = node.capture.as_mut();
3090            if let Some((_bo, dmabuf)) = node.offscreen_buffer.as_mut() {
3091                let render_age = if node.overlay_state.is_animated() || needs_full { 0 } else { 1 };
3092                match renderer.bind(dmabuf) {
3093                    Ok(mut frame) => {
3094                        let mut elements: Vec<CompositionElements<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>> = Vec::new();
3095
3096                        if state.render_cursor_on_framebuffer
3097                            && let Some(pos) = pointer_local {
3098                                let scale = Scale::from(output_scale_val);
3099
3100                                if let Some(CursorImageStatus::Named(icon)) = &state.current_cursor_icon {
3101                                    let name = wayland::frontend::cursor_icon_to_str(icon);
3102                                    let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3103                                    if let Some(image) = state.cursor_helper.get_image_by_name(name, output_scale_val.round() as u32, time)
3104                                        && let Some(elem) = node.overlay_state.get_cursor_element(renderer, image, pos, output_scale_val) {
3105                                            elements.push(CompositionElements::Cursor(elem));
3106                                        }
3107                                } else if let Some(CursorImageStatus::Surface(surface)) = &state.current_cursor_icon {
3108                                     let hot = cursor_surface_hotspot(surface).to_f64();
3109                                     let phys_pos = (pos - hot).to_physical(scale);
3110                                     let elem_result = with_states(surface, |states| {
3111                                         WaylandSurfaceRenderElement::from_surface(renderer, surface, states, phys_pos, 1.0, smithay::backend::renderer::element::Kind::Cursor)
3112                                     });
3113                                     if let Ok(Some(cursor_elem)) = elem_result {
3114                                         elements.push(CompositionElements::Surface(cursor_elem));
3115                                     }
3116                                } else if state.current_cursor_icon.is_none() {
3117                                    let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3118                                    let image = state.cursor_helper.get_image(output_scale_val.round() as u32, time);
3119                                    if let Some(elem) = node.overlay_state.get_cursor_element(renderer, image, pos, output_scale_val) {
3120                                        elements.push(CompositionElements::Cursor(elem));
3121                                    }
3122                                }
3123                            }
3124
3125                        if let Some(elem) = node.overlay_state.get_watermark_element(renderer) {
3126                            elements.push(CompositionElements::Cursor(elem));
3127                        }
3128
3129                        {
3130                            let layer_map = layer_map_for_output(&output);
3131
3132                            let draw_layer = |renderer: &mut GlesRenderer, elements: &mut Vec<CompositionElements<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>>, target_layer: smithay::wayland::shell::wlr_layer::Layer| {
3133                                for surface in layer_map.layers().rev() {
3134                                    let current_layer = surface.layer();
3135                                    if current_layer == target_layer
3136                                        && let Some(geo) = layer_map.layer_geometry(surface) {
3137                                            let elem = smithay::wayland::compositor::with_states(surface.wl_surface(), |states| {
3138                                                WaylandSurfaceRenderElement::from_surface(
3139                                                    renderer, surface.wl_surface(), states,
3140                                                    geo.loc.to_physical_precise_round(output_scale_val), 1.0,
3141                                                    smithay::backend::renderer::element::Kind::Unspecified
3142                                                )
3143                                            });
3144                                            if let Ok(Some(e)) = elem {
3145                                                elements.push(CompositionElements::Surface(e));
3146                                            }
3147                                        }
3148                                }
3149                            };
3150
3151                            draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Overlay);
3152                            draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Top);
3153                        }
3154
3155                        for window in state.space.elements_for_output(&output).collect::<Vec<_>>().into_iter().rev() {
3156                            let window_loc = state.space.element_location(window).unwrap_or_default() - origin;
3157
3158                            if let Some(surface) = window.wl_surface() {
3159                                let popups = PopupManager::popups_for_surface(&surface);
3160                                for (popup, location) in popups {
3161                                    let popup_surface = popup.wl_surface();
3162                                    let popup_pos = window_loc + location;
3163                                    let elem = smithay::wayland::compositor::with_states(popup_surface, |states| {
3164                                        WaylandSurfaceRenderElement::from_surface(
3165                                            renderer,
3166                                            popup_surface,
3167                                            states,
3168                                            popup_pos.to_physical_precise_round(output_scale_val),
3169                                            1.0,
3170                                            smithay::backend::renderer::element::Kind::Unspecified
3171                                        )
3172                                    });
3173                                    if let Ok(Some(e)) = elem {
3174                                        elements.push(CompositionElements::Surface(e));
3175                                    }
3176                                }
3177                            }
3178
3179                            elements.extend(window.render_elements(renderer, window_loc.to_physical_precise_round(output_scale_val), Scale::from(output_scale_val), 1.0).into_iter().map(CompositionElements::Space));
3180                        }
3181
3182                        {
3183                            let layer_map = layer_map_for_output(&output);
3184
3185                            let draw_layer = |renderer: &mut GlesRenderer, elements: &mut Vec<CompositionElements<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>>, target_layer: smithay::wayland::shell::wlr_layer::Layer| {
3186                                for surface in layer_map.layers().rev() {
3187                                    let current_layer = surface.layer();
3188                                    if current_layer == target_layer
3189                                        && let Some(geo) = layer_map.layer_geometry(surface) {
3190                                            let elem = smithay::wayland::compositor::with_states(surface.wl_surface(), |states| {
3191                                                WaylandSurfaceRenderElement::from_surface(
3192                                                    renderer, surface.wl_surface(), states,
3193                                                    geo.loc.to_physical_precise_round(output_scale_val), 1.0,
3194                                                    smithay::backend::renderer::element::Kind::Unspecified
3195                                                )
3196                                            });
3197                                            if let Ok(Some(e)) = elem {
3198                                                elements.push(CompositionElements::Surface(e));
3199                                            }
3200                                        }
3201                                }
3202                            };
3203
3204                            draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Bottom);
3205                            draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Background);
3206                        }
3207                        match node.damage_tracker.render_output(renderer, &mut frame, render_age, &elements, [0.1, 0.1, 0.1, 1.0]) {
3208                            Ok(result) => {
3209                                render_success = true;
3210                                if let Some(damage) = result.damage {
3211                                    damage_rects = damage.clone();
3212                                }
3213                                render_sync = Some(result.sync);
3214                                if let Some(c) = cap.as_deref_mut() {
3215                                    c.needs_full_render = false;
3216                                }
3217                            },
3218                            Err(e) => eprintln!("Render error: {:?}", e)
3219                        }
3220                        if let Some(c) = cap {
3221                            if !damage_rects.is_empty() {
3222                                c.content_gen += 1;
3223                            }
3224                            if let Some((id, ref mut buf)) = pool_slot {
3225                                // No-damage ticks skip the readback, so a pooled buffer can
3226                                // lag the offscreen target whenever the encoder held the
3227                                // other slot across a tick; one catch-up readback keeps
3228                                // every published buffer current.
3229                                if render_success && c.pool_content_gen[id] != c.content_gen {
3230                                    let _ = renderer.with_context(|gl| unsafe {
3231                                        gl.ReadPixels(
3232                                            0,
3233                                            0,
3234                                            width,
3235                                            height,
3236                                            smithay::backend::renderer::gles::ffi::RGBA,
3237                                            smithay::backend::renderer::gles::ffi::UNSIGNED_BYTE,
3238                                            buf.as_mut_ptr() as *mut std::ffi::c_void,
3239                                        );
3240                                    });
3241                                    c.pool_content_gen[id] = c.content_gen;
3242                                }
3243                            }
3244                        }
3245                        if pool_slot.is_none() && take_screenshot {
3246                            let _ = renderer.with_context(|gl| unsafe {
3247                                gl.ReadPixels(
3248                                    0,
3249                                    0,
3250                                    width,
3251                                    height,
3252                                    smithay::backend::renderer::gles::ffi::RGBA,
3253                                    smithay::backend::renderer::gles::ffi::UNSIGNED_BYTE,
3254                                    node.frame_buffer.as_mut_ptr() as *mut std::ffi::c_void,
3255                                );
3256                            });
3257                        }
3258                    },
3259                    Err(e) => eprintln!("Failed to bind buffer: {:?}", e)
3260                }
3261            }
3262        }
3263    } else if !host_mode
3264        && let Some(renderer) = state.pixman_renderer.as_mut() {
3265            let mut cap = node.capture.as_mut();
3266            let (ptr, buf_age) = match pool_slot {
3267                Some((id, ref mut buf)) => {
3268                    let age = cap
3269                        .as_ref()
3270                        .map(|c| {
3271                            if c.pool_last_render[id] == 0 {
3272                                0
3273                            } else {
3274                                (c.render_seq + 1 - c.pool_last_render[id]) as usize
3275                            }
3276                        })
3277                        .unwrap_or(0);
3278                    (buf.as_mut_ptr() as *mut u32, age)
3279                }
3280                None => (node.frame_buffer.as_mut_ptr() as *mut u32, 0),
3281            };
3282            let mut image = unsafe {
3283                pixman::Image::from_raw_mut(pixman::FormatCode::A8R8G8B8, width as usize, height as usize, ptr, (width as usize) * 4, false).expect("Failed to create pixman image")
3284            };
3285                        match renderer.bind(&mut image) {
3286                        Ok(mut frame) => {
3287                            let mut elements: Vec<CompositionElements<PixmanRenderer, WaylandSurfaceRenderElement<PixmanRenderer>>> = Vec::new();
3288
3289                            if state.render_cursor_on_framebuffer
3290                                && let Some(pos) = pointer_local {
3291                                    let scale = Scale::from(output_scale_val);
3292
3293                                    if let Some(CursorImageStatus::Named(icon)) = &state.current_cursor_icon {
3294                                        let name = wayland::frontend::cursor_icon_to_str(icon);
3295                                        let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3296                                        if let Some(image) = state.cursor_helper.get_image_by_name(name, output_scale_val.round() as u32, time)
3297                                            && let Some(elem) = node.overlay_state.get_cursor_element(renderer, image, pos, output_scale_val) {
3298                                                elements.push(CompositionElements::Cursor(elem));
3299                                            }
3300                                    } else if let Some(CursorImageStatus::Surface(surface)) = &state.current_cursor_icon {
3301                                         let hot = cursor_surface_hotspot(surface).to_f64();
3302                                         let phys_pos = (pos - hot).to_physical(scale);
3303                                         let elem_result = with_states(surface, |states| {
3304                                             WaylandSurfaceRenderElement::from_surface(renderer, surface, states, phys_pos, 1.0, smithay::backend::renderer::element::Kind::Cursor)
3305                                         });
3306                                         if let Ok(Some(cursor_elem)) = elem_result {
3307                                             elements.push(CompositionElements::Surface(cursor_elem));
3308                                         }
3309                                    } else if state.current_cursor_icon.is_none() {
3310                                        let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3311                                        let image = state.cursor_helper.get_image(output_scale_val.round() as u32, time);
3312                                        if let Some(elem) = node.overlay_state.get_cursor_element(renderer, image, pos, output_scale_val) {
3313                                            elements.push(CompositionElements::Cursor(elem));
3314                                        }
3315                                    }
3316                                }
3317
3318                            if let Some(elem) = node.overlay_state.get_watermark_element(renderer) {
3319                                elements.push(CompositionElements::Cursor(elem));
3320                            }
3321
3322                            {
3323                                let layer_map = layer_map_for_output(&output);
3324
3325                                let draw_layer = |renderer: &mut PixmanRenderer, elements: &mut Vec<CompositionElements<PixmanRenderer, WaylandSurfaceRenderElement<PixmanRenderer>>>, target_layer: smithay::wayland::shell::wlr_layer::Layer| {
3326                                    for surface in layer_map.layers().rev() {
3327                                        let current_layer = surface.layer();
3328                                        if current_layer == target_layer
3329                                            && let Some(geo) = layer_map.layer_geometry(surface) {
3330                                                let elem = smithay::wayland::compositor::with_states(surface.wl_surface(), |states| {
3331                                                    WaylandSurfaceRenderElement::from_surface(
3332                                                        renderer, surface.wl_surface(), states,
3333                                                        geo.loc.to_physical_precise_round(output_scale_val), 1.0,
3334                                                        smithay::backend::renderer::element::Kind::Unspecified
3335                                                    )
3336                                                });
3337                                                if let Ok(Some(e)) = elem {
3338                                                    elements.push(CompositionElements::Surface(e));
3339                                                }
3340                                            }
3341                                    }
3342                                };
3343
3344                                draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Overlay);
3345                                draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Top);
3346                            }
3347
3348                            for window in state.space.elements_for_output(&output).collect::<Vec<_>>().into_iter().rev() {
3349                                let loc = state.space.element_location(window).unwrap_or_default() - origin;
3350
3351                                if let Some(surface) = window.wl_surface() {
3352                                    let popups = PopupManager::popups_for_surface(&surface);
3353                                    for (popup, location) in popups {
3354                                        let popup_surface = popup.wl_surface(); {
3355                                            let popup_pos = loc + location;
3356                                            let elem = smithay::wayland::compositor::with_states(popup_surface, |states| {
3357                                                WaylandSurfaceRenderElement::from_surface(
3358                                                    renderer,
3359                                                    popup_surface,
3360                                                    states,
3361                                                    popup_pos.to_physical_precise_round(output_scale_val),
3362                                                    1.0,
3363                                                    smithay::backend::renderer::element::Kind::Unspecified
3364                                                )
3365                                            });
3366                                            if let Ok(Some(e)) = elem {
3367                                                elements.push(CompositionElements::Surface(e));
3368                                            }
3369                                        }
3370                                    }
3371                                }
3372
3373                                elements.extend(window.render_elements(renderer, loc.to_physical_precise_round(output_scale_val), Scale::from(output_scale_val), 1.0).into_iter().map(CompositionElements::Space));
3374                            }
3375
3376                            {
3377                                let layer_map = layer_map_for_output(&output);
3378
3379                                let draw_layer = |renderer: &mut PixmanRenderer, elements: &mut Vec<CompositionElements<PixmanRenderer, WaylandSurfaceRenderElement<PixmanRenderer>>>, target_layer: smithay::wayland::shell::wlr_layer::Layer| {
3380                                    for surface in layer_map.layers().rev() {
3381                                        let current_layer = surface.layer();
3382                                        if current_layer == target_layer
3383                                            && let Some(geo) = layer_map.layer_geometry(surface) {
3384                                                let elem = smithay::wayland::compositor::with_states(surface.wl_surface(), |states| {
3385                                                    WaylandSurfaceRenderElement::from_surface(
3386                                                        renderer, surface.wl_surface(), states,
3387                                                        geo.loc.to_physical_precise_round(output_scale_val), 1.0,
3388                                                        smithay::backend::renderer::element::Kind::Unspecified
3389                                                    )
3390                                                });
3391                                                if let Ok(Some(e)) = elem {
3392                                                    elements.push(CompositionElements::Surface(e));
3393                                                }
3394                                            }
3395                                    }
3396                                };
3397
3398                                draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Bottom);
3399                                draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Background);
3400                            }
3401
3402                    let render_age = if node.overlay_state.is_animated() || needs_full { 0 } else { buf_age };
3403                    match node.damage_tracker.render_output(renderer, &mut frame, render_age, &elements, [0.1, 0.1, 0.1, 1.0]) {
3404                        Ok(result) => {
3405                            render_success = true;
3406                            if let Some(c) = cap.as_deref_mut() {
3407                                c.needs_full_render = false;
3408                            }
3409                            if let Some(damage) = result.damage { damage_rects = damage.clone(); }
3410                        },
3411                        Err(e) => eprintln!("Render error: {:?}", e)
3412                    }
3413                    if let Some(c) = cap {
3414                        c.render_seq += 1;
3415                        if render_success
3416                            && let Some((id, _)) = pool_slot {
3417                                c.pool_last_render[id] = c.render_seq;
3418                            }
3419                    }
3420                },
3421                Err(e) => eprintln!("Failed to bind pixman image: {:?}", e)
3422            }
3423        }
3424
3425    if render_success {
3426        node.target_seeded = true;
3427        let time = state.clock.now();
3428        // The composited frame is what the capture consumes, so this render is also the
3429        // presentation moment for wp_presentation feedback.
3430        let mut feedback = OutputPresentationFeedback::new(&output);
3431        for window in state.space.elements_for_output(&output).cloned().collect::<Vec<_>>() {
3432            window.send_frame(&output, time, Some(Duration::ZERO), |_, _| Some(output.clone()));
3433            window.take_presentation_feedback(
3434                &mut feedback,
3435                |_, _| Some(output.clone()),
3436                |_, _| wp_presentation_feedback::Kind::empty(),
3437            );
3438        }
3439        // Panels, backgrounds and other layer-shell surfaces are composited from the
3440        // layer map rather than the space, so they need the callback separately: one
3441        // that never arrives leaves a client which draws on frame callbacks showing
3442        // whatever it painted first, for as long as the session lasts.
3443        for layer in layer_map_for_output(&output).layers() {
3444            layer.send_frame(&output, time, Some(Duration::ZERO), |_, _| Some(output.clone()));
3445            layer.take_presentation_feedback(
3446                &mut feedback,
3447                |_, _| Some(output.clone()),
3448                |_, _| wp_presentation_feedback::Kind::empty(),
3449            );
3450        }
3451        send_cursor_frame(state, &output, time);
3452        let refresh = match node.capture.as_ref() {
3453            Some(c) => Refresh::Fixed(Duration::from_secs_f64(1.0 / c.settings.target_fps.max(1.0))),
3454            None => Refresh::Unknown,
3455        };
3456        node.frame_seq += 1;
3457        feedback.presented(time, refresh, node.frame_seq, wp_presentation_feedback::Kind::Vsync);
3458
3459        // Host mode renders nothing locally, so there is no composited buffer to
3460        // serve capture clients from; their frames stay parked.
3461        if !host_mode {
3462            service_copy_frames(state, node, width, height, &damage_rects);
3463        }
3464
3465        if !hold_frame && let Some(cap) = node.capture.as_mut() {
3466            // A dead encode thread (panic, unexpected exit) cannot drain the pool;
3467            // every publish would park the slot and each tick would silently skip
3468            // while is_capturing still reports true. Rebuild the readback path in
3469            // place, reusing a cleanly handed-back encoder session if any.
3470            if cap.encode_join.as_ref().is_some_and(|j| j.is_finished()) {
3471                let prior = cap
3472                    .encode_join
3473                    .take()
3474                    .and_then(|j| j.join().ok().flatten());
3475                if let Some(pool) = cap.encode_pool.take() {
3476                    pool.shutdown();
3477                }
3478                let s = &cap.settings;
3479                let try_gpu = s.output_mode == 1
3480                    && !(s.use_cpu || s.encode_node_index == -1);
3481                eprintln!("[Wayland] encode thread died; rebuilding the readback path.");
3482                // Host frames reach the pool as BGRA; only our own GLES readback produces RGBA.
3483                bootstrap_readback_pool(cap, node.id, state.use_gpu && !host_mode, try_gpu, prior, None);
3484                cap.request_idr();
3485            }
3486            if cap.encode_pool.is_some() {
3487                if take_screenshot
3488                    && let Some((_, ref buf)) = pool_slot {
3489                        let n = buf.len().min(node.frame_buffer.len());
3490                        node.frame_buffer[..n].copy_from_slice(&buf[..n]);
3491                    }
3492                if let Some((id, buf)) = pool_slot.take() {
3493                    let is_animated = node.overlay_state.is_animated();
3494                    cap.encode_pool.as_ref().unwrap().publish(WlFrame {
3495                        id,
3496                        buf,
3497                        frame_id: cap.frame_counter,
3498                        damage: std::mem::take(&mut damage_rects),
3499                        is_animated,
3500                    });
3501                    cap.frame_counter = cap.frame_counter.wrapping_add(1);
3502                }
3503            } else if let Some(ref mut encoder) = cap.video_encoder {
3504                // Deliver the parked frame (if any) first, WITHOUT blocking: this runs
3505                // on the calloop thread, and a blocking send would freeze
3506                // input/command/Wayland dispatch for as long as the Python consumer
3507                // stalls. While a frame stays parked, no new frame is encoded — an
3508                // encoded frame joins the H.264 reference chain and can never be
3509                // dropped — and the tick's damage is latched so the pause never loses
3510                // a change.
3511                let slot_free = match cap.pending_hw_delivery.take() {
3512                    None => true,
3513                    Some(pending) => match cap.deliver_tx.as_ref() {
3514                        None => true,
3515                        Some(tx) => match tx.try_send(pending) {
3516                            Ok(()) => true,
3517                            Err(std::sync::mpsc::TrySendError::Full(p)) => {
3518                                cap.pending_hw_delivery = Some(p);
3519                                false
3520                            }
3521                            Err(std::sync::mpsc::TrySendError::Disconnected(_)) => true,
3522                        },
3523                    },
3524                };
3525                if !slot_free {
3526                    if !damage_rects.is_empty() {
3527                        cap.pending_hw_damage = true;
3528                    }
3529                } else {
3530                let is_animated = node.overlay_state.is_animated();
3531                let had_damage = !damage_rects.is_empty()
3532                    || std::mem::take(&mut cap.pending_hw_damage);
3533                let decision = crate::pipeline::decide_hw_fullframe(
3534                    &mut cap.vaapi_state,
3535                    &cap.settings,
3536                    cap.frame_counter,
3537                    had_damage,
3538                    is_animated,
3539                    requested_idr,
3540                );
3541                let send_frame = decision.send;
3542                let force_idr = decision.force_idr;
3543                let target_qp = decision.target_qp;
3544
3545                let mut frame_out = false;
3546                if send_frame {
3547                    if let Some(sync) = render_sync.take() {
3548                        let _ = sync.wait();
3549                    }
3550                    // Host-capture frames encode from the buffer the host blitted
3551                    // into; otherwise from this display's own composited buffer.
3552                    let enc_dmabuf: Option<Dmabuf> = host_enc_dmabuf
3553                        .clone()
3554                        .or_else(|| node.offscreen_buffer.as_ref().map(|(_, d)| d.clone()));
3555                    let result = match encoder {
3556                        GpuEncoder::Nvenc(enc) => {
3557                            if let Some(ref dmabuf) = enc_dmabuf {
3558                                enc.encode(dmabuf, cap.frame_counter as u64, target_qp, force_idr)
3559                            } else {
3560                                Err("NVENC ZeroCopy requires offscreen buffer (GPU context)".to_string())
3561                            }
3562                        },
3563                        GpuEncoder::Vaapi(enc) => {
3564                            if let Some(ref dmabuf) = enc_dmabuf {
3565                                enc.encode_dmabuf(dmabuf, cap.frame_counter as u64, target_qp, force_idr)
3566                            } else {
3567                                Err("Vaapi ZeroCopy requires offscreen buffer (GPU context)".to_string())
3568                            }
3569                        }
3570                    };
3571
3572                    if let Ok(data) = result {
3573                        cap.hw_error_streak = 0;
3574                        cap.hw_rebuilt = false;
3575                        if !data.is_empty() {
3576                            frame_out = true;
3577                            cap.encode_stats.frames.fetch_add(1, Ordering::Relaxed);
3578                            cap.encode_stats.stripes.fetch_add(1, Ordering::Relaxed);
3579                            if let Some(ref tx) = cap.deliver_tx {
3580                                let stripes = vec![EncodedStripe {
3581                                    data: Arc::new(data), data_type: 2, stripe_y_start: 0,
3582                                    stripe_height: height, frame_id: cap.frame_counter as i32,
3583                                }];
3584                                if let Some(ref socket) = cap.recording_sink {
3585                                    socket.write_frame(&stripes, height);
3586                                }
3587                                crate::recorder::wayland_tap(node.id, &stripes);
3588                                // Non-blocking: a full slot parks the frame (delivered
3589                                // ahead of any new encode above).
3590                                match tx.try_send(stripes) {
3591                                    Ok(()) => {}
3592                                    Err(std::sync::mpsc::TrySendError::Full(s)) => {
3593                                        cap.pending_hw_delivery = Some(s);
3594                                    }
3595                                    Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {}
3596                                }
3597                            }
3598                        }
3599                    } else if let Err(e) = result {
3600                        eprintln!("HW Encode Error: {}", e);
3601                        cap.hw_error_streak = cap.hw_error_streak.saturating_add(1);
3602                        if cap.hw_error_streak == HW_ERROR_RECOVERY_THRESHOLD {
3603                            // The zero-copy session persistently fails after having
3604                            // worked (driver hiccup, CUDA pressure from a co-tenant):
3605                            // rebuild the session once, else demote to the readback
3606                            // path. Streaming black frames forever is not an option.
3607                            // A session whose encodes keep failing still constructs, so
3608                            // the rebuild only counts as recovery until the next streak;
3609                            // otherwise the stream would rebuild in a loop and never demote.
3610                            let rebuilt = if cap.hw_rebuilt {
3611                                None
3612                            } else {
3613                                // The broken session is released before its replacement is
3614                                // opened: the failure it recovers from is usually device
3615                                // memory pressure, and holding both at once is what would
3616                                // make the rebuild fail too.
3617                                drop(cap.video_encoder.take());
3618                                rebuild_zerocopy_encoder(cap, state)
3619                            };
3620                            match rebuilt {
3621                                Some(enc) => {
3622                                    cap.video_encoder = Some(enc);
3623                                    cap.pending_force_idr = true;
3624                                    cap.hw_rebuilt = true;
3625                                    eprintln!("[Wayland] zero-copy HW encoder rebuilt after repeated encode errors.");
3626                                }
3627                                None => {
3628                                    eprintln!("[Wayland] zero-copy HW encoder unrecoverable; demoting to readback encode.");
3629                                    cap.video_encoder = None;
3630                                    cap.hw_rebuilt = false;
3631                                    // Mirror the startup intent: readback still
3632                                    // tries the GPU unless the operator opted out.
3633                                    let s = &cap.settings;
3634                                    let try_gpu = s.output_mode == 1
3635                                        && !(s.use_cpu || s.encode_node_index == -1);
3636                                    // Host frames reach the pool as BGRA; only our own
3637                                    // GLES readback produces RGBA.
3638                                    bootstrap_readback_pool(
3639                                        cap, node.id, state.use_gpu && !host_mode, try_gpu, None,
3640                                        None,
3641                                    );
3642                                    // The host has to switch to buffers the readback path
3643                                    // can read back on the CPU.
3644                                    if host_mode
3645                                        && let Some(h) = state.host.as_ref() {
3646                                            h.set_buffer_type(node.id, false);
3647                                        }
3648                                }
3649                            }
3650                            cap.hw_error_streak = 0;
3651                        }
3652                    }
3653                }
3654                // An unserved request stays armed: on an infinite GOP an IDR lost to an
3655                // encode error would never self-heal.
3656                cap.pending_force_idr = requested_idr && !frame_out;
3657                cap.frame_counter = cap.frame_counter.wrapping_add(1);
3658                }
3659            }
3660        }
3661        if take_screenshot
3662            && let Some((_, resp)) = state.pending_screenshot.take() {
3663                if !node.frame_buffer.is_empty() {
3664                    let w = width as u32;
3665                    let h = height as u32;
3666                    // A host software frame was written BGRA into the frame buffer, so it
3667                    // needs the swap even when the local renderer is GLES.
3668                    let png = if state.use_gpu && !host_cpu_frame {
3669                        crate::computer_use::encode_png_rgba(&node.frame_buffer, w, h)
3670                    } else {
3671                        let mut rgba = node.frame_buffer.clone();
3672                        for px in rgba.chunks_exact_mut(4) {
3673                            px.swap(0, 2);
3674                        }
3675                        crate::computer_use::encode_png_rgba(&rgba, w, h)
3676                    };
3677                    match png {
3678                        Ok(data) => { let _ = resp.send(Ok(data)); }
3679                        Err(e) => {
3680                            let _ = resp.send(Err(format!("PNG encode error: {e}")));
3681                            eprintln!("[ComputerUse] PNG encode error: {}", e);
3682                        }
3683                    }
3684                } else {
3685                    let _ = resp.send(Err("Screenshot render produced no pixels".to_string()));
3686                }
3687            }
3688    }
3689    if let Some((id, buf)) = pool_slot.take()
3690        && let Some(cap) = node.capture.as_ref()
3691        && let Some(ref pool) = cap.encode_pool {
3692                pool.cancel(id, buf);
3693            }
3694    false
3695}
3696
3697/// True when the rectangles `(x, y, w, h)` overlap: strict interior intersection, so
3698/// touching edges do not count and empty (non-positive-dimension) rectangles never
3699/// overlap anything. Arithmetic is widened so extreme coordinates cannot wrap.
3700fn rects_overlap(a: (i32, i32, i32, i32), b: (i32, i32, i32, i32)) -> bool {
3701    let (ax, ay, aw, ah) = (a.0 as i64, a.1 as i64, a.2 as i64, a.3 as i64);
3702    let (bx, by, bw, bh) = (b.0 as i64, b.1 as i64, b.2 as i64, b.3 as i64);
3703    aw > 0 && ah > 0 && bw > 0 && bh > 0
3704        && ax < bx + bw && bx < ax + aw
3705        && ay < by + bh && by < ay + ah
3706}
3707
3708/// An overlapping output as `(id, flavor, rect)`, where flavor names which rectangle
3709/// pair matched and rect is `(x, y, width, height)`.
3710type OutputOverlap = (u32, &'static str, (i32, i32, i32, i32));
3711
3712/// The first live output (excluding `skip_id`) whose rectangle overlaps a candidate
3713/// placement. Both rectangle flavors are checked — logical
3714/// (Space layout, scale-divided) and physical (mode pixels at the same origin) — because
3715/// input injection and cursor compositing key off the physical rects while window layout
3716/// keys off the logical ones, and neither may overlap.
3717fn find_output_overlap(
3718    nodes: &[wayland::frontend::OutputNode],
3719    skip_id: Option<u32>,
3720    logical: (i32, i32, i32, i32),
3721    physical: (i32, i32, i32, i32),
3722) -> Option<OutputOverlap> {
3723    for n in nodes {
3724        if Some(n.id) == skip_id {
3725            continue;
3726        }
3727        if let Some(geo) = n.logical_geometry() {
3728            let other = (geo.loc.x, geo.loc.y, geo.size.w, geo.size.h);
3729            if rects_overlap(logical, other) {
3730                return Some((n.id, "logical", other));
3731            }
3732        }
3733        if let Some(mode) = n.output.current_mode() {
3734            let other = (n.pos.0, n.pos.1, mode.size.w, mode.size.h);
3735            if rects_overlap(physical, other) {
3736                return Some((n.id, "physical", other));
3737            }
3738        }
3739    }
3740    None
3741}
3742
3743/// Create an additional output mapped into the layout at `(x, y)`. Fails (false) on a
3744/// duplicate id, non-positive geometry/scale, a rectangle overlapping a live output, or a
3745/// GPU render-target allocation failure. Only Create/Reposition placements are validated:
3746/// a capture reconfigure (StartCapture on an existing output) resizes UNVALIDATED, so
3747/// keeping a multi-step relayout overlap-free at every step is the caller's ordering
3748/// responsibility.
3749fn create_output_on(
3750    state: &mut AppState,
3751    id: u32,
3752    width: i32,
3753    height: i32,
3754    x: i32,
3755    y: i32,
3756    scale: f64,
3757) -> bool {
3758    if state.node_idx_for_id(id).is_some() || width <= 0 || height <= 0 || scale <= 0.0 {
3759        return false;
3760    }
3761    // Host-capture mode: displays map onto host outputs by rank, so an output beyond
3762    // the host's count would exist but never receive a frame. Refuse it instead.
3763    if let Some(host) = state.host.as_ref() {
3764        let capacity = host.output_count();
3765        if state.output_nodes.len() >= capacity {
3766            eprintln!(
3767                "[Wayland] CreateOutput {id}: rejected, the host compositor has {capacity} output(s) and all are backing displays."
3768            );
3769            return false;
3770        }
3771    }
3772    let logical_size = (
3773        (width as f64 / scale).round() as i32,
3774        (height as f64 / scale).round() as i32,
3775    );
3776    if let Some((oid, flavor, other)) = find_output_overlap(
3777        &state.output_nodes,
3778        None,
3779        (x, y, logical_size.0, logical_size.1),
3780        (x, y, width, height),
3781    ) {
3782        eprintln!(
3783            "[Wayland] CreateOutput {id}: rejected, {flavor} rect {}x{}+{x}+{y} overlaps output {oid} at {}x{}+{}+{}.",
3784            if flavor == "logical" { logical_size.0 } else { width },
3785            if flavor == "logical" { logical_size.1 } else { height },
3786            other.2, other.3, other.0, other.1,
3787        );
3788        return false;
3789    }
3790    let output = Output::new(
3791        format!("HEADLESS-{}", id + 1),
3792        PhysicalProperties {
3793            size: (width, height).into(),
3794            subpixel: Subpixel::Unknown,
3795            make: "Pixelflux".into(),
3796            model: "Virtual".into(),
3797            serial_number: format!("{:03}", id + 1),
3798        },
3799    );
3800    let mode = OutputMode { size: (width, height).into(), refresh: 60_000 };
3801    output.change_current_state(
3802        Some(mode),
3803        Some(Transform::Normal),
3804        Some(OutputScale::Fractional(scale)),
3805        Some((x, y).into()),
3806    );
3807    output.set_preferred(mode);
3808    let mut offscreen = None;
3809    if state.use_gpu {
3810        let Some(gbm) = state.gbm_device.as_mut() else { return false };
3811        match gbm.create_buffer_object(
3812            width as u32,
3813            height as u32,
3814            GbmFormat::Argb8888,
3815            BufferObjectFlags::RENDERING,
3816        ) {
3817            Ok(bo) => {
3818                let dmabuf = create_dmabuf_from_bo(&bo);
3819                offscreen = Some((bo, dmabuf));
3820            }
3821            Err(e) => {
3822                eprintln!("[Wayland] CreateOutput {id}: GBM allocation {width}x{height} failed ({e:?}).");
3823                return false;
3824            }
3825        }
3826    }
3827    state.space.map_output(&output, (x, y));
3828    let global = output.create_global::<AppState>(&state.dh);
3829    let damage_tracker = OutputDamageTracker::from_output(&output);
3830    if let Some(host) = state.host.as_ref() {
3831        host.set_layout(id, x, y);
3832    }
3833    println!("[Wayland] Output {id} created: {width}x{height} @ ({x}, {y}) scale {scale:.2}.");
3834    state.output_nodes.push(wayland::frontend::OutputNode {
3835        id,
3836        output,
3837        global,
3838        pos: (x, y),
3839        damage_tracker,
3840        frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
3841        offscreen_buffer: offscreen,
3842        overlay_state: OverlayState::default(),
3843        capture: None,
3844        frame_seq: 0,
3845        target_seeded: false,
3846        content_hold_until: None,
3847    });
3848    // A nested session opens one host toplevel per screen and the extras wait
3849    // parked until a display exists for them: hand the newest waiting window to
3850    // the new output. Windows stacked on an output rather than parked — anything
3851    // placed before this compositor started parking them — remain candidates.
3852    let mut counts: Vec<(u32, usize)> = Vec::new();
3853    for w in state.space.elements() {
3854        let oid = wayland::frontend::window_output_id(w);
3855        match counts.iter_mut().find(|(o, _)| *o == oid) {
3856            Some((_, c)) => *c += 1,
3857            None => counts.push((oid, 1)),
3858        }
3859    }
3860    let newest = |pred: &dyn Fn(&smithay::desktop::Window) -> bool| {
3861        state
3862            .space
3863            .elements()
3864            .filter(|w| pred(w))
3865            .max_by_key(|w| wayland::frontend::window_meta(w).map(|m| m.id).unwrap_or(0))
3866            .cloned()
3867    };
3868    let adopt = newest(&|w| {
3869        wayland::frontend::window_meta(w)
3870            .map(|m| m.parked.load(std::sync::atomic::Ordering::Relaxed))
3871            .unwrap_or(false)
3872    })
3873    .or_else(|| {
3874        newest(&|w| {
3875            let oid = wayland::frontend::window_output_id(w);
3876            counts.iter().any(|(o, c)| *o == oid && *c >= 2)
3877        })
3878    });
3879    if let Some(window) = adopt {
3880        state.place_window_on_output(&window, id);
3881        println!(
3882            "[Wayland] Output {id}: adopted waiting window {}.",
3883            wayland::frontend::window_meta(&window).map(|m| m.id).unwrap_or(0)
3884        );
3885    }
3886    true
3887}
3888
3889/// Move an existing output (the primary included) to layout offset `(x, y)`. The output's
3890/// advertised position, its Space mapping, and the windows placed on it (mapped at the
3891/// output's origin — window positions are output-relative under forced fullscreen) all
3892/// follow, so absolute input injection and cursor compositing — both keyed off `node.pos` —
3893/// resolve against the new layout immediately. A destination overlapping another live
3894/// output is refused (false). As with `CreateOutput`, only the placement itself is
3895/// validated: a capture reconfigure (StartCapture on an existing output) resizes
3896/// UNVALIDATED, so keeping a multi-step relayout overlap-free at every step is the
3897/// caller's ordering responsibility.
3898fn reposition_output_on(state: &mut AppState, id: u32, x: i32, y: i32) -> bool {
3899    let Some(idx) = state.node_idx_for_id(id) else { return false };
3900    let output = state.output_nodes[idx].output.clone();
3901    if state.output_nodes[idx].pos == (x, y) {
3902        return true;
3903    }
3904    let logical_size = state.output_nodes[idx]
3905        .logical_geometry()
3906        .map(|g| (g.size.w, g.size.h))
3907        .unwrap_or((0, 0));
3908    let physical_size = output.current_mode().map(|m| (m.size.w, m.size.h)).unwrap_or((0, 0));
3909    if let Some((oid, flavor, other)) = find_output_overlap(
3910        &state.output_nodes,
3911        Some(id),
3912        (x, y, logical_size.0, logical_size.1),
3913        (x, y, physical_size.0, physical_size.1),
3914    ) {
3915        eprintln!(
3916            "[Wayland] RepositionOutput {id}: rejected, {flavor} rect {}x{}+{x}+{y} overlaps output {oid} at {}x{}+{}+{}.",
3917            if flavor == "logical" { logical_size.0 } else { physical_size.0 },
3918            if flavor == "logical" { logical_size.1 } else { physical_size.1 },
3919            other.2, other.3, other.0, other.1,
3920        );
3921        return false;
3922    }
3923    state.output_nodes[idx].pos = (x, y);
3924    if let Some(host) = state.host.as_ref() {
3925        host.set_layout(id, x, y);
3926    }
3927    output.change_current_state(None, None, None, Some((x, y).into()));
3928    state.space.map_output(&output, (x, y));
3929    let windows: Vec<smithay::desktop::Window> = state
3930        .space
3931        .elements()
3932        .filter(|w| wayland::frontend::window_output_id(w) == id)
3933        .cloned()
3934        .collect();
3935    for window in &windows {
3936        state.space.map_element(window.clone(), (x, y), false);
3937    }
3938    if let Some(cap) = state.output_nodes[idx].capture.as_mut() {
3939        cap.needs_full_render = true;
3940    }
3941    println!("[Wayland] Output {id} repositioned to ({x}, {y}).");
3942    true
3943}
3944
3945/// Destroy a secondary output: end its capture, relocate its windows onto the primary
3946/// output, unmap it from the space, and retract its global. The primary (id 0) is refused.
3947fn destroy_output_on(state: &mut AppState, id: u32) -> bool {
3948    if id == 0 {
3949        return false;
3950    }
3951    let Some(_) = state.node_idx_for_id(id) else { return false };
3952    stop_capture_on_display(state, id);
3953    if let Some(host) = state.host.as_ref() {
3954        host.idle_output(id);
3955    }
3956    wayland_owners().lock().unwrap().remove(&id);
3957    // Relocate while the node is still registered so output leave/enter both resolve.
3958    let windows: Vec<smithay::desktop::Window> = state
3959        .space
3960        .elements()
3961        .filter(|w| wayland::frontend::window_output_id(w) == id)
3962        .cloned()
3963        .collect();
3964    for window in &windows {
3965        // The primary keeps whichever screen it already shows: a nested session's
3966        // second screen parks again rather than covering the first.
3967        if state.would_cover_screen(window, 0) {
3968            state.park_window(window, 0);
3969        } else {
3970            state.place_window_on_output(window, 0);
3971        }
3972    }
3973    for w in &state.pending_windows {
3974        if let Some(meta) = wayland::frontend::window_meta(w)
3975            && meta.output.load(Ordering::Relaxed) == id {
3976                meta.output.store(0, Ordering::Relaxed);
3977            }
3978    }
3979    let idx = state.node_idx_for_id(id).unwrap();
3980    let node = state.output_nodes.remove(idx);
3981    state.space.unmap_output(&node.output);
3982    state.dh.remove_global::<AppState>(node.global);
3983    println!(
3984        "[Wayland] Output {id} destroyed; {} window(s) relocated to primary.",
3985        windows.len()
3986    );
3987    true
3988}
3989/// Startup inputs for the compositor thread: its two calloop channels, the sender it hands
3990/// to computer-use, the initial geometry, and the GPU / cursor policy.
3991struct WaylandThreadConfig {
3992    command_rx: smithay::reexports::calloop::channel::Channel<ThreadCommand>,
3993    wake_rx: smithay::reexports::calloop::channel::Channel<()>,
3994    command_tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
3995    initial_width: i32,
3996    initial_height: i32,
3997    explicit_dri_node: String,
3998    auto_gpu_selected: bool,
3999    cursor_size: i32,
4000}
4001
4002/// Bring the hardware renderer up on a DRM render node: the GBM device, the EGL display and
4003/// context it backs, the GLES renderer, and the allocator the render targets come from.
4004///
4005/// The compositor and [`probe_wayland_gpu`] share this so the probe's answer is the
4006/// compositor's own. Errors name the step that failed, which is all a caller can act on.
4007fn gpu_render_init(
4008    device_path: &std::path::Path,
4009) -> Result<(RawGbmDevice<File>, GlesRenderer), String> {
4010    let file = File::options().read(true).write(true).open(device_path)
4011        .map_err(|e| format!("Failed to open render device: {}", e))?;
4012    let file_for_alloc = file.try_clone()
4013        .map_err(|e| format!("Failed to clone file for GBM Allocator: {}", e))?;
4014    let gbm_allocator = RawGbmDevice::new(file_for_alloc)
4015        .map_err(|_| "Failed to create Raw GBM Device")?;
4016    let gbm = GbmDevice::new(file)
4017        .map_err(|_| "Failed to create GBM device")?;
4018    let egl = unsafe { EGLDisplay::new(gbm) }
4019        .map_err(|_| "Failed to create EGL display")?;
4020    let context = EGLContext::new(&egl)
4021        .map_err(|_| "Failed to create EGL context")?;
4022    let renderer = unsafe { GlesRenderer::new(context) }
4023        .map_err(|_| "Failed to init GlesRenderer")?;
4024    Ok((gbm_allocator, renderer))
4025}
4026
4027/// `GL_RENDERER` of a live renderer — the driver that actually answered, which is the only
4028/// thing that separates a GPU from Mesa's software fallback on the same node. Empty when the
4029/// context cannot be made current or the string is unavailable.
4030fn gl_renderer_name(renderer: &mut GlesRenderer) -> String {
4031    renderer
4032        .with_context(|gl| unsafe {
4033            let ptr = gl.GetString(smithay::backend::renderer::gles::ffi::RENDERER);
4034            if ptr.is_null() {
4035                String::new()
4036            } else {
4037                std::ffi::CStr::from_ptr(ptr as *const std::ffi::c_char)
4038                    .to_string_lossy()
4039                    .into_owned()
4040            }
4041        })
4042        .unwrap_or_default()
4043}
4044
4045/// A GPU is exposed to this container or machine.
4046///
4047/// Only device nodes count. `/sys/class/drm` is the host's and lists cards a container may
4048/// have no access to, while `/dev` is the container's own; the NVIDIA character devices
4049/// answer for a driver stack that was given no DRM node at all.
4050fn gpu_exposed() -> bool {
4051    std::path::Path::new("/dev/nvidiactl").exists()
4052        || std::fs::read_dir("/dev/dri")
4053            .into_iter()
4054            .flatten()
4055            .flatten()
4056            .any(|e| {
4057                let name = e.file_name();
4058                let name = name.to_string_lossy();
4059                name.starts_with("renderD") || name.starts_with("card")
4060            })
4061}
4062
4063/// The main execution loop of the Wayland backend.
4064///
4065/// This function is the central nervous system of the backend. It runs on its own thread and owns
4066/// the entire lifecycle of the headless Wayland compositor:
4067///
4068/// 1. **Initialization**: builds the `calloop` event loop and the Wayland display, raises
4069///    libwayland's per-client buffer limit when the newer setter is available (resolved at runtime
4070///    so the module still loads against older libwayland), and brings up the rendering pipeline —
4071///    GBM/EGL hardware acceleration on the resolved DRM render node, falling back to software
4072///    rendering (Pixman) when no node is usable.
4073/// 2. **State management**: constructs and holds the `AppState` — the Wayland globals (compositor,
4074///    seat, SHM, shell, dmabuf, selections, and the rest) plus the output registry: the primary
4075///    virtual `HEADLESS-1` output at layout (0, 0), extended at runtime by CreateOutput with
4076///    additional outputs at their layout offsets, each `OutputNode` owning its damage tracker,
4077///    render targets, and (at most one) capture pipeline.
4078/// 3. **Event dispatch**:
4079///    - **Command channel**: control messages from the Python thread — per-display start/stop,
4080///      output lifecycle (create/destroy/list/move-window), input injection routed across the
4081///      output layout, keymap and clipboard operations, live rate / tunable changes, and the
4082///      computer-use queries.
4083///    - **Wayland socket**: accepts client connections and drives the compositor protocol.
4084/// 4. **StartCapture reconfigure** (per display): reprograms that output's mode / scale / refresh,
4085///    resizes its framebuffer and offscreen GBM buffer, and fullscreens the toplevels placed on
4086///    it. The encode device is resolved here: an operator's explicit `encode_node_index`
4087///    (-1 software, >= 0 a device) always wins, and only the unset `-2` sentinel is filled from
4088///    the auto-picked render node. H.264 output masks the dimensions even, because 4:2:0 needs
4089///    even width and height.
4090/// 5. **Encode-path choice + render loop**: only a same-GPU GLES session encodes zero-copy on this
4091///    calloop thread, because the dmabuf and its EGL context are calloop-affine; every readback
4092///    flavor (striped software H.264/JPEG, Pixman, or a cross-GPU hardware encoder) builds its
4093///    encoders on that display's dedicated encode thread instead. A shared timer (paced at the
4094///    fastest active capture) renders each capturing output — its windows, popups and layers made
4095///    output-local, the cursor only on the pointer's output — applies the shared paint-over /
4096///    recovery-IDR policy per display, and delivers each display's encoded stripes through its own
4097///    frame callback. The zero-copy encode waits the GL render fence first, so a hardware encoder
4098///    reading the dmabuf through CUDA/VA never maps a half-rasterized (torn) frame.
4099/// 6. **Thread lifecycle**: the Python frame callback runs on a dedicated delivery thread so its
4100///    GIL never stalls calloop input / control dispatch, and in readback mode the encoders run on
4101///    the `wl-encode` thread. On a restart or stop the encode thread is torn down before the
4102///    delivery thread — it feeds the delivery sender and must be gone first — and the retained
4103///    callbacks are dropped and gated by a process-shutdown flag so nothing fires into a finalizing
4104///    interpreter.
4105fn run_wayland_thread(cfg: WaylandThreadConfig) {
4106    let WaylandThreadConfig {
4107        command_rx,
4108        wake_rx,
4109        command_tx,
4110        initial_width,
4111        initial_height,
4112        explicit_dri_node,
4113        auto_gpu_selected,
4114        cursor_size,
4115    } = cfg;
4116    let width: i32 = if initial_width > 0 { initial_width } else { 1024 };
4117    let height: i32 = if initial_height > 0 { initial_height } else { 768 };
4118
4119    let mut event_loop = match EventLoop::<AppState>::try_new() {
4120        Ok(l) => l,
4121        Err(e) => {
4122            eprintln!("[Wayland] compositor thread aborting: event loop init failed: {e}");
4123            return;
4124        }
4125    };
4126    let display: Display<AppState> = match Display::new() {
4127        Ok(d) => d,
4128        Err(e) => {
4129            eprintln!("[Wayland] compositor thread aborting: display init failed: {e}");
4130            return;
4131        }
4132    };
4133    let dh: DisplayHandle = display.handle();
4134    unsafe {
4135        if let Ok(lib) = libloading::Library::new("libwayland-server.so.0") {
4136            if let Ok(set_max) = lib.get::<unsafe extern "C" fn(*mut std::ffi::c_void, usize)>(
4137                b"wl_display_set_default_max_buffer_size\0",
4138            ) {
4139                set_max(
4140                    dh.backend_handle().display_ptr() as *mut std::ffi::c_void,
4141                    10 * 1024 * 1024,
4142                );
4143            }
4144            std::mem::forget(lib);
4145        }
4146    }
4147
4148    let dri_node = explicit_dri_node;
4149
4150    let mut use_gpu = !dri_node.is_empty();
4151    let render_node_path = dri_node.clone();
4152
4153    let mut gles_renderer = None;
4154    let mut pixman_renderer = None;
4155    let mut offscreen_buffer: Option<(BufferObject<()>, Dmabuf)> = None;
4156    let mut dmabuf_global = None;
4157    let mut gbm_device_raw = None;
4158    let mut dmabuf_state = DmabufState::new();
4159
4160    let mut gpu_success = false;
4161    if use_gpu {
4162        println!("[Wayland] Initializing GL Renderer using device: {}", dri_node);
4163        let init_res: Result<(), String> = (|| {
4164            let device_path = std::path::Path::new(&dri_node);
4165            let (gbm_allocator, mut renderer) = gpu_render_init(device_path)?;
4166
4167            if let Err(e) = renderer.bind_wl_display(&dh) {
4168                println!("[Wayland] Warning: Failed to bind EGL to Wayland Display (Optional): {:?}", e);
4169            }
4170
4171            let formats = Bind::<Dmabuf>::supported_formats(&renderer)
4172                .ok_or("Failed to query formats")?
4173                .into_iter()
4174                .collect::<Vec<_>>();
4175
4176            let node = DrmNode::from_path(device_path)
4177                .map_err(|_| "Failed to create DrmNode")?;
4178            let dmabuf_default_feedback = DmabufFeedbackBuilder::new(node.dev_id(), formats.clone()).build();
4179
4180            dmabuf_global = Some(if let Ok(default_feedback) = dmabuf_default_feedback {
4181                dmabuf_state.create_global_with_default_feedback::<AppState>(&dh, &default_feedback)
4182            } else {
4183                dmabuf_state.create_global::<AppState>(&dh, formats)
4184            });
4185
4186            let bo = gbm_allocator.create_buffer_object(
4187                width as u32, height as u32, GbmFormat::Argb8888, BufferObjectFlags::RENDERING
4188            ).map_err(|_| "Failed to allocate GBM buffer")?;
4189
4190            let dmabuf = create_dmabuf_from_bo(&bo);
4191            offscreen_buffer = Some((bo, dmabuf));
4192            gbm_device_raw = Some(gbm_allocator);
4193            gles_renderer = Some(renderer);
4194            Ok(())
4195        })();
4196
4197        match init_res {
4198            Ok(_) => gpu_success = true,
4199            Err(e) => {
4200                println!("[Wayland] GPU Initialization failed: {}. Falling back to Software Renderer (Pixman).", e);
4201                use_gpu = false;
4202            }
4203        }
4204    }
4205
4206    if !gpu_success {
4207        if dri_node.is_empty() {
4208            println!("[Wayland] No render node. Initializing Software Renderer (Pixman).");
4209        }
4210        pixman_renderer = Some(PixmanRenderer::new().expect("Failed to init PixmanRenderer"));
4211        use_gpu = false;
4212    }
4213
4214    let compositor_state = CompositorState::new_v6::<AppState>(&dh);
4215    let image_capture_source_state = ImageCaptureSourceState::new();
4216    let output_capture_source_state = OutputCaptureSourceState::new::<AppState>(&dh);
4217    let image_copy_capture_state = ImageCopyCaptureState::new::<AppState>(&dh);
4218    let fractional_scale_state = FractionalScaleManagerState::new::<AppState>(&dh);
4219    let shm_state = ShmState::new::<AppState>(&dh, vec![]);
4220    let output_state = OutputManagerState::new_with_xdg_output::<AppState>(&dh);
4221    let mut seat_state = SeatState::new();
4222    let shell_state = XdgShellState::new::<AppState>(&dh);
4223    let space = Space::default();
4224    let layer_shell_state = WlrLayerShellState::new::<AppState>(&dh);
4225    let data_device_state = DataDeviceState::new::<AppState>(&dh);
4226    let data_control_state = DataControlState::new::<AppState, _>(&dh, None, |_| true);
4227    let ext_data_control_state = ExtDataControlState::new::<AppState, _>(&dh, None, |_| true);
4228    let cursor_shape_state = CursorShapeManagerState::new::<AppState>(&dh);
4229    let _vk_global = dh.create_global::<AppState, ZwpVirtualKeyboardManagerV1, _>(1, ());
4230    let pointer_warp_state = PointerWarpManager::new::<AppState>(&dh);
4231    let relative_pointer_state = RelativePointerManagerState::new::<AppState>(&dh);
4232    let pointer_constraints_state = PointerConstraintsState::new::<AppState>(&dh);
4233
4234    let foreign_toplevel_list = ForeignToplevelListState::new::<AppState>(&dh);
4235    let xdg_decoration_state = XdgDecorationState::new::<AppState>(&dh);
4236    let single_pixel_buffer = SinglePixelBufferState::new::<AppState>(&dh);
4237    let viewporter_state = ViewporterState::new::<AppState>(&dh);
4238    let presentation_state = PresentationState::new::<AppState>(&dh, 1);
4239    let xdg_activation_state = XdgActivationState::new::<AppState>(&dh);
4240    let primary_selection_state = PrimarySelectionState::new::<AppState>(&dh);
4241    let popups = PopupManager::default();
4242
4243    let mut seat = seat_state.new_wl_seat(&dh, "seat0");
4244    seat.add_keyboard(XkbConfig::default(), 200, 25)
4245        .expect("Failed to init keyboard");
4246    seat.add_pointer();
4247
4248    let mut state = AppState {
4249        compositor_state,
4250        fractional_scale_state,
4251        viewporter_state,
4252        presentation_state,
4253        shm_state,
4254        single_pixel_buffer,
4255        dmabuf_state,
4256        dmabuf_global,
4257        ext_data_control_state,
4258        cursor_shape_state,
4259        image_capture_source_state,
4260        output_capture_source_state,
4261        image_copy_capture_state,
4262        copy_sessions: Vec::new(),
4263        output_state,
4264        seat_state,
4265        shell_state,
4266        layer_shell_state,
4267        space,
4268        data_device_state,
4269        data_control_state,
4270        dh: dh.clone(),
4271        seat,
4272        pointer_warp_state,
4273        relative_pointer_state,
4274        pointer_constraints_state,
4275        output_nodes: Vec::new(),
4276        pending_windows: Vec::new(),
4277        foreign_toplevel_list,
4278        xdg_decoration_state,
4279        xdg_activation_state,
4280        primary_selection_state,
4281        popups,
4282        gles_renderer,
4283        pixman_renderer,
4284        gbm_device: gbm_device_raw,
4285        settings: RustCaptureSettings {
4286            width,
4287            height,
4288            ..RustCaptureSettings::default()
4289        },
4290        cursor_callback_set: false,
4291        cursor_tx: wayland::cursor::spawn_cursor_worker(
4292            cursor_size,
4293            RustCaptureSettings::default().cursor_size_cap,
4294        ),
4295        clipboard_callback: None,
4296        pending_clipboard_read: None,
4297        current_selection_mime: None,
4298        last_log_time: Instant::now(),
4299        start_time: Instant::now(),
4300        clock: Clock::new(),
4301        use_gpu,
4302        cursor_helper: Cursor::load(cursor_size),
4303        keymap_policy: wayland::keymap::KeymapPolicy::empty(),
4304        host: None,
4305        host_layout_pending: std::collections::HashMap::new(),
4306        current_cursor_icon: None,
4307        cursor_surface_pending: false,
4308        cursor_buffer: None,
4309        render_cursor_on_framebuffer: false,
4310        render_node_path,
4311        auto_gpu_selected,
4312        pending_screenshot: None,
4313        command_rx: None,
4314        last_input_at: None,
4315        frame_idle_long: false,
4316        last_idle_service_at: None,
4317        deliver_reaper: Vec::new(),
4318        encode_reaper: Vec::new(),
4319    };
4320    // Seed the keymap policy with the seat's initial keymap so overlay binds splice onto
4321    // the exact text clients received.
4322    {
4323        let initial_keymap = if let Some(kb) = state.seat.get_keyboard() {
4324            kb.with_xkb_state(&mut state, |context| match context.xkb().lock() {
4325                Ok(guard) => {
4326                    let keymap = unsafe { guard.keymap() };
4327                    keymap.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
4328                }
4329                Err(_) => String::new(),
4330            })
4331        } else {
4332            String::new()
4333        };
4334        state.keymap_policy.rebuild_base(initial_keymap);
4335    }
4336
4337    let output = Output::new(
4338        "HEADLESS-1".into(),
4339        PhysicalProperties {
4340            size: (width, height).into(),
4341            subpixel: Subpixel::Unknown,
4342            make: "Pixelflux".into(),
4343            model: "Virtual".into(),
4344            serial_number: "001".into(),
4345        },
4346    );
4347    output.change_current_state(
4348        Some(OutputMode {
4349            size: (width, height).into(),
4350            refresh: 60_000,
4351        }),
4352        Some(Transform::Normal),
4353        Some(OutputScale::Fractional(1.0)),
4354        Some((0, 0).into()),
4355    );
4356    output.set_preferred(OutputMode {
4357        size: (width, height).into(),
4358        refresh: 60_000,
4359    });
4360    state.space.map_output(&output, (0, 0));
4361    let global = output.create_global::<AppState>(&dh);
4362    let damage_tracker = OutputDamageTracker::from_output(&output);
4363    state.output_nodes.push(wayland::frontend::OutputNode {
4364        id: 0,
4365        output,
4366        global,
4367        pos: (0, 0),
4368        damage_tracker,
4369        frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
4370        offscreen_buffer,
4371        overlay_state: OverlayState::default(),
4372        capture: None,
4373        frame_seq: 0,
4374        target_seeded: false,
4375        content_hold_until: None,
4376    });
4377
4378    /// Apply every queued control command in FIFO order. Sends wake the loop through the
4379    /// separate wake channel, and the render tick ALSO drains before starting its work, so
4380    /// queued input is applied ahead of a long render/encode instead of waiting it out.
4381    fn drain_thread_commands(state: &mut AppState) {
4382        let Some(rx) = state.command_rx.take() else { return };
4383        let mut had_input = false;
4384        while let Ok(cmd) = rx.try_recv() {
4385            had_input |= matches!(
4386                cmd,
4387                ThreadCommand::KeyboardKey { .. }
4388                    | ThreadCommand::KeyboardKeys { .. }
4389                    | ThreadCommand::PointerMotion { .. }
4390                    | ThreadCommand::PointerRelativeMotion { .. }
4391                    | ThreadCommand::PointerButton { .. }
4392                    | ThreadCommand::PointerAxis { .. }
4393            );
4394            handle_thread_command(state, cmd);
4395        }
4396        if had_input {
4397            state.last_input_at = Some(Instant::now());
4398        }
4399        state.command_rx = Some(rx);
4400    }
4401
4402    /// One idle-tick service pass for committed clients while nothing captures:
4403    /// frame callbacks unblock vsynced clients, requested presentation feedback
4404    /// is discarded (nothing presents), and the renderer's import cache is
4405    /// pruned since no render will do it. Shared by the frame timer's idle
4406    /// branch and the input wake handler, which must not let a fresh keypress
4407    /// wait out an already-armed long idle deadline.
4408    fn send_idle_frame_callbacks(state: &mut AppState) {
4409        let time = state.clock.now();
4410        for node in &state.output_nodes {
4411            let mut feedback = OutputPresentationFeedback::new(&node.output);
4412            for window in state
4413                .space
4414                .elements_for_output(&node.output)
4415                .cloned()
4416                .collect::<Vec<_>>()
4417            {
4418                window.send_frame(&node.output, time, Some(Duration::ZERO), |_, _| {
4419                    Some(node.output.clone())
4420                });
4421                window.take_presentation_feedback(
4422                    &mut feedback,
4423                    |_, _| Some(node.output.clone()),
4424                    |_, _| wp_presentation_feedback::Kind::empty(),
4425                );
4426            }
4427            for layer in layer_map_for_output(&node.output).layers() {
4428                layer.send_frame(&node.output, time, Some(Duration::ZERO), |_, _| {
4429                    Some(node.output.clone())
4430                });
4431                layer.take_presentation_feedback(
4432                    &mut feedback,
4433                    |_, _| Some(node.output.clone()),
4434                    |_, _| wp_presentation_feedback::Kind::empty(),
4435                );
4436            }
4437            feedback.discarded();
4438        }
4439        if let Some(output) = state.primary_output() {
4440            send_cursor_frame(state, output, time);
4441        }
4442        if let Some(renderer) = state.gles_renderer.as_mut() {
4443            let _ = renderer.cleanup_texture_cache();
4444        }
4445    }
4446
4447    fn handle_thread_command(state: &mut AppState, cmd: ThreadCommand) {
4448            match cmd {
4449                ThreadCommand::StartCapture { display_id, callback, settings } => {
4450                    start_capture_on_display(state, display_id, callback.map(Arc::new), settings);
4451                }
4452                ThreadCommand::StopCapture { display_id } => {
4453                    // Cursor and clipboard callbacks deliberately SURVIVE StopCapture:
4454                    // captures cycle on client disconnects and setting restarts, and a
4455                    // copy or cursor change during that gap must still reach Python.
4456                    // PY_SHUTDOWN gates every use against a finalizing interpreter.
4457                    stop_capture_on_display(state, display_id);
4458                }
4459                ThreadCommand::CreateOutput { id, width, height, x, y, scale, reply } => {
4460                    let _ = reply.send(create_output_on(state, id, width, height, x, y, scale));
4461                }
4462                ThreadCommand::DestroyOutput { id, reply } => {
4463                    let _ = reply.send(destroy_output_on(state, id));
4464                }
4465                ThreadCommand::OutputCapacity { reply } => {
4466                    let _ = reply
4467                        .send(state.host.as_ref().map_or(-1, |h| h.output_count() as i64));
4468                }
4469                ThreadCommand::RepositionOutput { id, x, y, reply } => {
4470                    let _ = reply.send(reposition_output_on(state, id, x, y));
4471                }
4472                ThreadCommand::ListOutputs { reply } => {
4473                    let list = state
4474                        .output_nodes
4475                        .iter()
4476                        .map(|n| {
4477                            let (w, h) = n
4478                                .output
4479                                .current_mode()
4480                                .map(|m| (m.size.w, m.size.h))
4481                                .unwrap_or((0, 0));
4482                            (
4483                                n.id,
4484                                n.pos.0,
4485                                n.pos.1,
4486                                w,
4487                                h,
4488                                n.output.current_scale().fractional_scale(),
4489                                n.capture.is_some(),
4490                            )
4491                        })
4492                        .collect();
4493                    let _ = reply.send(list);
4494                }
4495                ThreadCommand::MoveWindowToOutput { window_id, output_id, reply } => {
4496                    let window = state
4497                        .space
4498                        .elements()
4499                        .find(|w| {
4500                            wayland::frontend::window_meta(w)
4501                                .map(|m| m.id == window_id)
4502                                .unwrap_or(false)
4503                        })
4504                        .cloned();
4505                    let ok = match window {
4506                        Some(w) => state.place_window_on_output(&w, output_id),
4507                        None => false,
4508                    };
4509                    let _ = reply.send(ok);
4510                }
4511                ThreadCommand::ListWindows { reply } => {
4512                    use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
4513                    let mut list = Vec::new();
4514                    for window in state.space.elements() {
4515                        let Some(meta) = wayland::frontend::window_meta(window) else { continue };
4516                        let (title, app_id) = window
4517                            .toplevel()
4518                            .map(|tl| {
4519                                with_states(tl.wl_surface(), |states| {
4520                                    states
4521                                        .data_map
4522                                        .get::<XdgToplevelSurfaceData>()
4523                                        .map(|d| {
4524                                            let a = d.lock().unwrap();
4525                                            (
4526                                                a.title.clone().unwrap_or_default(),
4527                                                a.app_id.clone().unwrap_or_default(),
4528                                            )
4529                                        })
4530                                        .unwrap_or_default()
4531                                })
4532                            })
4533                            .unwrap_or_default();
4534                        list.push((
4535                            meta.id,
4536                            title,
4537                            app_id,
4538                            meta.output.load(Ordering::Relaxed),
4539                            meta.parked.load(Ordering::Relaxed),
4540                        ));
4541                    }
4542                    let _ = reply.send(list);
4543                }
4544                ThreadCommand::SetClipboardCallback(cb) => {
4545                    state.clipboard_callback = Some(cb);
4546                    // Re-stage a read of the CURRENT selection so a copy made before this
4547                    // callback was (re)armed is delivered rather than lost; the post-dispatch
4548                    // drain performs the read (a compositor-owned selection is skipped there).
4549                    if let Some(mime) = state.current_selection_mime.clone() {
4550                        state.pending_clipboard_read = Some(mime);
4551                    }
4552                }
4553                ThreadCommand::SetClipboard { mime, data } => {
4554                    let mimes: Vec<String> = if mime.starts_with("text/plain") {
4555                        ["text/plain;charset=utf-8", "UTF8_STRING", "text/plain",
4556                         "STRING", "TEXT"].iter().map(|s| s.to_string()).collect()
4557                    } else {
4558                        vec![mime.clone()]
4559                    };
4560                    let payload = std::sync::Arc::new((mime, data));
4561                    smithay::wayland::selection::data_device::set_data_device_selection(
4562                        &state.dh,
4563                        &state.seat.clone(),
4564                        mimes.clone(),
4565                        payload.clone(),
4566                    );
4567                    // Middle-click parity with the X11 clipboard bridge: the same
4568                    // offer backs the primary selection too.
4569                    smithay::wayland::selection::primary_selection::set_primary_selection(
4570                        &state.dh,
4571                        &state.seat.clone(),
4572                        mimes,
4573                        payload,
4574                    );
4575                    // The selection is compositor-owned now; a later SetClipboardCallback
4576                    // must not try to re-read a client source that no longer holds it.
4577                    state.current_selection_mime = None;
4578                }
4579                ThreadCommand::SetCursorCallback(cb) => {
4580                    let _ = state.cursor_tx.send(CursorJob::SetCallback(cb));
4581                    state.cursor_callback_set = true;
4582                    if let Some(icon) = state.current_cursor_icon.clone() {
4583                        state.send_cursor_image(&icon);
4584                    } else {
4585                        // No client has set a cursor yet. The render path treats
4586                        // None as the default theme cursor, so the first consumer
4587                        // must receive that same sprite instead of a blank pointer
4588                        // until the first client cursor event.
4589                        state.send_cursor_image(&CursorImageStatus::Named(Default::default()));
4590                    }
4591                }
4592                ThreadCommand::KeyboardKeys { events } => {
4593                    for (scancode, key_state_val) in events {
4594                        if let Some(host) = state.host.as_ref() {
4595                            host.key(scancode, key_state_val > 0);
4596                            continue;
4597                        }
4598                        let key_state = if key_state_val > 0 {
4599                            KeyState::Pressed
4600                        } else {
4601                            KeyState::Released
4602                        };
4603                        let serial = next_serial();
4604                        let time = wayland_time();
4605                        if let Some(keyboard) = state.seat.get_keyboard() {
4606                            keyboard.input(
4607                                state,
4608                                Keycode::new(scancode),
4609                                key_state,
4610                                serial,
4611                                time,
4612                                |_, _, _| FilterResult::<()>::Forward,
4613                            );
4614                        }
4615                    }
4616                }
4617                ThreadCommand::KeyboardKey { scancode, state: key_state_val } => {
4618                    if let Some(host) = state.host.as_ref() {
4619                        host.key(scancode, key_state_val > 0);
4620                        return;
4621                    }
4622                    let key_state = if key_state_val > 0 { KeyState::Pressed } else { KeyState::Released };
4623                    let serial = next_serial();
4624                    let time = wayland_time();
4625                    if let Some(keyboard) = state.seat.get_keyboard() {
4626                        keyboard.input(state, Keycode::new(scancode), key_state, serial, time, |_, _, _| {
4627                            FilterResult::<()>::Forward
4628                        });
4629                    }
4630                }
4631                ThreadCommand::SetKeymapString(text) => {
4632                    // rebuild_base rejects a string that will not compile without touching
4633                    // the policy, so the seat keymap survives a bad one either way.
4634                    if state.keymap_policy.rebuild_base(text) {
4635                        state.apply_keymap_policy();
4636                    } else {
4637                        eprintln!("[Wayland] set_keymap_string: keymap failed to compile; keeping current keymap.");
4638                    }
4639                }
4640                ThreadCommand::SetXkbLayout { rules, model, layout, variant, options, reply } => {
4641                    match crate::wayland::keymap::compile_rmlvo(&rules, &model, &layout, &variant, &options) {
4642                        Some(text) => {
4643                            state.keymap_policy.rebuild_base(text);
4644                            state.apply_keymap_policy();
4645                            let _ = reply.send(true);
4646                        }
4647                        None => {
4648                            eprintln!("[Wayland] set_xkb_layout: RMLVO ({rules:?}, {model:?}, {layout:?}, {variant:?}, {options:?}) failed to compile.");
4649                            let _ = reply.send(false);
4650                        }
4651                    }
4652                }
4653                ThreadCommand::BindKeysyms { keysyms, reply } => {
4654                    let _ = reply.send(state.bind_keysyms(&keysyms));
4655                }
4656                ThreadCommand::SetKeymapOverlay { binds } => {
4657                    if state.keymap_policy.has_base() {
4658                        state.keymap_policy.set_manual_overlay(&binds);
4659                        state.apply_keymap_policy();
4660                    } else {
4661                        eprintln!(
4662                            "[Wayland] set_keymap_overlay: no base keymap to splice onto."
4663                        );
4664                    }
4665                }
4666                ThreadCommand::GetKeyboardState { reply } => {
4667                    let (pressed, mods) = state
4668                        .seat
4669                        .get_keyboard()
4670                        .map(|kb| {
4671                            let pressed: Vec<u32> =
4672                                kb.pressed_keys().iter().map(|c| c.raw()).collect();
4673                            let m = kb.modifier_state();
4674                            let mask = (m.ctrl as u32)
4675                                | (m.shift as u32) << 1
4676                                | (m.alt as u32) << 2
4677                                | (m.logo as u32) << 3
4678                                | (m.caps_lock as u32) << 4
4679                                | (m.num_lock as u32) << 5
4680                                | (m.iso_level3_shift as u32) << 6
4681                                | (m.iso_level5_shift as u32) << 7;
4682                            (pressed, mask)
4683                        })
4684                        .unwrap_or_default();
4685                    let _ = reply.send((pressed, mods));
4686                }
4687                ThreadCommand::Barrier { reply } => {
4688                    // The shutdown path fences on this after its StopCaptures:
4689                    // joining the reaped threads before acknowledging means
4690                    // nothing that can attach to Python survives past the
4691                    // fence, so the interpreter never finalizes under a live
4692                    // callback. Bounded — discard flags are up and senders
4693                    // dropped, so each chain exits after at most its
4694                    // in-flight callback.
4695                    for join in state.encode_reaper.drain(..) {
4696                        let _ = join.join();
4697                    }
4698                    for join in state.deliver_reaper.drain(..) {
4699                        let _ = join.join();
4700                    }
4701                    let _ = reply.send(());
4702                }
4703                ThreadCommand::GetXkbKeymap { reply } => {
4704                    let mut keymap_str = String::new();
4705                    if let Some(keyboard) = state.seat.get_keyboard() {
4706                        keymap_str = keyboard.with_xkb_state(state, |context| {
4707                            match context.xkb().lock() {
4708                                Ok(guard) => {
4709                                    let keymap = unsafe { guard.keymap() };
4710                                    keymap.get_as_string(
4711                                        smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1,
4712                                    )
4713                                }
4714                                Err(_) => String::new(),
4715                            }
4716                        });
4717                    }
4718                    let _ = reply.send(keymap_str);
4719                }
4720                ThreadCommand::PointerMotion { x, y } => {
4721                    if let Some(host) = state.host.as_ref() {
4722                        host.pointer_motion_abs(x, y);
4723                        return;
4724                    }
4725                    let serial = next_serial();
4726                    let time = wayland_time();
4727                    // (x, y) are physical union-layout coordinates: each output occupies
4728                    // the physical rectangle at its layout offset, and the point maps
4729                    // through the CONTAINING output's scale (clamped into the nearest
4730                    // output when outside all of them).
4731                    let p = state.layout_physical_to_logical(x, y);
4732
4733                    if let Some(pointer) = state.seat.get_pointer() {
4734                        // Layer surfaces live on the output under the point; their
4735                        // geometry is output-local, so hit-test with the local point and
4736                        // report the global location.
4737                        let layer_hit = |state: &AppState, layers: &[smithay::wayland::shell::wlr_layer::Layer]| {
4738                            let idx = state.node_idx_under(p)?;
4739                            let node = &state.output_nodes[idx];
4740                            let origin = Point::<i32, smithay::utils::Logical>::from(node.pos);
4741                            let local = (p - origin.to_f64()).to_i32_round();
4742                            let layer_map = layer_map_for_output(&node.output);
4743                            for layer in layer_map.layers().rev() {
4744                                if layers.contains(&layer.layer())
4745                                    && let Some(bbox) = layer_map.layer_geometry(layer)
4746                                    && bbox.contains(local) {
4747                                            return Some((
4748                                                FocusTarget::LayerSurface(layer.clone()),
4749                                                (bbox.loc + origin).to_f64(),
4750                                            ));
4751                                        }
4752                            }
4753                            None
4754                        };
4755
4756                        let mut under = layer_hit(state, &[
4757                            smithay::wayland::shell::wlr_layer::Layer::Overlay,
4758                            smithay::wayland::shell::wlr_layer::Layer::Top,
4759                        ]);
4760
4761                        if under.is_none() {
4762                            under = state.space.element_under(p).map(|(window, loc)| {
4763                                (FocusTarget::Window(window.clone()), loc.to_f64())
4764                            });
4765                        }
4766
4767                        if under.is_none() {
4768                            under = layer_hit(state, &[
4769                                smithay::wayland::shell::wlr_layer::Layer::Bottom,
4770                                smithay::wayland::shell::wlr_layer::Layer::Background,
4771                            ]);
4772                        }
4773
4774                        pointer.motion(state, under, &MotionEvent { location: p, serial, time });
4775                        pointer.frame(state);
4776                    }
4777                }
4778                ThreadCommand::PointerRelativeMotion { dx, dy } => {
4779                    if let Some(host) = state.host.as_ref() {
4780                        host.pointer_motion_rel(dx, dy);
4781                        return;
4782                    }
4783                    let utime = wayland_utime();
4784                    let time = wayland_time();
4785                    let serial = next_serial();
4786
4787                    if let Some(pointer) = state.seat.get_pointer() {
4788                        let current_pos = pointer.current_location();
4789                        let new_pos = state.clamp_logical(
4790                            (current_pos.x + dx, current_pos.y + dy).into(),
4791                        );
4792
4793                        let under = state.space.element_under(new_pos).map(|(window, loc)| {
4794                            (FocusTarget::Window(window.clone()), loc.to_f64())
4795                        });
4796
4797                        pointer.motion(
4798                            state, 
4799                            under.clone(), 
4800                            &MotionEvent { 
4801                                location: new_pos, 
4802                                serial, 
4803                                time 
4804                            }
4805                        );
4806
4807                        let event = RelativeMotionEvent {
4808                            utime,
4809                            delta: (dx, dy).into(),
4810                            delta_unaccel: (dx, dy).into(),
4811                        };
4812                        pointer.relative_motion(state, under, &event);
4813
4814                        pointer.frame(state);
4815                    }
4816                }
4817                ThreadCommand::PointerButton { btn, state: btn_state_val } => {
4818                    if let Some(host) = state.host.as_ref() {
4819                        host.pointer_button(btn, btn_state_val > 0);
4820                        return;
4821                    }
4822                    let serial = next_serial();
4823                    let time = wayland_time();
4824                    let button_state = if btn_state_val > 0 { smithay::backend::input::ButtonState::Pressed } else { smithay::backend::input::ButtonState::Released };
4825
4826                    if let Some(pointer) = state.seat.get_pointer() {
4827                        if button_state == smithay::backend::input::ButtonState::Pressed {
4828                            let pos = pointer.current_location();
4829                            let target_window = state.space.element_under(pos).map(|(w, _)| w.clone());
4830
4831                            if let Some(window) = target_window {
4832                                state.space.raise_element(&window, true);
4833                                if let Some(keyboard) = state.seat.get_keyboard() {
4834                                    keyboard.set_focus(state, Some(FocusTarget::Window(window)), serial);
4835                                }
4836                            }
4837                        }
4838                        let button = btn;
4839                        pointer.button(state, &ButtonEvent { button, state: button_state, serial, time });
4840                        pointer.frame(state);
4841                    }
4842                }
4843                ThreadCommand::PointerAxis { x, y } => {
4844                    if let Some(host) = state.host.as_ref() {
4845                        host.pointer_axis(x, y);
4846                        return;
4847                    }
4848                    let time = wayland_time();
4849                    
4850                    if let Some(pointer) = state.seat.get_pointer() {
4851                        let mut frame = AxisFrame::new(time).source(AxisSource::Wheel);
4852
4853                        if x != 0.0 { 
4854                            frame = frame
4855                                .value(Axis::Horizontal, x)
4856                                .v120(Axis::Horizontal, (x * SCROLL_V120_PER_UNIT) as i32);
4857                        }
4858                        
4859                        if y != 0.0 { 
4860                            frame = frame
4861                                .value(Axis::Vertical, y)
4862                                .v120(Axis::Vertical, (y * SCROLL_V120_PER_UNIT) as i32);
4863                        }
4864
4865                        if x != 0.0 || y != 0.0 {
4866                            pointer.axis(state, frame);
4867                            pointer.frame(state);
4868                        }
4869                    }
4870                }
4871                ThreadCommand::UpdateCursorConfig { render_on_framebuffer } => {
4872                    state.render_cursor_on_framebuffer = render_on_framebuffer;
4873                    if let Some(host) = state.host.as_ref() {
4874                        host.set_cursor_painting(render_on_framebuffer);
4875                    }
4876                }
4877                ThreadCommand::SetCursorSize { size, reply } => {
4878                    if size <= 0 {
4879                        let _ = reply.send(false);
4880                    } else {
4881                        state.cursor_helper = Cursor::load(size);
4882                        let _ = state.cursor_tx.send(CursorJob::SetSize(size));
4883                        // The burned-in cursor changed size; force a repaint everywhere so
4884                        // a static screen doesn't keep showing the old sprite.
4885                        for node in state.output_nodes.iter_mut() {
4886                            if let Some(cap) = node.capture.as_mut() {
4887                                cap.needs_full_render = true;
4888                            }
4889                        }
4890                        let _ = reply.send(true);
4891                    }
4892                }
4893                ThreadCommand::RequestIdr { display_id } => {
4894                    if let Some(idx) = state.node_idx_for_id(display_id)
4895                        && let Some(cap) = state.output_nodes[idx].capture.as_mut() {
4896                            cap.request_idr();
4897                        }
4898                }
4899                ThreadCommand::UpdateRate { display_id, bitrate_kbps, vbv_multiplier, fps } => {
4900                    if let Some(idx) = state.node_idx_for_id(display_id)
4901                        && let Some(cap) = state.output_nodes[idx].capture.as_mut() {
4902                            if let Some(b) = bitrate_kbps { cap.settings.video_bitrate_kbps = b; }
4903                            if let Some(v) = vbv_multiplier { cap.settings.video_vbv_multiplier = v; }
4904                            if let Some(f) = fps && f > 0.0 { cap.settings.target_fps = f; }
4905                            if let Some(GpuEncoder::Nvenc(enc)) = cap.video_encoder.as_mut() {
4906                                enc.reconfigure_rate(&cap.settings);
4907                            }
4908                            if let Some(GpuEncoder::Vaapi(enc)) = cap.video_encoder.as_mut()
4909                                && let Err(e) = enc.reconfigure_rate(&cap.settings) {
4910                                    // The failed re-open left no codec context: the next
4911                                    // tick's encode fails, and a full streak makes that
4912                                    // failure run the recovery ladder at once.
4913                                    eprintln!("[Wayland] VAAPI rate reconfigure failed: {e}");
4914                                    cap.hw_error_streak = HW_ERROR_RECOVERY_THRESHOLD - 1;
4915                                }
4916                            let c = &cap.encode_controls;
4917                            c.bitrate_kbps.store(cap.settings.video_bitrate_kbps, Ordering::Relaxed);
4918                            c.vbv_mult_milli.store(
4919                                (cap.settings.video_vbv_multiplier * 1000.0).round() as i32,
4920                                Ordering::Relaxed,
4921                            );
4922                            c.fps_milli.store(
4923                                (cap.settings.target_fps.max(1.0) * 1000.0) as u64,
4924                                Ordering::Relaxed,
4925                            );
4926                            c.rate_dirty.store(true, Ordering::Release);
4927                            if display_id == 0 {
4928                                state.settings.video_bitrate_kbps = cap.settings.video_bitrate_kbps;
4929                                state.settings.video_vbv_multiplier = cap.settings.video_vbv_multiplier;
4930                                state.settings.target_fps = cap.settings.target_fps;
4931                            }
4932                        }
4933                }
4934                ThreadCommand::UpdateTunables { display_id, tunables: t } => {
4935                    state.render_cursor_on_framebuffer = t.capture_cursor;
4936                    if let Some(host) = state.host.as_ref() {
4937                        host.set_cursor_painting(t.capture_cursor);
4938                    }
4939                    let _ = state.cursor_tx.send(CursorJob::SetSizeCap(t.cursor_size_cap));
4940                    if display_id == 0 {
4941                        t.apply_to(&mut state.settings);
4942                    }
4943                    if let Some(idx) = state.node_idx_for_id(display_id)
4944                        && let Some(cap) = state.output_nodes[idx].capture.as_mut() {
4945                            t.apply_to(&mut cap.settings);
4946                            *cap.encode_controls.tunables.lock().unwrap() = Some(t);
4947                            cap.encode_controls.tunables_dirty.store(true, Ordering::Release);
4948                        }
4949                }
4950                ThreadCommand::CuScreenshot { display_id, resp } => {
4951                    if state.node_idx_for_id(display_id).is_some() {
4952                        state.pending_screenshot = Some((display_id, resp));
4953                    } else {
4954                        let _ = resp.send(Err(format!("Unknown display: {display_id}")));
4955                    }
4956                }
4957                ThreadCommand::CuCursorPosition { resp } => {
4958                    let pos = state.seat.get_pointer()
4959                        .map(|p| p.current_location())
4960                        .unwrap_or_else(|| (0.0f64, 0.0f64).into());
4961                    let _ = resp.send(state.layout_logical_to_physical(pos));
4962                }
4963                ThreadCommand::CuGetInfo { display_id, resp } => {
4964                    // A host-capture start whose mode the host has not answered yet:
4965                    // the read parks until it has (the reply then carries the size
4966                    // actually captured), which is what makes it a barrier.
4967                    match state.host_layout_pending.get_mut(&display_id) {
4968                        Some(p) => p.geometry_waiters.push(resp),
4969                        None => {
4970                            let _ = resp.send(realized_geometry(state, display_id));
4971                        }
4972                    }
4973                }
4974            }
4975    }
4976
4977    state.command_rx = Some(command_rx);
4978    event_loop
4979        .handle()
4980        .insert_source(wake_rx, |_, _, state| {
4981            drain_thread_commands(state);
4982            // Input landing while the frame timer sits on its long idle
4983            // deadline must not wait it out: service the frame callbacks now
4984            // (at most at frame pace) so the app's echo repaint starts with
4985            // the keypress; the timer's own next fire re-arms the short pace.
4986            if state.frame_idle_long
4987                && state
4988                    .last_input_at
4989                    .is_some_and(|t| t.elapsed() < Duration::from_millis(50))
4990                && state
4991                    .last_idle_service_at
4992                    .is_none_or(|t| t.elapsed() >= Duration::from_millis(16))
4993            {
4994                state.last_idle_service_at = Some(Instant::now());
4995                send_idle_frame_callbacks(state);
4996            }
4997        })
4998        .unwrap();
4999
5000    let source = match ListeningSocketSource::new_auto() {
5001        Ok(s) => s,
5002        Err(e) => {
5003            eprintln!("[Wayland] compositor thread aborting: could not bind a wayland-N socket (XDG_RUNTIME_DIR unset/full?): {e}");
5004            return;
5005        }
5006    };
5007    let socket_name = source.socket_name().to_string_lossy().into_owned();
5008    println!("[Wayland] Socket listening on: {:?}", socket_name);
5009    // Writing the process environment races any concurrent getenv, which is why
5010    // it is unsafe: nothing here can stop a thread already running from reading
5011    // it. This one write happens during compositor bring-up, before any capture
5012    // or encoder thread exists, and the value is what children of this process
5013    // and the backend probe below need to find the compositor.
5014    unsafe { std::env::set_var("WAYLAND_DISPLAY", &socket_name) };
5015    publish_socket_name(&socket_name);
5016
5017    event_loop
5018        .handle()
5019        .insert_source(source, |client_stream, _, state| {
5020            if let Err(err) = state
5021                .dh
5022                .insert_client(client_stream, Arc::new(ClientState::default()))
5023            {
5024                eprintln!("Error adding wayland client: {:?}", err);
5025            }
5026        })
5027        .expect("Failed to init wayland socket source");
5028
5029    let timer = Timer::immediate();
5030    event_loop
5031        .handle()
5032        .insert_source(timer, move |_, _, state| {
5033            // Apply queued commands (input above all) BEFORE the render/encode work: a
5034            // command that raced the timer wakeup would otherwise wait out the whole tick.
5035            reap_dead_host(state);
5036            drain_thread_commands(state);
5037            reconcile_host_layouts(state);
5038            // Deliver and deferred encode threads of stopped captures are
5039            // joined only once they report finished (a plain atomic load), so
5040            // the tick never waits on one; whatever remains at shutdown, the
5041            // Barrier drain joins.
5042            let mut i = 0;
5043            while i < state.deliver_reaper.len() {
5044                if state.deliver_reaper[i].is_finished() {
5045                    let _ = state.deliver_reaper.swap_remove(i).join();
5046                } else {
5047                    i += 1;
5048                }
5049            }
5050            let mut i = 0;
5051            while i < state.encode_reaper.len() {
5052                if state.encode_reaper[i].is_finished() {
5053                    let _ = state.encode_reaper.swap_remove(i).join();
5054                } else {
5055                    i += 1;
5056                }
5057            }
5058            let loop_start_time = Instant::now();
5059            state.space.refresh();
5060
5061            let now = Instant::now();
5062            let elapsed = now.duration_since(state.last_log_time).as_secs_f64();
5063            if elapsed >= 1.0 {
5064                // Memory is read here rather than per tick: it is reported, not acted on,
5065                // and /proc plus a /dev/shm walk have no place in the render path.
5066                let mut mem: Option<(usize, u64)> = None;
5067                for node in &state.output_nodes {
5068                    let Some(cap) = node.capture.as_ref() else { continue };
5069                    let frames = cap.encode_stats.frames.swap(0, Ordering::Relaxed);
5070                    let stripes = cap.encode_stats.stripes.swap(0, Ordering::Relaxed);
5071                    if cap.settings.debug_logging {
5072                        let actual_fps = frames as f64 / elapsed;
5073                        let stripes_per_sec = stripes as f64 / elapsed;
5074                        let mode_str = cap.encode_stats.desc.lock().unwrap().clone();
5075                        let n_stripes = cap.encode_stats.n_stripes.load(Ordering::Relaxed);
5076                        let (current_rss, shm_usage) = *mem
5077                            .get_or_insert_with(|| (get_process_rss_bytes(), get_shm_usage_bytes()));
5078
5079                        println!("Display: {} Res: {}x{} Mode: {} Stripes: {} EncFPS: {:.2} EncStripes/s: {:.2} Mem: {}MB SHM: {}MB",
5080                            node.id, cap.settings.width, cap.settings.height, mode_str, n_stripes, actual_fps, stripes_per_sec, current_rss / 1024 / 1024, shm_usage / 1024 / 1024);
5081                    }
5082                }
5083                state.last_log_time = now;
5084            }
5085
5086            let any_capturing = state.output_nodes.iter().any(|n| n.capture.is_some());
5087            let any_copy_frame = state.copy_sessions.iter().any(|cs| cs.pending.is_some());
5088            if !any_capturing && state.pending_screenshot.is_none() && !any_copy_frame {
5089                // No render/encode work, but committed clients still need their
5090                // frame callbacks: a vsynced client (FIFO Vulkan present, games, a
5091                // nested compositor's own clients) otherwise blocks in its swap
5092                // until a viewer attaches — apps appear frozen whenever nobody is
5093                // watching.
5094                //
5095                // Slowly, though: at a viewing rate the clients draw at a viewing rate
5096                // too, and every frame they hand over is one that nothing renders,
5097                // encodes or looks at. Unblocking them is the whole purpose here, and
5098                // that costs a callback every so often rather than sixty a second.
5099                state.last_idle_service_at = Some(Instant::now());
5100                send_idle_frame_callbacks(state);
5101                // A parked capture session is an attached consumer that may request a
5102                // frame at any moment: tick at frame pace so that request waits at most
5103                // one frame, not a quarter second. Input gets the same window: apps
5104                // pacing on frame callbacks see their echo promptly after a keypress,
5105                // or the idle rate alone turns typing into a 250ms stutter. The wake
5106                // handler covers the first keypress landing while the long deadline
5107                // is already armed.
5108                let post_input = state
5109                    .last_input_at
5110                    .is_some_and(|t| t.elapsed() < Duration::from_secs(1));
5111                let idle = if !state.copy_sessions.is_empty() || post_input {
5112                    Duration::from_millis(16)
5113                } else {
5114                    IDLE_FRAME_INTERVAL
5115                };
5116                state.frame_idle_long = idle == IDLE_FRAME_INTERVAL;
5117                return TimeoutAction::ToDuration(idle);
5118            }
5119            state.frame_idle_long = false;
5120
5121            // Render every output that needs it. The nodes are taken out of the state so
5122            // each per-output render can borrow the shared renderer/space alongside its
5123            // own damage tracker and buffers.
5124            let mut nodes = std::mem::take(&mut state.output_nodes);
5125            let mut any_pool_busy = false;
5126            for node in nodes.iter_mut() {
5127                if render_node_tick(state, node) {
5128                    any_pool_busy = true;
5129                }
5130            }
5131            state.output_nodes = nodes;
5132
5133            if any_pool_busy {
5134                return TimeoutAction::ToDuration(Duration::from_millis(1));
5135            }
5136            let work_elapsed = loop_start_time.elapsed();
5137            let max_fps = state
5138                .output_nodes
5139                .iter()
5140                .filter_map(|n| n.capture.as_ref().map(|c| c.settings.target_fps))
5141                .fold(0.0f64, f64::max);
5142            let raw_fps = if max_fps > 0.0 { max_fps } else { state.settings.target_fps };
5143            // Settings are sanitized at the Python boundary; this final guard keeps a
5144            // non-finite value from ever reaching Duration::from_secs_f64 (panics on NaN).
5145            let fps = if raw_fps.is_finite() && raw_fps > 0.0 { raw_fps.min(MAX_FPS) } else { DEFAULT_FPS };
5146            let target_frame_duration = Duration::from_secs_f64(1.0 / fps);
5147            let wait_duration = target_frame_duration.saturating_sub(work_elapsed);
5148            let final_wait = if wait_duration.as_millis() < 1 { Duration::from_millis(1) } else { wait_duration };
5149            TimeoutAction::ToDuration(final_wait)
5150        })
5151        .expect("Failed to init capture timer");
5152
5153    event_loop
5154        .handle()
5155        .insert_source(Generic::new(display, Interest::READ, Mode::Level), |_, display, _state| {
5156            // A single misbehaving client must not take the compositor (and every other
5157            // session) down with it.
5158            if let Err(e) = unsafe { display.get_mut().dispatch_clients(_state) } {
5159                eprintln!("[Wayland] client dispatch error: {e:?}");
5160            }
5161            Ok(PostAction::Continue)
5162        })
5163        .unwrap();
5164
5165    crate::computer_use::register_wayland_backend(command_tx.clone());
5166    crate::computer_use::spawn_cu_from_env();
5167
5168    let _ = event_loop.run(None, &mut state, |state| {
5169        state.process_pending_clipboard_read();
5170        state.flush_pending_cursor();
5171        let _ = state.dh.flush_clients();
5172    });
5173}
5174
5175/// Zero-copy encoded-frame handoff to Python. Owns the encoded `Vec<u8>` and
5176/// exposes it read-only via the buffer protocol, so `bytes(frame)` /
5177/// `memoryview(frame)` alias the Rust buffer instead of copying. Carries the
5178/// four stripe-metadata ints as Python attributes.
5179#[pyclass]
5180struct StripeFrame {
5181    data: Arc<Vec<u8>>,
5182    #[pyo3(get, set)]
5183    data_type: i32,
5184    #[pyo3(get, set)]
5185    stripe_y_start: i32,
5186    #[pyo3(get, set)]
5187    stripe_height: i32,
5188    #[pyo3(get, set)]
5189    frame_id: i32,
5190}
5191
5192impl StripeFrame {
5193    /// Hot-path constructor: shares the encoder's buffer by `Arc` (no copy) and carries stripe
5194    /// metadata as attributes, so the consumer can read it without parsing a header
5195    /// (required for omit_stripe_headers).
5196    fn new_owned_meta(data: Arc<Vec<u8>>, data_type: i32, stripe_y_start: i32, stripe_height: i32, frame_id: i32) -> Self {
5197        Self { data, data_type, stripe_y_start, stripe_height, frame_id }
5198    }
5199}
5200
5201#[pymethods]
5202impl StripeFrame {
5203    /// Symmetry / testability constructor: copies the bytes-like into the owned `Vec`.
5204    /// The hot path uses `new_owned_meta` (a move) instead.
5205    #[new]
5206    #[pyo3(signature = (data, data_type = 0, stripe_y_start = 0, stripe_height = 0, frame_id = 0))]
5207    fn new(data: Vec<u8>, data_type: i32, stripe_y_start: i32, stripe_height: i32, frame_id: i32) -> Self {
5208        Self { data: Arc::new(data), data_type, stripe_y_start, stripe_height, frame_id }
5209    }
5210
5211    fn __len__(&self) -> usize {
5212        self.data.len()
5213    }
5214
5215    /// Expose the owned bytes read-only through the Python buffer protocol.
5216    ///
5217    /// `PyBuffer_FillInfo` INCREFs `slf` into `view->obj`, pinning the `Vec` until every view is
5218    /// released, so memoryviews can outlive the Python `frame` handle.
5219    unsafe fn __getbuffer__(
5220        slf: PyRef<'_, Self>,
5221        view: *mut pyo3::ffi::Py_buffer,
5222        flags: std::os::raw::c_int,
5223    ) -> PyResult<()> {
5224        let r = unsafe {
5225            pyo3::ffi::PyBuffer_FillInfo(
5226                view,
5227                slf.as_ptr(),
5228                slf.data.as_ptr() as *mut std::os::raw::c_void,
5229                slf.data.len() as pyo3::ffi::Py_ssize_t,
5230                1,
5231                flags,
5232            )
5233        };
5234        if r != 0 {
5235            return Err(PyErr::fetch(slf.py()));
5236        }
5237        Ok(())
5238    }
5239
5240    unsafe fn __releasebuffer__(&self, _view: *mut pyo3::ffi::Py_buffer) {}
5241}
5242
5243/// The Python handle to the one long-lived compositor thread. Because calloop, EGL/GBM, and
5244/// the Wayland display are all thread-affine and the compositor has to keep running to serve its
5245/// clients even between captures, the backend cannot be a passive object that starts work on demand:
5246/// constructing it spawns that thread and it stays up for the process lifetime. The struct itself is
5247/// nothing but the command-channel sender used to drive that thread across the boundary.
5248#[pyclass]
5249struct WaylandBackend {
5250    tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
5251    /// Wakes the calloop after each command send: the command channel itself is drained in
5252    /// place by the compositor thread (render tick and wake handler), not registered as its
5253    /// own source, so input never waits behind an in-flight render tick's timer wakeup.
5254    wake_tx: smithay::reexports::calloop::channel::Sender<()>,
5255}
5256
5257impl WaylandBackend {
5258    fn send(&self, cmd: ThreadCommand) -> Result<(), String> {
5259        self.tx.send(cmd).map_err(|e| e.to_string())?;
5260        let _ = self.wake_tx.send(());
5261        Ok(())
5262    }
5263}
5264
5265#[pymethods]
5266impl WaylandBackend {
5267    /// Construct the backend and spawn the long-lived compositor thread, handing it the
5268    /// strongest scheduling edge (nice -15) because it drives the calloop, input dispatch, the render
5269    /// loop, and — on the zero-copy path — the encode itself, all on this single thread, so any
5270    /// scheduling starvation here surfaces directly as dropped or late frames.
5271    #[new]
5272    #[pyo3(signature = (width, height, dri_node, auto_gpu_selected = false, cursor_size = -1))]
5273    fn new(
5274        width: i32,
5275        height: i32,
5276        dri_node: String,
5277        auto_gpu_selected: bool,
5278        cursor_size: i32,
5279    ) -> Self {
5280        let (tx, rx) = smithay::reexports::calloop::channel::channel();
5281        let (wake_tx, wake_rx) = smithay::reexports::calloop::channel::channel();
5282        let cu_tx = tx.clone();
5283        thread::spawn(move || {
5284            crate::boost_thread_priority(-15);
5285            run_wayland_thread(WaylandThreadConfig {
5286                command_rx: rx,
5287                wake_rx,
5288                command_tx: cu_tx,
5289                initial_width: width,
5290                initial_height: height,
5291                explicit_dri_node: dri_node,
5292                auto_gpu_selected,
5293                cursor_size,
5294            });
5295        });
5296        WaylandBackend { tx, wake_tx }
5297    }
5298
5299    /// Begin a capture with the given frame callback and settings. The target display is
5300    /// the settings' `display_id` attribute (absent = 0, the primary); each display id runs
5301    /// at most one capture, independent of every other display's.
5302    ///
5303    /// Issuing the start also clears the interpreter-teardown gate: starting from Python proves the
5304    /// interpreter is live again after a manual atexit sweep.
5305    fn start_capture(&self, callback: Py<PyAny>, settings: &Bound<'_, PyAny>) -> PyResult<()> {
5306        let rust_settings = extract_settings(settings)?;
5307        let display_id = read_display_id(settings);
5308
5309        PY_SHUTDOWN.store(false, Ordering::Relaxed);
5310        self.send(ThreadCommand::StartCapture { display_id, callback: Some(callback), settings: rust_settings })
5311            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to send start command: {}", e)))?;
5312        Ok(())
5313    }
5314
5315    /// Stop the capture bound to `display_id` (default: the primary display).
5316    #[pyo3(signature = (display_id = 0))]
5317    fn stop_capture(&self, display_id: u32) -> PyResult<()> {
5318        self.send(ThreadCommand::StopCapture { display_id })
5319            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to send stop command: {}", e)))?;
5320        Ok(())
5321    }
5322
5323    /// Create an additional output (`WxH` physical pixels at fractional `scale`) mapped into
5324    /// the layout at offset `(x, y)`; `id` is the display key used by every per-display API.
5325    /// False when the id is taken, the geometry/scale is invalid, the rectangle overlaps a
5326    /// live output, or the GPU render target cannot be allocated. Capture reconfigures
5327    /// (`start_capture` on an existing output) resize without this validation, so a
5328    /// multi-step relayout must stay overlap-free at every step by caller ordering.
5329    // The parameter list is the Python signature; grouping it would change the ABI.
5330    #[allow(clippy::too_many_arguments)]
5331    #[pyo3(signature = (id, width, height, x = 0, y = 0, scale = 1.0))]
5332    fn create_output(
5333        &self,
5334        py: Python<'_>,
5335        id: u32,
5336        width: i32,
5337        height: i32,
5338        x: i32,
5339        y: i32,
5340        scale: f64,
5341    ) -> PyResult<bool> {
5342        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5343        self.send(ThreadCommand::CreateOutput { id, width, height, x, y, scale, reply: reply_tx })
5344            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to create output: {}", e)))?;
5345        Ok(py
5346            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5347            .unwrap_or(false))
5348    }
5349
5350    /// Destroy a secondary output: its capture ends cleanly and its windows relocate to the
5351    /// primary output. False for the primary (id 0) or an unknown id.
5352    fn destroy_output(&self, py: Python<'_>, id: u32) -> PyResult<bool> {
5353        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5354        self.send(ThreadCommand::DestroyOutput { id, reply: reply_tx })
5355            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to destroy output: {}", e)))?;
5356        Ok(py
5357            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5358            .unwrap_or(false))
5359    }
5360
5361    /// Move an existing output (the primary, id 0, included) to layout offset `(x, y)`;
5362    /// its windows, absolute input injection, and cursor compositing follow. False for an
5363    /// unknown id or a destination overlapping a live output. Capture reconfigures
5364    /// (`start_capture` on an existing output) resize without this validation, so a
5365    /// multi-step relayout must stay overlap-free at every step by caller ordering.
5366    fn reposition_output(&self, py: Python<'_>, id: u32, x: i32, y: i32) -> PyResult<bool> {
5367        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5368        self.send(ThreadCommand::RepositionOutput { id, x, y, reply: reply_tx })
5369            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to reposition output: {}", e)))?;
5370        Ok(py
5371            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5372            .unwrap_or(false))
5373    }
5374
5375    /// Every live output as `(id, x, y, width, height, scale, capturing)` — width/height in
5376    /// physical pixels, `(x, y)` the layout offset.
5377    fn list_outputs(&self, py: Python<'_>) -> PyResult<Vec<OutputDesc>> {
5378        let (reply_tx, reply_rx) = std::sync::mpsc::channel();
5379        self.send(ThreadCommand::ListOutputs { reply: reply_tx })
5380            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to list outputs: {}", e)))?;
5381        Ok(py
5382            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5383            .unwrap_or_default())
5384    }
5385
5386    /// How many displays this backend can back with real content: -1 when
5387    /// self-compositing (outputs are created on demand, no fixed bound), otherwise the
5388    /// host compositor's output count. Host-capture mode reports -1 until the first
5389    /// capture start establishes the host session; 0 when the backend is unresponsive.
5390    fn output_capacity(&self, py: Python<'_>) -> PyResult<i64> {
5391        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<i64>();
5392        self.send(ThreadCommand::OutputCapacity { reply: reply_tx })
5393            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to query output capacity: {}", e)))?;
5394        Ok(py
5395            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5396            .unwrap_or(0))
5397    }
5398
5399    /// Move the window with the given id onto output `output_id`, fullscreened at that
5400    /// output's logical size. False for an unknown window or output id.
5401    fn move_window_to_output(&self, py: Python<'_>, window_id: u32, output_id: u32) -> PyResult<bool> {
5402        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5403        self.send(ThreadCommand::MoveWindowToOutput { window_id, output_id, reply: reply_tx })
5404            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to move window: {}", e)))?;
5405        Ok(py
5406            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5407            .unwrap_or(false))
5408    }
5409
5410    /// Every mapped window as `(window_id, title, app_id, output_id, waiting)`. A waiting
5411    /// window is tagged for that output but mapped clear of every one of them, holding its
5412    /// size until an output exists for it — a nested session's spare screens.
5413    fn list_windows(&self, py: Python<'_>) -> PyResult<Vec<WindowDesc>> {
5414        let (reply_tx, reply_rx) = std::sync::mpsc::channel();
5415        self.send(ThreadCommand::ListWindows { reply: reply_tx })
5416            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to list windows: {}", e)))?;
5417        Ok(py
5418            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5419            .unwrap_or_default())
5420    }
5421
5422    fn set_cursor_callback(&self, callback: Py<PyAny>) -> PyResult<()> {
5423        self.send(ThreadCommand::SetCursorCallback(callback))
5424            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set cursor callback: {}", e)))?;
5425        Ok(())
5426    }
5427
5428    /// Recreate the cursor theme at `size` pixels — no restart: subsequent named-cursor
5429    /// callbacks and the burned-in cursor overlay render at the new size. False for a
5430    /// non-positive size.
5431    fn set_cursor_size(&self, py: Python<'_>, size: i32) -> PyResult<bool> {
5432        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5433        self.send(ThreadCommand::SetCursorSize { size, reply: reply_tx })
5434            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set cursor size: {}", e)))?;
5435        Ok(py
5436            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5437            .unwrap_or(false))
5438    }
5439
5440    /// cb(mime: str, data: bytes) fires when a client app copies to the clipboard.
5441    fn set_clipboard_callback(&self, callback: Py<PyAny>) -> PyResult<()> {
5442        self.send(ThreadCommand::SetClipboardCallback(callback))
5443            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set clipboard callback: {}", e)))?;
5444        Ok(())
5445    }
5446
5447    /// Compositor-side clipboard offer: serve `data` as `mime` to pasting clients.
5448    fn set_clipboard(&self, mime: String, data: Vec<u8>) -> PyResult<()> {
5449        self.send(ThreadCommand::SetClipboard { mime, data })
5450            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set clipboard: {}", e)))?;
5451        Ok(())
5452    }
5453
5454    fn inject_key(&self, scancode: u32, state: u32) -> PyResult<()> {
5455        self.send(ThreadCommand::KeyboardKey { scancode, state })
5456            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject key: {}", e)))?;
5457        Ok(())
5458    }
5459
5460    /// Inject an ordered run of `(keycode, state)` events as one message, so a paste
5461    /// costs one channel send and one wake instead of one per event.
5462    fn inject_keys(&self, events: Vec<(u32, u32)>) -> PyResult<()> {
5463        if events.is_empty() {
5464            return Ok(());
5465        }
5466        self.send(ThreadCommand::KeyboardKeys { events })
5467            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject keys: {}", e)))?;
5468        Ok(())
5469    }
5470
5471    /// Swap the seat keyboard's xkb keymap (XKB_KEYMAP_FORMAT_TEXT_V1 text). The caller
5472    /// owns keysym-to-keycode policy: define keycodes here, then press them via
5473    /// `inject_key`. Ordered with key events on the one compositor channel.
5474    /// Bind explicit `(keycode, keysym)` pairs onto the current base keymap in ONE swap.
5475    ///
5476    /// The caller owns the assignment — which keysym goes to which keycode, and when to
5477    /// recycle one — because that tracks layouts and user reports. This end only assembles
5478    /// the xkb text and delivers it, reusing the installed base rather than recompiling a
5479    /// re-supplied one. False when no base keymap is installed yet.
5480    fn set_keymap_overlay(&self, binds: Vec<(u32, u32)>) -> PyResult<()> {
5481        self.send(ThreadCommand::SetKeymapOverlay { binds })
5482            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set overlay: {}", e)))?;
5483        Ok(())
5484    }
5485
5486    fn set_keymap_string(&self, text: String) -> PyResult<()> {
5487        self.send(ThreadCommand::SetKeymapString(text))
5488            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set keymap: {}", e)))?;
5489        Ok(())
5490    }
5491
5492    /// Return the active xkb keymap as an XKB_KEYMAP_FORMAT_TEXT_V1 string so a consumer can
5493    /// build a reverse keysym->keycode map from the identical keymap.
5494    ///
5495    /// The GIL is released while awaiting the reply, because the Wayland thread can call back into
5496    /// Python and would otherwise deadlock; the wait is bounded so a stall cannot hang the caller,
5497    /// and an empty string is returned when the keymap cannot be read in time.
5498    fn get_xkb_keymap_string(&self, py: Python<'_>) -> PyResult<String> {
5499        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<String>();
5500        self.send(ThreadCommand::GetXkbKeymap { reply: reply_tx })
5501            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to request keymap: {}", e)))?;
5502        let result = py.detach(move || reply_rx.recv_timeout(Duration::from_secs(2)));
5503        match result {
5504            Ok(s) => Ok(s),
5505            Err(_) => Ok(String::new()),
5506        }
5507    }
5508
5509    fn inject_mouse_move(&self, x: f64, y: f64) -> PyResult<()> {
5510        self.send(ThreadCommand::PointerMotion { x, y })
5511            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject motion: {}", e)))?;
5512        Ok(())
5513    }
5514
5515    fn inject_relative_mouse_move(&self, dx: f64, dy: f64) -> PyResult<()> {
5516        self.send(ThreadCommand::PointerRelativeMotion { dx, dy })
5517            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject relative motion: {}", e)))?;
5518        Ok(())
5519    }
5520
5521    fn inject_mouse_button(&self, btn: u32, state: u32) -> PyResult<()> {
5522        self.send(ThreadCommand::PointerButton { btn, state })
5523            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject button: {}", e)))?;
5524        Ok(())
5525    }
5526
5527    fn inject_mouse_scroll(&self, x: f64, y: f64) -> PyResult<()> {
5528        self.send(ThreadCommand::PointerAxis { x, y })
5529            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject axis: {}", e)))?;
5530        Ok(())
5531    }
5532
5533    fn set_cursor_rendering(&self, enabled: bool) -> PyResult<()> {
5534        self.send(ThreadCommand::UpdateCursorConfig { render_on_framebuffer: enabled })
5535            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set cursor config: {}", e)))?;
5536        Ok(())
5537    }
5538
5539    /// Forces an IDR/keyframe on the next captured frame so a (re)connecting client
5540    /// or a decoder reset can resume immediately. With the default infinite GOP this
5541    /// is the only recovery path, so every consumer that can lose decoder state must
5542    /// call it. No-op cost on the JPEG/software path (keyframes are N/A).
5543    #[pyo3(signature = (display_id = 0))]
5544    fn request_idr_frame(&self, display_id: u32) -> PyResult<()> {
5545        self.send(ThreadCommand::RequestIdr { display_id })
5546            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to request IDR: {}", e)))?;
5547        Ok(())
5548    }
5549
5550    /// Apply a live bitrate (kbps) / VBV (kb) / framerate change to the given display's
5551    /// running capture.
5552    #[pyo3(signature = (bitrate_kbps = None, vbv_multiplier = None, fps = None, display_id = 0))]
5553    fn update_rate(&self, bitrate_kbps: Option<i32>, vbv_multiplier: Option<f64>, fps: Option<f64>, display_id: u32) -> PyResult<()> {
5554        self.send(ThreadCommand::UpdateRate { display_id, bitrate_kbps, vbv_multiplier, fps })
5555            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to update rate: {}", e)))?;
5556        Ok(())
5557    }
5558
5559    /// Set the seat's BASE xkb layout from RMLVO names at runtime (empty strings select the
5560    /// xkbcommon defaults). Returns whether the layout compiled and was applied; overlay binds
5561    /// rebuild on top with their keycodes unchanged.
5562    #[pyo3(signature = (layout, variant = String::new(), options = String::new(), model = String::new(), rules = String::new()))]
5563    fn set_xkb_layout(
5564        &self,
5565        py: Python<'_>,
5566        layout: String,
5567        variant: String,
5568        options: String,
5569        model: String,
5570        rules: String,
5571    ) -> PyResult<bool> {
5572        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5573        self.send(ThreadCommand::SetXkbLayout { rules, model, layout, variant, options, reply: reply_tx })
5574            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set layout: {}", e)))?;
5575        Ok(py
5576            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5577            .unwrap_or(false))
5578    }
5579
5580    /// Debug/verification readback of the seat keyboard: `(pressed_keycodes, modifier_mask)`
5581    /// with mask bits 1 ctrl, 2 shift, 4 alt, 8 logo, 16 caps, 32 num, 64 altgr, 128 level5.
5582    fn get_keyboard_state(&self, py: Python<'_>) -> PyResult<(Vec<u32>, u32)> {
5583        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<(Vec<u32>, u32)>();
5584        self.send(ThreadCommand::GetKeyboardState { reply: reply_tx })
5585            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to read keyboard state: {}", e)))?;
5586        Ok(py
5587            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5588            .unwrap_or_default())
5589    }
5590
5591    /// The capture geometry actually live on the given display: `(width, height, scale)` in
5592    /// physical pixels, or `None` when the compositor did not answer in time. Reflects any
5593    /// degrade a `start_capture` performed (H.264 even-masking, GBM allocation failure keeping
5594    /// the previous mode, a host that kept its own mode), and the command channel is FIFO, so
5595    /// calling this after `start_capture` returns what that start realized. A host-capture
5596    /// start answers once the host has ruled on the requested mode (the compositor thread
5597    /// keeps serving input meanwhile), so a slow-but-live start reads its real size instead
5598    /// of being reported as a timeout; the `None` a genuine timeout returns lets the caller
5599    /// treat the geometry as unknown, not as "nothing to reconcile", which a `(0, 0, 0.0)`
5600    /// sentinel could not.
5601    #[pyo3(signature = (display_id = 0))]
5602    fn get_realized_geometry(
5603        &self,
5604        py: Python<'_>,
5605        display_id: u32,
5606    ) -> PyResult<Option<(i32, i32, f64)>> {
5607        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<(i32, i32, f64)>();
5608        self.send(ThreadCommand::CuGetInfo { display_id, resp: reply_tx })
5609            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to read geometry: {}", e)))?;
5610        Ok(py
5611            .detach(move || reply_rx.recv_timeout(GEOMETRY_BARRIER_TIMEOUT))
5612            .ok())
5613    }
5614
5615    /// Lifecycle of `display_id`'s capture as `(state, last_error)`: `state` is `"running"`
5616    /// (pipeline live), `"failed"` (start left no live pipeline) or `"idle"` (never started
5617    /// / stopped clean); `last_error` is the reason a start failed, or a caveat a live
5618    /// capture came up with (host connect refused -> local compositing, a hardware encoder
5619    /// that fell back to CPU, a refused resize). Read straight from the recorded outcome, so
5620    /// pair it with a prior `get_realized_geometry` when the ordering after a start matters.
5621    #[pyo3(signature = (display_id = 0))]
5622    fn capture_state(&self, display_id: u32) -> (String, Option<String>) {
5623        wayland_capture_state(display_id)
5624    }
5625}
5626
5627/// Bound on the geometry read-back barrier. Behind a host-capture start the read is held
5628/// until the host has ruled on the requested mode, which the host's control thread bounds
5629/// by `LAYOUT_DEADLINE`, so this clears that with margin or a live-but-slow start would read
5630/// as a timeout.
5631const GEOMETRY_BARRIER_TIMEOUT: Duration = Duration::from_secs(6);
5632
5633/// `(state, last_error)` for `display_id`'s Wayland capture, from the shared liveness and
5634/// outcome maps (no command round-trip).
5635fn wayland_capture_state(display_id: u32) -> (String, Option<String>) {
5636    let running = wayland_alive().lock().unwrap().contains(&display_id);
5637    let last_error = wayland_capture_err().lock().unwrap().get(&display_id).cloned();
5638    let state = if running {
5639        "running"
5640    } else if last_error.is_some() {
5641        "failed"
5642    } else {
5643        "idle"
5644    };
5645    (state.to_string(), last_error)
5646}
5647
5648impl WaylandBackend {
5649    fn update_tunables(&self, display_id: u32, t: LiveTunables) -> PyResult<()> {
5650        self.send(ThreadCommand::UpdateTunables { display_id, tunables: t })
5651            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to update tunables: {}", e)))?;
5652        Ok(())
5653    }
5654}
5655
5656/// The optional `display_id` attribute on a settings object (absent/invalid = 0, the
5657/// primary display).
5658fn read_display_id(settings: &Bound<'_, PyAny>) -> u32 {
5659    settings
5660        .getattr("display_id")
5661        .ok()
5662        .and_then(|v| v.extract::<u32>().ok())
5663        .unwrap_or(0)
5664}
5665
5666use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering};
5667use std::sync::{Condvar, Mutex, OnceLock};
5668
5669use crate::encoders::software::EncodedStripe;
5670
5671/// Let Python wrap already-encoded bytes back into a `StripeFrame`, for callers that produce or
5672/// replay stripe data outside a live capture (tests, re-sends to a late joiner). It copies the
5673/// buffer-like input (bytes/bytearray/memoryview) in because the frame owns its bytes; the capture
5674/// hot path instead uses `new_owned_meta` to MOVE the encoder's buffer with no copy, so this
5675/// copying constructor stays off that path.
5676#[pyfunction]
5677#[pyo3(signature = (data, data_type = 0, stripe_y_start = 0, stripe_height = 0, frame_id = 0))]
5678fn stripe_frame_from_buffer(
5679    data: Vec<u8>,
5680    data_type: i32,
5681    stripe_y_start: i32,
5682    stripe_height: i32,
5683    frame_id: i32,
5684) -> StripeFrame {
5685    StripeFrame::new_owned_meta(Arc::new(data), data_type, stripe_y_start, stripe_height, frame_id)
5686}
5687
5688/// Capture configuration read by `start_capture` (each field by attribute name via
5689/// `extract_settings`, so the field names must match exactly). Declared `dict` so callers
5690/// can stash extra attributes not listed here.
5691#[pyclass(dict)]
5692struct CaptureSettings {
5693    /// Wayland display key this capture binds to (0 = primary output); ignored on X11.
5694    #[pyo3(get, set)] display_id: u32,
5695    #[pyo3(get, set)] capture_width: i32,
5696    #[pyo3(get, set)] capture_height: i32,
5697    #[pyo3(get, set)] scale: f64,
5698    #[pyo3(get, set)] capture_x: i32,
5699    #[pyo3(get, set)] capture_y: i32,
5700    #[pyo3(get, set)] target_fps: f64,
5701    #[pyo3(get, set)] jpeg_quality: i32,
5702    #[pyo3(get, set)] paint_over_jpeg_quality: i32,
5703    #[pyo3(get, set)] use_paint_over_quality: bool,
5704    #[pyo3(get, set)] paint_over_trigger_frames: i32,
5705    #[pyo3(get, set)] damage_block_threshold: i32,
5706    #[pyo3(get, set)] damage_block_duration: i32,
5707    #[pyo3(get, set)] output_mode: i32,
5708    #[pyo3(get, set)] video_crf: i32,
5709    #[pyo3(get, set)] video_paintover_crf: i32,
5710    #[pyo3(get, set)] video_paintover_burst_frames: i32,
5711    #[pyo3(get, set)] video_fullcolor: bool,
5712    #[pyo3(get, set)] video_fullframe: bool,
5713    #[pyo3(get, set)] video_streaming_mode: bool,
5714    #[pyo3(get, set)] capture_cursor: bool,
5715    #[pyo3(get, set)] watermark_path: Py<PyAny>,
5716    #[pyo3(get, set)] watermark_location_enum: i32,
5717    #[pyo3(get, set)] encode_node_index: i32,
5718    #[pyo3(get, set)] use_cpu: bool,
5719    #[pyo3(get, set)] debug_logging: bool,
5720    #[pyo3(get, set)] video_cbr_mode: bool,
5721    #[pyo3(get, set)] video_bitrate_kbps: i32,
5722    #[pyo3(get, set)] video_vbv_multiplier: f64,
5723    #[pyo3(get, set)] keyframe_interval_s: f64,
5724    #[pyo3(get, set)] video_min_qp: i32,
5725    #[pyo3(get, set)] video_max_qp: i32,
5726    #[pyo3(get, set)] auto_adjust_screen_capture_size: bool,
5727    #[pyo3(get, set)] omit_stripe_headers: bool,
5728    #[pyo3(get, set)] encode_node_path: Py<PyAny>,
5729    /// Compositor render node (Wayland): an explicit path wins; empty with auto_gpu
5730    /// set lets the library pick one; empty without falls back to the encoder node.
5731    #[pyo3(get, set)] render_node_path: Py<PyAny>,
5732    /// Auto-GPU request: "" = off, "true" = first GPU, any other token = first GPU
5733    /// whose kernel identity matches (vendor name, driver name, DT prefix, PCI id).
5734    #[pyo3(get, set)] auto_gpu: Py<PyAny>,
5735    /// Backend choice: True/False force Wayland/X11; None follows WAYLAND_DISPLAY.
5736    #[pyo3(get, set)] use_wayland: Py<PyAny>,
5737    /// H.264 recording tap: a Unix socket path to bind, or empty for none.
5738    #[pyo3(get, set)] recording_socket: Py<PyAny>,
5739    /// Wayland display of an EXTERNAL compositor to capture (host-capture mode).
5740    #[pyo3(get, set)] wayland_host_display: Py<PyAny>,
5741    /// Compositor cursor-theme size in pixels; <=0 keeps the theme default (24).
5742    #[pyo3(get, set)] cursor_size: i32,
5743    /// Longest cursor edge the X11 out-of-band cursor callback delivers; larger
5744    /// images are downscaled. <=0 disables the cap.
5745    #[pyo3(get, set)] cursor_size_cap: i32,
5746}
5747
5748#[pymethods]
5749impl CaptureSettings {
5750    #[new]
5751    fn new(py: Python<'_>) -> Self {
5752        Self {
5753            display_id: 0,
5754            capture_width: 1920, capture_height: 1080, scale: 1.0, capture_x: 0, capture_y: 0,
5755            target_fps: 60.0, jpeg_quality: 85, paint_over_jpeg_quality: 95,
5756            use_paint_over_quality: false, paint_over_trigger_frames: 10,
5757            damage_block_threshold: 15, damage_block_duration: 30, output_mode: 0,
5758            video_crf: 25, video_paintover_crf: 18, video_paintover_burst_frames: 5,
5759            video_fullcolor: false, video_fullframe: false, video_streaming_mode: false,
5760            capture_cursor: false, watermark_path: py.None(), watermark_location_enum: 0,
5761            encode_node_index: -2, use_cpu: false, debug_logging: false,
5762            video_cbr_mode: false, video_bitrate_kbps: 4000, video_vbv_multiplier: 0.0,
5763            keyframe_interval_s: 0.0,
5764            video_min_qp: 0, video_max_qp: 0,
5765            auto_adjust_screen_capture_size: false, omit_stripe_headers: false,
5766            encode_node_path: py.None(),
5767            render_node_path: py.None(), auto_gpu: py.None(), use_wayland: py.None(),
5768            recording_socket: py.None(), wayland_host_display: py.None(),
5769            cursor_size: -1, cursor_size_cap: 32,
5770        }
5771    }
5772}
5773
5774/// Process-wide Wayland backend: input and capture share ONE compositor (constructed lazily).
5775static WAYLAND_BACKEND: OnceLock<Mutex<Option<Py<WaylandBackend>>>> = OnceLock::new();
5776/// The compositor's auto-picked socket name (e.g. "wayland-1"), published by the compositor
5777/// thread once its listening socket exists. `ListeningSocketSource::new_auto` binds the first
5778/// FREE wayland-N, which need not match any configured index — consumers must read the real
5779/// name from here instead of assuming one.
5780static WAYLAND_SOCKET_NAME: Mutex<Option<String>> = Mutex::new(None);
5781static WAYLAND_SOCKET_CV: Condvar = Condvar::new();
5782
5783fn publish_socket_name(name: &str) {
5784    *WAYLAND_SOCKET_NAME.lock().unwrap() = Some(name.to_string());
5785    WAYLAND_SOCKET_CV.notify_all();
5786}
5787
5788/// Wait (bounded) for the compositor thread to publish its socket name.
5789fn wait_socket_name(timeout: Duration) -> Option<String> {
5790    let deadline = Instant::now() + timeout;
5791    let mut g = WAYLAND_SOCKET_NAME.lock().unwrap();
5792    loop {
5793        if let Some(name) = g.as_ref() {
5794            return Some(name.clone());
5795        }
5796        let now = Instant::now();
5797        if now >= deadline {
5798            return None;
5799        }
5800        let (gg, _) = WAYLAND_SOCKET_CV.wait_timeout(g, deadline - now).unwrap();
5801        g = gg;
5802    }
5803}
5804/// Cursor callback registered before the backend exists (selkies registers it pre-start);
5805/// applied when the backend is created, which is deferred to capture start so the real
5806/// render node (not a placeholder) reaches the compositor.
5807static PENDING_CURSOR_CALLBACK: Mutex<Option<Py<PyAny>>> = Mutex::new(None);
5808/// Interpreter-teardown gate, set by the atexit sweep: the detached compositor and delivery
5809/// threads must never attach to a finalizing interpreter (aborts the process pre-3.13).
5810/// Cleared by a fresh capture start (only a live interpreter can start one).
5811pub(crate) static PY_SHUTDOWN: AtomicBool = AtomicBool::new(false);
5812/// Per-display capture ownership: display id -> the ScreenCapture id that owns that
5813/// display's capture. Only the owner may stop it, so an input-only or stale instance can't
5814/// tear down a live capture.
5815static WAYLAND_OWNERS: OnceLock<Mutex<std::collections::HashMap<u32, u64>>> = OnceLock::new();
5816
5817fn wayland_owners() -> &'static Mutex<std::collections::HashMap<u32, u64>> {
5818    WAYLAND_OWNERS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
5819}
5820
5821/// Display ids whose capture pipeline is actually running (StartCapture inserts,
5822/// StopCapture/DestroyOutput remove), so the Python-facing is_capturing() reports pipeline
5823/// liveness, not merely which ScreenCapture owns the backend.
5824static WAYLAND_ALIVE_DISPLAYS: OnceLock<Mutex<std::collections::HashSet<u32>>> = OnceLock::new();
5825
5826fn wayland_alive() -> &'static Mutex<std::collections::HashSet<u32>> {
5827    WAYLAND_ALIVE_DISPLAYS.get_or_init(|| Mutex::new(std::collections::HashSet::new()))
5828}
5829/// Per-display outcome of the most recent capture start, recorded by the calloop thread
5830/// so the Python-facing `capture_state` can report it. A start that came up clean removes
5831/// its entry; a hard failure (no output, ...) or a caveat that still capturing (host
5832/// connect failed -> local compositing, hardware encoder fell back to CPU) leaves the
5833/// reason here. Distinct from `wayland_alive`, which says only whether the pipeline runs.
5834static WAYLAND_CAPTURE_ERR: OnceLock<Mutex<std::collections::HashMap<u32, String>>> =
5835    OnceLock::new();
5836
5837fn wayland_capture_err() -> &'static Mutex<std::collections::HashMap<u32, String>> {
5838    WAYLAND_CAPTURE_ERR.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
5839}
5840
5841/// Record (or clear, with `None`) the last-start outcome for `display_id`.
5842fn set_wayland_capture_err(display_id: u32, err: Option<String>) {
5843    let mut map = wayland_capture_err().lock().unwrap();
5844    match err {
5845        Some(e) => {
5846            map.insert(display_id, e);
5847        }
5848        None => {
5849            map.remove(&display_id);
5850        }
5851    }
5852}
5853/// Hands each `ScreenCapture` a unique, monotonic id — the token `WAYLAND_OWNERS` compares to
5854/// decide which instance is allowed to stop a display's capture. Starts at 1 so 0 can
5855/// mean "no owner".
5856static NEXT_CAPTURE_ID: AtomicU64 = AtomicU64::new(1);
5857/// Registry of every live X11 capture's `Controls`, so the atexit sweep can flag them all to
5858/// stop before the interpreter finalizes even after the owning `ScreenCapture` Python handles have
5859/// been dropped — without a central registry those captures would have no reachable stop switch.
5860static LIVE_X11: OnceLock<Mutex<Vec<Arc<crate::x11::Controls>>>> = OnceLock::new();
5861
5862fn live_x11() -> &'static Mutex<Vec<Arc<crate::x11::Controls>>> {
5863    LIVE_X11.get_or_init(|| Mutex::new(Vec::new()))
5864}
5865
5866/// Convert premultiplied-alpha RGBA pixels to the straight alpha PNG expects. Every cursor
5867/// source feeding the callback stores premultiplied color (XFixes and Xcursor by format
5868/// definition, wl_shm/dmabuf by Wayland convention); encoding those values as straight
5869/// alpha double-darkens antialiased edges. Rounds as `(c * 255 + a/2) / a`, clamped —
5870/// selkies' python seed path mirrors this exact integer math so both sources hash a cursor
5871/// to the same content handle.
5872pub(crate) fn unpremultiply_rgba(image: &mut image::RgbaImage) {
5873    for p in image.pixels_mut() {
5874        let a = p.0[3] as u32;
5875        if a == 0 {
5876            p.0 = [0, 0, 0, 0];
5877        } else if a < 255 {
5878            for c in &mut p.0[..3] {
5879                *c = ((*c as u32 * 255 + a / 2) / a).min(255) as u8;
5880            }
5881        }
5882    }
5883}
5884
5885/// Best-effort nice boost for the calling capture/encode/delivery thread. These threads
5886/// compete with the very workload being captured, so a scheduling edge keeps frame pacing
5887/// steady under load. Requires CAP_SYS_NICE (or root); otherwise EPERM and silently a no-op.
5888pub(crate) fn boost_thread_priority(nice: libc::c_int) {
5889    unsafe {
5890        let tid = libc::syscall(libc::SYS_gettid) as libc::id_t;
5891        let _ = libc::setpriority(libc::PRIO_PROCESS, tid, nice);
5892    }
5893}
5894
5895/// Forward a live rate change to the shared Wayland backend (no-op if none is running).
5896fn wayland_update_rate(
5897    py: Python<'_>,
5898    display_id: u32,
5899    bitrate_kbps: Option<i32>,
5900    vbv_multiplier: Option<f64>,
5901    fps: Option<f64>,
5902) {
5903    if let Some(slot) = WAYLAND_BACKEND.get()
5904        && let Some(be) = slot.lock().unwrap().as_ref() {
5905            let _ = be.bind(py).borrow().update_rate(bitrate_kbps, vbv_multiplier, fps, display_id);
5906        }
5907}
5908
5909/// Forward live per-frame tunables to the shared Wayland backend (no-op if none is running).
5910fn wayland_update_tunables(py: Python<'_>, display_id: u32, t: LiveTunables) {
5911    if let Some(slot) = WAYLAND_BACKEND.get()
5912        && let Some(be) = slot.lock().unwrap().as_ref() {
5913            let _ = be.bind(py).borrow().update_tunables(display_id, t);
5914        }
5915}
5916
5917/// Get-or-create the singleton Wayland backend (idempotent: the first dimensions and render
5918/// node win, and a later capture just resizes).
5919///
5920/// Called from capture start (which knows the operator's real node) and from the import-time
5921/// bootstrap when the deployment opts into pixelflux-as-compositor. The render node is chosen once,
5922/// at creation, by precedence: an explicit `render_node_path`, then an `auto_gpu` pick, then the
5923/// encoder node (so a caller that sets only one node still renders on that GPU); empty selects the
5924/// software renderer.
5925fn ensure_wayland_backend(
5926    py: Python<'_>,
5927    width: i32,
5928    height: i32,
5929    explicit_node: String,
5930    auto_gpu: String,
5931    fallback_node: String,
5932    cursor_size: i32,
5933) -> PyResult<Py<WaylandBackend>> {
5934    let slot = WAYLAND_BACKEND.get_or_init(|| Mutex::new(None));
5935    let mut g = slot.lock().unwrap();
5936    if g.is_none() {
5937        let mut node = (!explicit_node.is_empty()).then_some(explicit_node);
5938        let mut auto_gpu_selected = false;
5939        if node.is_none()
5940            && let Some(request) = parse_auto_gpu(&auto_gpu) {
5941                match auto_select_render_node(request.as_deref()) {
5942                    Some(picked) => {
5943                        println!("[Wayland] AUTO_GPU enabled. Selected: {}", picked);
5944                        node = Some(picked);
5945                        auto_gpu_selected = true;
5946                    }
5947                    None => {
5948                        if let Some(token) = request {
5949                            eprintln!("[pixelflux] AUTO_GPU={token}: no matching GPU found.");
5950                        }
5951                    }
5952                }
5953            }
5954        let node = node.unwrap_or(fallback_node);
5955        let be = Py::new(
5956            py,
5957            WaylandBackend::new(width, height, node, auto_gpu_selected, cursor_size),
5958        )?;
5959        if let Some(cb) = PENDING_CURSOR_CALLBACK.lock().unwrap().take() {
5960            let _ = be.bind(py).borrow().set_cursor_callback(cb);
5961        }
5962        *g = Some(be);
5963    }
5964    Ok(g.as_ref().unwrap().clone_ref(py))
5965}
5966
5967/// The live Wayland backend, if any — never creates one. The pre-capture entry points
5968/// (input injection, cursor/config setters) use this so they can't lock in a backend
5969/// with a placeholder render node.
5970fn wayland_backend_running(py: Python<'_>) -> Option<Py<WaylandBackend>> {
5971    let slot = WAYLAND_BACKEND.get()?;
5972    let g = slot.lock().unwrap();
5973    g.as_ref().map(|b| b.clone_ref(py))
5974}
5975
5976/// Backend choice: an explicit `use_wayland` bool in the settings wins (selkies
5977/// forwards --wayland / SELKIES_WAYLAND there); when left unset (None), capture
5978/// goes through Wayland exactly when the session exposes a WAYLAND_DISPLAY.
5979fn want_wayland(settings: &Bound<'_, PyAny>) -> bool {
5980    if let Some(explicit) = settings
5981        .getattr("use_wayland")
5982        .ok()
5983        .and_then(|v| v.extract::<bool>().ok())
5984    {
5985        return explicit;
5986    }
5987    std::env::var("WAYLAND_DISPLAY").map(|v| !v.is_empty()).unwrap_or(false)
5988}
5989
5990/// Mutable per-capture state behind `ScreenCapture`'s mutex: the active backend, the live
5991/// X11 controls and thread handle, and the capture / encode thread ids used to detect a re-entrant
5992/// stop.
5993struct ScState {
5994    /// 0 = idle, 1 = X11, 2 = Wayland.
5995    backend: u8,
5996    /// This capture holds one reference on the shared X11 cursor monitor. Set in
5997    /// the same locked section as `backend = 1` and TAKEN in the same locked
5998    /// section a stop reads the backend, so acquire/release pair exactly per
5999    /// capture — inferring the reference from `backend` alone would let a stop
6000    /// that interleaves with a start release a reference not yet taken (leaking
6001    /// the monitor once the acquire lands). The GIL happens to serialize that
6002    /// window today; the pairing must not depend on it.
6003    cursor_ref: bool,
6004    controls: Option<Arc<crate::x11::Controls>>,
6005    handle: Option<thread::JoinHandle<()>>,
6006    cap_thread_id: Option<thread::ThreadId>,
6007    /// The internal encode thread's id, so a re-entrant stop arriving on it is
6008    /// detected and doesn't try to join a chain that includes itself.
6009    encode_thread_id: Option<thread::ThreadId>,
6010    /// Handshake receiver kept when the bounded start-time wait for the encode thread id
6011    /// lapsed (slow X11 setup precedes the encode-thread spawn): the id is late-resolved
6012    /// from here on demand, so the re-entrant-stop guard still recognizes the encode
6013    /// thread — with `encode_thread_id` stuck at `None`, a stop from inside the delivery
6014    /// callback would join the capture thread, which joins the encode thread (the
6015    /// caller), a deadlock cycle. The id send strictly precedes any callback running on
6016    /// that thread, so a `try_recv` at stop time cannot miss it.
6017    encode_tid_rx: Option<std::sync::mpsc::Receiver<thread::ThreadId>>,
6018    /// Delivery thread: owns the GIL-bound Python callback so encode(N+1) never
6019    /// serializes behind deliver(N). Joined on stop; a re-entrant stop from
6020    /// inside the callback (which runs on this thread) must detach instead of
6021    /// self-joining.
6022    deliver_handle: Option<thread::JoinHandle<()>>,
6023    deliver_thread_id: Option<thread::ThreadId>,
6024    /// The Wayland display id this instance's capture is bound to (backend == 2).
6025    wl_display: u32,
6026    /// The X11 capture thread's exit error (backend == 1): the thread records why it died
6027    /// mid-run here, so `capture_state` can report it instead of the error scrolling past in
6028    /// a single stderr line. `None` while healthy.
6029    err: Option<Arc<Mutex<Option<String>>>>,
6030}
6031
6032/// Unified capture handle exposed to Python. Drives the X11 capture directly or delegates to the
6033/// shared Wayland backend, chosen at `start_capture` time. Exposes start_capture / stop_capture /
6034/// request_idr_frame / update_* / is_capturing, plus the Wayland input-injection methods.
6035#[pyclass]
6036struct ScreenCapture {
6037    id: u64,
6038    inner: Mutex<ScState>,
6039}
6040
6041impl ScreenCapture {
6042    /// Stop this capture: signal the capture thread, drop the live controls, and join.
6043    ///
6044    /// The path forks on the backend. A **Wayland** capture only tells the shared compositor to
6045    /// stop when this instance still owns it — ownership is claimed-and-cleared atomically so a
6046    /// stale stop cannot tear down a capture another instance just started. An **X11** capture joins
6047    /// its capture thread (which also joins the encode thread) and then its deliver thread,
6048    /// releasing the GIL first because the deliver thread runs the Python callback and holding the
6049    /// GIL across the joins would deadlock. A re-entrant stop arriving on the capture, encode, or
6050    /// deliver thread cannot join itself, so it detaches and lets the threads exit on the stop flag.
6051    fn stop_internal(&self, py: Python<'_>) -> PyResult<()> {
6052        let (handle, deliver_handle, same_thread, backend, controls, wl_display, cursor_ref) = {
6053            let mut st = self.inner.lock().unwrap();
6054            if let Some(c) = &st.controls {
6055                c.stop.store(true, Ordering::Relaxed);
6056            }
6057            let cur = Some(thread::current().id());
6058            if st.encode_thread_id.is_none()
6059                && let Some(rx) = st.encode_tid_rx.as_ref()
6060                && let Ok(id) = rx.try_recv() {
6061                        st.encode_thread_id = Some(id);
6062                    }
6063            let same = st.cap_thread_id == cur
6064                || st.encode_thread_id == cur
6065                || st.deliver_thread_id == cur;
6066            let controls = st.controls.take();
6067            let handle = st.handle.take();
6068            let deliver_handle = st.deliver_handle.take();
6069            let backend = st.backend;
6070            let cursor_ref = std::mem::take(&mut st.cursor_ref);
6071            let wl_display = st.wl_display;
6072            st.backend = 0;
6073            st.cap_thread_id = None;
6074            st.encode_thread_id = None;
6075            st.encode_tid_rx = None;
6076            st.deliver_thread_id = None;
6077            st.wl_display = 0;
6078            st.err = None;
6079            (handle, deliver_handle, same, backend, controls, wl_display, cursor_ref)
6080        };
6081        if let Some(c) = &controls {
6082            live_x11().lock().unwrap().retain(|x| !Arc::ptr_eq(x, c));
6083        }
6084        if cursor_ref {
6085            crate::x11::cursor::release(py);
6086        }
6087        if backend == 2 {
6088            let did = wl_display;
6089            let owned = {
6090                let mut owners = wayland_owners().lock().unwrap();
6091                if owners.get(&did) == Some(&self.id) {
6092                    owners.remove(&did);
6093                    true
6094                } else {
6095                    false
6096                }
6097            };
6098            if owned
6099                && let Some(slot) = WAYLAND_BACKEND.get()
6100                && let Some(be) = slot.lock().unwrap().as_ref() {
6101                        let _ = be.bind(py).borrow().stop_capture(did);
6102                    }
6103        } else {
6104            if same_thread {
6105                // Detach: the threads exit on the stop flag once the callback returns.
6106                drop(handle);
6107                drop(deliver_handle);
6108            } else {
6109                py.detach(|| {
6110                    if let Some(h) = handle {
6111                        let _ = h.join();
6112                    }
6113                    // The capture join above ends the encode thread, dropping the
6114                    // delivery sender; the deliver thread then drains its one
6115                    // queued frame and exits, so this join is bounded.
6116                    if let Some(h) = deliver_handle {
6117                        let _ = h.join();
6118                    }
6119                });
6120            }
6121        }
6122        Ok(())
6123    }
6124}
6125
6126#[pymethods]
6127impl ScreenCapture {
6128    #[new]
6129    fn new() -> Self {
6130        Self {
6131            id: NEXT_CAPTURE_ID.fetch_add(1, Ordering::Relaxed),
6132            inner: Mutex::new(ScState {
6133                backend: 0,
6134                cursor_ref: false,
6135                controls: None,
6136                handle: None,
6137                cap_thread_id: None,
6138                encode_thread_id: None,
6139                encode_tid_rx: None,
6140                deliver_handle: None,
6141                deliver_thread_id: None,
6142                wl_display: 0,
6143                err: None,
6144            }),
6145        }
6146    }
6147
6148    /// Begin capture: `callback(frame)` is invoked per encoded stripe with a `StripeFrame`.
6149    ///
6150    /// The backend is chosen from the settings (`want_wayland`). A **Wayland** start delegates to
6151    /// the shared backend, distinguishing the compositor RENDER node from the ENCODER node and
6152    /// resolving an AUTO_GPU request; restarting this instance's own live Wayland capture skips the
6153    /// stop so the calloop can reconfigure the running session in place — keeping a compatible NVENC
6154    /// session alive — instead of destroying it and forcing a full rebuild. An **X11** start
6155    /// resolves AUTO_GPU to an encoder device when none was chosen explicitly, then spawns the
6156    /// capture thread (which internally spawns the encode+deliver thread). The per-frame delivery
6157    /// closure makes one GIL acquisition per frame with all stripes batched, and a failed start
6158    /// surfaces as a `PyErr` rather than a silent, forever-"capturing" state.
6159    fn start_capture(
6160        &self,
6161        py: Python<'_>,
6162        callback: Py<PyAny>,
6163        settings: &Bound<'_, PyAny>,
6164    ) -> PyResult<()> {
6165        let display_id = read_display_id(settings);
6166        let live_wayland_restart = want_wayland(settings)
6167            && {
6168                let st = self.inner.lock().unwrap();
6169                st.backend == 2 && st.wl_display == display_id
6170            }
6171            && wayland_owners().lock().unwrap().get(&display_id) == Some(&self.id)
6172            && wayland_alive().lock().unwrap().contains(&display_id);
6173        if !live_wayland_restart {
6174            self.stop_internal(py)?;
6175        }
6176        let rs = extract_settings(settings)?;
6177
6178        if want_wayland(settings) {
6179            let read_node = |attr: &str| -> Option<String> {
6180                settings.getattr(attr).ok().and_then(|o| {
6181                    o.extract::<String>()
6182                        .or_else(|_| {
6183                            o.extract::<Vec<u8>>()
6184                                .map(|b| String::from_utf8_lossy(&b).into_owned())
6185                        })
6186                        .ok()
6187                })
6188            };
6189            let cursor_size = settings
6190                .getattr("cursor_size")
6191                .ok()
6192                .and_then(|v| v.extract::<i32>().ok())
6193                .unwrap_or(-1);
6194            let be = ensure_wayland_backend(
6195                py,
6196                rs.width,
6197                rs.height,
6198                read_node("render_node_path").unwrap_or_default(),
6199                read_node("auto_gpu").unwrap_or_default(),
6200                read_node("encode_node_path").unwrap_or_default(),
6201                cursor_size,
6202            )?;
6203            be.bind(py).borrow().start_capture(callback, settings)?;
6204            wayland_owners().lock().unwrap().insert(display_id, self.id);
6205            {
6206                let mut st = self.inner.lock().unwrap();
6207                st.backend = 2;
6208                st.wl_display = display_id;
6209            }
6210            return Ok(());
6211        }
6212
6213        // A fresh start proves the interpreter is alive: clear the teardown flag the atexit
6214        // sweep may have set (the Wayland start path already does the same), or the delivery
6215        // thread below would drop every frame.
6216        PY_SHUTDOWN.store(false, Ordering::Relaxed);
6217        let mut rs = rs;
6218        if rs.encode_node_index < -1 {
6219            let auto_gpu = settings
6220                .getattr("auto_gpu")
6221                .ok()
6222                .and_then(|o| {
6223                    o.extract::<String>()
6224                        .or_else(|_| {
6225                            o.extract::<Vec<u8>>()
6226                                .map(|b| String::from_utf8_lossy(&b).into_owned())
6227                        })
6228                        .ok()
6229                })
6230                .unwrap_or_default();
6231            if let Some(request) = parse_auto_gpu(&auto_gpu)
6232                && let Some(picked) = auto_select_render_node(request.as_deref())
6233                && let Some(idx) = picked
6234                        .strip_prefix("/dev/dri/renderD")
6235                        .and_then(|s| s.parse::<i32>().ok())
6236                    {
6237                        println!("[x11] AUTO_GPU enabled. Selected: {picked}");
6238                        rs.encode_node_index = idx - 128;
6239                    }
6240        }
6241
6242        println!(
6243            "[x11] Configuring Output: {}x{} @ {:.2} FPS (Encode Node: {})",
6244            rs.width, rs.height, rs.target_fps, rs.encode_node_index
6245        );
6246
6247        let controls = Arc::new(crate::x11::Controls::new(&rs));
6248        let cursor_cap = rs.cursor_size_cap;
6249        live_x11().lock().unwrap().push(controls.clone());
6250        let c2 = controls.clone();
6251        let c3 = controls.clone();
6252        let cb = callback;
6253        let err_slot: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
6254        let err_slot2 = err_slot.clone();
6255
6256        // The Python callback is GIL-bound: run it on its own thread (as the
6257        // Wayland backend and pcmflux do) so encode(N+1) never serializes behind
6258        // deliver(N). Bounded at one in-flight frame: a slower consumer
6259        // backpressures the encoder by exactly one frame instead of growing a
6260        // queue, and no frame is ever dropped.
6261        let (deliver_tx, deliver_rx) = std::sync::mpsc::sync_channel::<Vec<EncodedStripe>>(1);
6262        let deliver_handle = thread::spawn(move || {
6263            crate::boost_thread_priority(-10);
6264            while let Ok(frame) = deliver_rx.recv() {
6265                if PY_SHUTDOWN.load(Ordering::Relaxed) {
6266                    continue;
6267                }
6268                Python::attach(|py| {
6269                    for s in frame {
6270                        match Py::new(
6271                            py,
6272                            StripeFrame::new_owned_meta(
6273                                s.data,
6274                                s.data_type,
6275                                s.stripe_y_start,
6276                                s.stripe_height,
6277                                s.frame_id,
6278                            ),
6279                        ) {
6280                            Ok(f) => {
6281                                if let Err(e) = cb.call1(py, (f,)) {
6282                                    e.print(py);
6283                                }
6284                            }
6285                            Err(e) => eprintln!("[x11] frame alloc error: {e:?}"),
6286                        }
6287                    }
6288                });
6289            }
6290        });
6291        let deliver_thread_id = deliver_handle.thread().id();
6292
6293        let on_frame = move |frame: Vec<EncodedStripe>| {
6294            // Blocks only when the single slot is still occupied (consumer more
6295            // than one frame behind); a dropped receiver (stop) discards.
6296            let _ = deliver_tx.send(frame);
6297        };
6298
6299        let (tid_tx, tid_rx) = std::sync::mpsc::channel();
6300        let (etid_tx, etid_rx) = std::sync::mpsc::channel();
6301        let handle = thread::spawn(move || {
6302            crate::boost_thread_priority(-15);
6303            let _ = tid_tx.send(thread::current().id());
6304            let res = crate::x11::run_capture(rs, c2, etid_tx, on_frame);
6305            c3.stop.store(true, Ordering::Release);
6306            if let Err(e) = res {
6307                let msg = e.to_string();
6308                eprintln!("[x11] capture error: {msg}");
6309                if let Ok(mut g) = err_slot2.lock() {
6310                    *g = Some(msg);
6311                }
6312            }
6313            // run_capture has joined its encode thread by now (NVENC/CUDA session dropped),
6314            // so the atexit sweep may finalize the interpreter without racing that drop.
6315            c3.finished.store(true, Ordering::Release);
6316        });
6317        let (tid, etid_res, etid_rx) = py.detach(move || {
6318            let tid = tid_rx.recv().ok();
6319            let etid_res = etid_rx.recv_timeout(std::time::Duration::from_secs(2));
6320            (tid, etid_res, etid_rx)
6321        });
6322        let mut late_etid_rx = None;
6323        let etid = match etid_res {
6324            Ok(id) => Some(id),
6325            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
6326                let _ = handle.join();
6327                live_x11().lock().unwrap().retain(|x| !Arc::ptr_eq(x, &controls));
6328                let msg = err_slot
6329                    .lock()
6330                    .ok()
6331                    .and_then(|g| g.clone())
6332                    .unwrap_or_else(|| "X11 capture thread exited during start".to_string());
6333                return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(msg));
6334            }
6335            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
6336                // Slow pre-spawn X11 setup: the id will still arrive on this
6337                // channel. Keep the receiver so stop_internal can late-resolve
6338                // it — a None id would blind the re-entrant-stop guard.
6339                late_etid_rx = Some(etid_rx);
6340                None
6341            }
6342        };
6343        // Take the monitor reference BEFORE publishing the capture: a stop that
6344        // observes this capture must always find the reference it is to release.
6345        crate::x11::cursor::acquire(cursor_cap);
6346        let mut st = self.inner.lock().unwrap();
6347        st.backend = 1;
6348        st.cursor_ref = true;
6349        st.controls = Some(controls);
6350        st.handle = Some(handle);
6351        st.cap_thread_id = tid;
6352        st.encode_thread_id = etid;
6353        st.encode_tid_rx = late_etid_rx;
6354        st.deliver_handle = Some(deliver_handle);
6355        st.deliver_thread_id = Some(deliver_thread_id);
6356        st.err = Some(err_slot);
6357        drop(st);
6358        Ok(())
6359    }
6360
6361    fn stop_capture(&self, py: Python<'_>) -> PyResult<()> {
6362        self.stop_internal(py)
6363    }
6364
6365    fn request_idr_frame(&self, py: Python<'_>) -> PyResult<()> {
6366        let (backend, controls, did) = {
6367            let st = self.inner.lock().unwrap();
6368            (st.backend, st.controls.clone(), st.wl_display)
6369        };
6370        match backend {
6371            1 => {
6372                if let Some(c) = controls {
6373                    c.force_idr.store(true, Ordering::Relaxed);
6374                }
6375            }
6376            2 => {
6377                if let Some(slot) = WAYLAND_BACKEND.get()
6378                    && let Some(be) = slot.lock().unwrap().as_ref() {
6379                        let _ = be.bind(py).borrow().request_idr_frame(did);
6380                    }
6381            }
6382            _ => {}
6383        }
6384        Ok(())
6385    }
6386
6387    /// Apply a live target-bitrate (kbps) change to the running capture.
6388    ///
6389    /// On the X11 path the dirty flag is Release-published after the payload store, so the encode
6390    /// thread's Acquire read can never observe the flag set against a stale bitrate.
6391    fn update_video_bitrate(&self, py: Python<'_>, kbps: i32) -> PyResult<()> {
6392        let (backend, controls, did) = {
6393            let st = self.inner.lock().unwrap();
6394            (st.backend, st.controls.clone(), st.wl_display)
6395        };
6396        match backend {
6397            1 => {
6398                if let Some(c) = &controls {
6399                    c.bitrate_kbps.store(kbps, Ordering::Relaxed);
6400                    c.rate_dirty.store(true, Ordering::Release);
6401                }
6402            }
6403            2 => wayland_update_rate(py, did, Some(kbps), None, None),
6404            _ => {}
6405        }
6406        Ok(())
6407    }
6408
6409    fn update_framerate(&self, py: Python<'_>, fps: f64) -> PyResult<()> {
6410        let (backend, controls, did) = {
6411            let st = self.inner.lock().unwrap();
6412            (st.backend, st.controls.clone(), st.wl_display)
6413        };
6414        match backend {
6415            1 => {
6416                if let Some(c) = &controls {
6417                    c.fps_milli.store((fps.max(1.0) * 1000.0) as u64, Ordering::Relaxed);
6418                    c.rate_dirty.store(true, Ordering::Release);
6419                }
6420            }
6421            2 => wayland_update_rate(py, did, None, None, Some(fps)),
6422            _ => {}
6423        }
6424        Ok(())
6425    }
6426
6427    /// Live CBR VBV change, as a multiple of one frame's bit budget (<= 0 = policy default).
6428    fn update_vbv_multiplier(&self, py: Python<'_>, multiplier: f64) -> PyResult<()> {
6429        let (backend, controls, did) = {
6430            let st = self.inner.lock().unwrap();
6431            (st.backend, st.controls.clone(), st.wl_display)
6432        };
6433        match backend {
6434            1 => {
6435                if let Some(c) = &controls {
6436                    c.vbv_mult_milli
6437                        .store((multiplier * 1000.0).round() as i32, Ordering::Relaxed);
6438                    c.rate_dirty.store(true, Ordering::Release);
6439                }
6440            }
6441            2 => wayland_update_rate(py, did, None, Some(multiplier), None),
6442            _ => {}
6443        }
6444        Ok(())
6445    }
6446
6447    /// Apply the live-tunable subset of `settings` (quality, paint-over, streaming mode,
6448    /// cursor overlay, keyframe interval) to the running capture -- no restart, no encoder
6449    /// re-init. Structural changes (encoder, chroma, RC mode, device) still need a restart.
6450    fn update_tunables(&self, py: Python<'_>, settings: &Bound<'_, PyAny>) -> PyResult<()> {
6451        let rs = extract_settings(settings)?;
6452        let t = LiveTunables::from_settings(&rs);
6453        let (backend, controls, did) = {
6454            let st = self.inner.lock().unwrap();
6455            (st.backend, st.controls.clone(), st.wl_display)
6456        };
6457        match backend {
6458            1 => {
6459                if let Some(c) = &controls {
6460                    c.capture_cursor.store(t.capture_cursor, Ordering::Relaxed);
6461                    *c.tunables.lock().unwrap() = Some(t);
6462                    c.tunables_dirty.store(true, Ordering::Release);
6463                }
6464                crate::x11::cursor::set_size_cap(rs.cursor_size_cap);
6465            }
6466            2 => wayland_update_tunables(py, did, t),
6467            _ => {}
6468        }
6469        Ok(())
6470    }
6471
6472    /// Move/resize the live X11 capture region (root-relative). The capture loop drains
6473    /// in-flight frames, re-targets its surfaces, and the encoder follows in place where
6474    /// it can (NVENC reconfigure / stripe re-derive) -- no capture restart. `width`/
6475    /// `height` <= 0 mean "to the root edge". On Wayland the output IS the capture
6476    /// region: restart the capture with new dimensions instead (in-place there too).
6477    fn update_capture_region(&self, x: i32, y: i32, width: i32, height: i32) -> PyResult<()> {
6478        let controls = {
6479            let st = self.inner.lock().unwrap();
6480            if st.backend == 2 {
6481                return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
6482                    "update_capture_region is X11-only; on Wayland restart the capture with new dimensions",
6483                ));
6484            }
6485            st.controls.clone()
6486        };
6487        if let Some(c) = &controls {
6488            *c.region.lock().unwrap() = (x.max(0), y.max(0), width, height);
6489            c.region_dirty.store(true, Ordering::Release);
6490        }
6491        Ok(())
6492    }
6493
6494    #[getter]
6495    fn is_capturing(&self) -> bool {
6496        let st = self.inner.lock().unwrap();
6497        match st.backend {
6498            1 => st
6499                .controls
6500                .as_ref()
6501                .map(|c| !c.stop.load(Ordering::Relaxed))
6502                .unwrap_or(false),
6503            2 => wayland_owners().lock().unwrap().get(&st.wl_display) == Some(&self.id)
6504                && wayland_alive().lock().unwrap().contains(&st.wl_display),
6505            _ => false,
6506        }
6507    }
6508
6509    fn inject_key(&self, py: Python<'_>, scancode: u32, state: u32) -> PyResult<()> {
6510        wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_key(scancode, state))
6511    }
6512    /// Inject an ordered run of `(keycode, state)` events in one message.
6513    fn inject_keys(&self, py: Python<'_>, events: Vec<(u32, u32)>) -> PyResult<()> {
6514        wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_keys(events))
6515    }
6516    fn set_keymap_string(&self, py: Python<'_>, text: String) -> PyResult<()> {
6517        wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().set_keymap_string(text))
6518    }
6519    /// Bind explicit `(keycode, keysym)` pairs onto the current base keymap in one swap;
6520    /// false when no backend runs or no base keymap is installed. See the backend method
6521    /// for the split of responsibility.
6522    fn set_keymap_overlay(&self, py: Python<'_>, binds: Vec<(u32, u32)>) -> PyResult<()> {
6523        wayland_backend_running(py)
6524            .map_or(Ok(()), |be| be.bind(py).borrow().set_keymap_overlay(binds))
6525    }
6526    /// Compositor apps run under (a nested labwc/kwin session pixelflux captures),
6527    /// the target for Computer-Use text injection; selkies resolves it and hands it
6528    /// over. Empty clears it. Stored process-wide since the CU server is per-process.
6529    fn set_app_wayland_display(&self, display: String) {
6530        crate::computer_use::set_app_wayland_display(
6531            if display.is_empty() { None } else { Some(display) },
6532        );
6533    }
6534    /// Type `text` through `display`'s zwp_virtual_keyboard_manager_v1 as a one-shot
6535    /// client: selkies' text-injection path, targeting whichever compositor the apps
6536    /// live under (the nested session's, or pixelflux's own in a direct session).
6537    /// Blocking; releases the GIL for the duration. Raises
6538    /// [`VirtualKeyboardUnavailable`] when the compositor lacks the protocol.
6539    fn type_text_wayland(&self, py: Python<'_>, display: String, text: String) -> PyResult<()> {
6540        py.detach(move || {
6541            let path = crate::wayland::wlclient::socket_path(&display)
6542                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
6543            crate::wayland::vkclient::type_text_to(&path, &text)
6544        })
6545        .map_err(|e: String| {
6546            if e.contains("zwp_virtual_keyboard_manager_v1") {
6547                VirtualKeyboardUnavailable::new_err(e)
6548            } else {
6549                pyo3::exceptions::PyRuntimeError::new_err(e)
6550            }
6551        })
6552    }
6553    /// Tap `keysyms` in order through `display`'s virtual keyboard, verbatim: the
6554    /// caller owns which keysym spells which character (selkies' policy layer);
6555    /// this owns delivery. Same one-shot client and errors as `type_text_wayland`.
6556    fn type_keysyms_wayland(
6557        &self,
6558        py: Python<'_>,
6559        display: String,
6560        keysyms: Vec<u32>,
6561    ) -> PyResult<()> {
6562        py.detach(move || {
6563            let path = crate::wayland::wlclient::socket_path(&display)
6564                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
6565            crate::wayland::vkclient::type_keysyms_to(&path, &keysyms)
6566        })
6567        .map_err(|e: String| {
6568            if e.contains("zwp_virtual_keyboard_manager_v1") {
6569                VirtualKeyboardUnavailable::new_err(e)
6570            } else {
6571                pyo3::exceptions::PyRuntimeError::new_err(e)
6572            }
6573        })
6574    }
6575    /// Scale the app compositor's `index`-th screen, so its applications draw
6576    /// larger while the capture keeps its full resolution. False when that
6577    /// compositor manages no outputs for clients (KWin), whose scale follows
6578    /// the capture output's instead.
6579    fn set_app_output_scale(
6580        &self,
6581        py: Python<'_>,
6582        display: String,
6583        index: usize,
6584        scale: f64,
6585    ) -> PyResult<bool> {
6586        py.detach(move || {
6587            let path = crate::wayland::wlclient::socket_path(&display)
6588                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
6589            crate::wayland::outclient::set_output_scale(&path, index, scale)
6590        })
6591        .map(|outcome| matches!(outcome, crate::wayland::outclient::ScaleOutcome::Applied))
6592        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6593    }
6594    /// Give the app compositor's `index`-th screen this mode and scale in one
6595    /// configuration, so the session lays its desktop out once. Setting them
6596    /// separately exposes a geometry that never exists — the pre-connect mode at
6597    /// the new scale — and a client that does not lay out again keeps it.
6598    /// False = that compositor manages no outputs for clients.
6599    fn set_app_screen_geometry(
6600        &self,
6601        py: Python<'_>,
6602        display: String,
6603        index: usize,
6604        width: i32,
6605        height: i32,
6606        scale: f64,
6607    ) -> PyResult<bool> {
6608        py.detach(move || {
6609            let path = crate::wayland::wlclient::socket_path(&display)
6610                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
6611            crate::wayland::outclient::set_screen_geometry(&path, index, (width, height), scale)
6612        })
6613        .map(|outcome| matches!(outcome, crate::wayland::outclient::ScaleOutcome::Applied))
6614        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6615    }
6616    /// Hold the app compositor's screens past the first `keep` at a small size,
6617    /// so a session that opened more screens than there are capture outputs does
6618    /// not lay its desktop out across one nobody sees. Returns how many were
6619    /// resized (0 = that compositor manages no outputs for clients).
6620    fn hold_spare_app_screens(
6621        &self,
6622        py: Python<'_>,
6623        display: String,
6624        keep: usize,
6625        width: i32,
6626        height: i32,
6627    ) -> PyResult<usize> {
6628        py.detach(move || {
6629            let path = crate::wayland::wlclient::socket_path(&display)
6630                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
6631            crate::wayland::outclient::hold_spare_screens(&path, keep, (width, height))
6632        })
6633        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6634    }
6635    /// Mimes the app compositor's current selection offers (empty = nothing copied).
6636    fn clipboard_types_app(&self, py: Python<'_>, display: String) -> PyResult<Vec<String>> {
6637        py.detach(move || {
6638            crate::wayland::dcclient::list_types(&app_socket_path(&display)?)
6639        })
6640        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6641    }
6642    /// The app compositor selection's payload for `mime`, or None when nothing is
6643    /// copied or the selection does not offer that mime.
6644    fn clipboard_read_app(
6645        &self,
6646        py: Python<'_>,
6647        display: String,
6648        mime: String,
6649    ) -> PyResult<Option<Py<pyo3::types::PyBytes>>> {
6650        let data = py
6651            .detach(move || crate::wayland::dcclient::read(&app_socket_path(&display)?, &mime))
6652            .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
6653        Ok(data.map(|d| pyo3::types::PyBytes::new(py, &d).unbind()))
6654    }
6655    /// Take the app compositor's selection, serving `entries` (mime, bytes) to
6656    /// every paster from a background thread until another client copies.
6657    fn clipboard_write_app(
6658        &self,
6659        py: Python<'_>,
6660        display: String,
6661        entries: Vec<(String, Vec<u8>)>,
6662    ) -> PyResult<()> {
6663        py.detach(move || crate::wayland::dcclient::write(&app_socket_path(&display)?, entries))
6664            .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6665    }
6666    /// Drop the app compositor's selection.
6667    fn clipboard_clear_app(&self, py: Python<'_>, display: String) -> PyResult<()> {
6668        py.detach(move || crate::wayland::dcclient::clear(&app_socket_path(&display)?))
6669            .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6670    }
6671    /// Invoke `callback(mimes: list[str])` from a background thread on every
6672    /// selection change in the app compositor (including the one current at call
6673    /// time). A second watch for the same display replaces the first.
6674    fn clipboard_watch_app(
6675        &self,
6676        py: Python<'_>,
6677        display: String,
6678        callback: Py<PyAny>,
6679    ) -> PyResult<()> {
6680        py.detach(move || crate::wayland::dcclient::watch(&app_socket_path(&display)?, callback))
6681            .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6682    }
6683    /// Stop the selection watch for `display` (no-op without one).
6684    fn clipboard_unwatch_app(&self, py: Python<'_>, display: String) {
6685        let _ = py.detach(move || {
6686            crate::wayland::dcclient::unwatch(&app_socket_path(&display)?);
6687            Ok::<(), String>(())
6688        });
6689    }
6690    fn inject_mouse_move(&self, py: Python<'_>, x: f64, y: f64) -> PyResult<()> {
6691        wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_mouse_move(x, y))
6692    }
6693    fn inject_relative_mouse_move(&self, py: Python<'_>, dx: f64, dy: f64) -> PyResult<()> {
6694        wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_relative_mouse_move(dx, dy))
6695    }
6696    fn inject_mouse_button(&self, py: Python<'_>, btn: u32, state: u32) -> PyResult<()> {
6697        wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_mouse_button(btn, state))
6698    }
6699    fn inject_mouse_scroll(&self, py: Python<'_>, x: f64, y: f64) -> PyResult<()> {
6700        wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_mouse_scroll(x, y))
6701    }
6702    /// Toggle compositing the cursor into captured frames (the alternative to the
6703    /// out-of-band cursor callback): the X11 grab re-reads the flag per frame, Wayland
6704    /// forwards to the compositor.
6705    fn set_cursor_rendering(&self, py: Python<'_>, enabled: bool) -> PyResult<()> {
6706        let (backend, controls) = {
6707            let st = self.inner.lock().unwrap();
6708            (st.backend, st.controls.clone())
6709        };
6710        if backend == 1 {
6711            if let Some(c) = &controls {
6712                c.capture_cursor.store(enabled, Ordering::Relaxed);
6713            }
6714            return Ok(());
6715        }
6716        wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().set_cursor_rendering(enabled))
6717    }
6718    /// Register the client-copy cursor callback for whichever backend runs: the X11 cursor
6719    /// monitor reads it from its shared slot (re-delivering the current cursor to a late
6720    /// registration), and the Wayland backend takes it directly — or stashes it in
6721    /// `PENDING_CURSOR_CALLBACK`, applied by `ensure_wayland_backend` at creation; the
6722    /// backend slot lock is held across the check so a concurrent creation cannot miss the
6723    /// stash.
6724    fn set_cursor_callback(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<()> {
6725        crate::x11::cursor::set_callback(callback.clone_ref(py));
6726        let slot = WAYLAND_BACKEND.get_or_init(|| Mutex::new(None));
6727        let g = slot.lock().unwrap();
6728        match g.as_ref() {
6729            Some(be) => be.bind(py).borrow().set_cursor_callback(callback),
6730            None => {
6731                *PENDING_CURSOR_CALLBACK.lock().unwrap() = Some(callback);
6732                Ok(())
6733            }
6734        }
6735    }
6736    fn get_xkb_keymap_string(&self, py: Python<'_>) -> PyResult<String> {
6737        wayland_backend_running(py)
6738            .map_or(Ok(String::new()), |be| be.bind(py).borrow().get_xkb_keymap_string(py))
6739    }
6740    /// cb(mime: str, data: bytes) fires when a client app copies to the clipboard.
6741    fn set_clipboard_callback(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<()> {
6742        match wayland_backend_running(py) {
6743            Some(be) => be.bind(py).borrow().set_clipboard_callback(callback),
6744            None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
6745                "wayland backend not running",
6746            )),
6747        }
6748    }
6749    /// Compositor-side clipboard offer: serve `data` as `mime` to pasting clients.
6750    fn set_clipboard(&self, py: Python<'_>, mime: String, data: Vec<u8>) -> PyResult<()> {
6751        match wayland_backend_running(py) {
6752            Some(be) => be.bind(py).borrow().set_clipboard(mime, data),
6753            None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
6754                "wayland backend not running",
6755            )),
6756        }
6757    }
6758    /// Set the seat's BASE xkb layout from RMLVO names; false when no backend runs or the
6759    /// layout fails to compile.
6760    #[pyo3(signature = (layout, variant = String::new(), options = String::new(), model = String::new(), rules = String::new()))]
6761    fn set_xkb_layout(
6762        &self,
6763        py: Python<'_>,
6764        layout: String,
6765        variant: String,
6766        options: String,
6767        model: String,
6768        rules: String,
6769    ) -> PyResult<bool> {
6770        wayland_backend_running(py).map_or(Ok(false), |be| {
6771            be.bind(py).borrow().set_xkb_layout(py, layout, variant, options, model, rules)
6772        })
6773    }
6774    /// Seat keyboard readback: `(pressed_keycodes, modifier_mask)`; empty when no backend runs.
6775    fn get_keyboard_state(&self, py: Python<'_>) -> PyResult<(Vec<u32>, u32)> {
6776        wayland_backend_running(py)
6777            .map_or(Ok((Vec::new(), 0)), |be| be.bind(py).borrow().get_keyboard_state(py))
6778    }
6779    /// The capture geometry actually live on the given display `(width, height, scale)`;
6780    /// `None` when no Wayland backend runs or the compositor did not answer in time (a
6781    /// timeout is distinct from a real answer so the caller treats it as unknown rather than
6782    /// "nothing to reconcile"). X11 has no compositor to read back and returns `None`.
6783    #[pyo3(signature = (display_id = 0))]
6784    fn get_realized_geometry(
6785        &self,
6786        py: Python<'_>,
6787        display_id: u32,
6788    ) -> PyResult<Option<(i32, i32, f64)>> {
6789        wayland_backend_running(py)
6790            .map_or(Ok(None), |be| be.bind(py).borrow().get_realized_geometry(py, display_id))
6791    }
6792    /// Lifecycle of this capture as `(state, last_error)`: `state` is `"running"`, `"failed"`
6793    /// or `"idle"`; `last_error` gives the reason a start failed, or a caveat a live capture
6794    /// came up with (encoder fell back to CPU, host connect refused, a refused resize). The
6795    /// Wayland outcome is recorded by the compositor thread; the X11 outcome is the capture
6796    /// thread's own exit error. `display_id` selects a Wayland output; X11 ignores it.
6797    #[pyo3(signature = (display_id = 0))]
6798    fn capture_state(&self, display_id: u32) -> (String, Option<String>) {
6799        let (backend, running, err) = {
6800            let st = self.inner.lock().unwrap();
6801            let running = st
6802                .controls
6803                .as_ref()
6804                .map(|c| !c.stop.load(Ordering::Relaxed))
6805                .unwrap_or(false);
6806            let err = st.err.as_ref().and_then(|e| e.lock().ok().and_then(|g| g.clone()));
6807            (st.backend, running, err)
6808        };
6809        match backend {
6810            1 => {
6811                let state = if running {
6812                    "running"
6813                } else if err.is_some() {
6814                    "failed"
6815                } else {
6816                    "idle"
6817                };
6818                (state.to_string(), err)
6819            }
6820            2 => wayland_capture_state(display_id),
6821            _ => ("idle".to_string(), None),
6822        }
6823    }
6824    /// Create an additional Wayland output (see `WaylandBackend.create_output`); false when
6825    /// no backend runs.
6826    // The parameter list is the Python signature; grouping it would change the ABI.
6827    #[allow(clippy::too_many_arguments)]
6828    #[pyo3(signature = (id, width, height, x = 0, y = 0, scale = 1.0))]
6829    fn create_output(
6830        &self,
6831        py: Python<'_>,
6832        id: u32,
6833        width: i32,
6834        height: i32,
6835        x: i32,
6836        y: i32,
6837        scale: f64,
6838    ) -> PyResult<bool> {
6839        wayland_backend_running(py).map_or(Ok(false), |be| {
6840            be.bind(py).borrow().create_output(py, id, width, height, x, y, scale)
6841        })
6842    }
6843    /// Destroy a secondary Wayland output; false when no backend runs.
6844    fn destroy_output(&self, py: Python<'_>, id: u32) -> PyResult<bool> {
6845        wayland_backend_running(py)
6846            .map_or(Ok(false), |be| be.bind(py).borrow().destroy_output(py, id))
6847    }
6848    /// Move a Wayland output (the primary included) to layout offset `(x, y)`; false when
6849    /// no backend runs or the id is unknown.
6850    fn reposition_output(&self, py: Python<'_>, id: u32, x: i32, y: i32) -> PyResult<bool> {
6851        wayland_backend_running(py)
6852            .map_or(Ok(false), |be| be.bind(py).borrow().reposition_output(py, id, x, y))
6853    }
6854    /// Recreate the Wayland cursor theme at `size` pixels (named-cursor callbacks and the
6855    /// burned-in overlay); false when no backend runs or the size is non-positive.
6856    fn set_cursor_size(&self, py: Python<'_>, size: i32) -> PyResult<bool> {
6857        wayland_backend_running(py)
6858            .map_or(Ok(false), |be| be.bind(py).borrow().set_cursor_size(py, size))
6859    }
6860    /// Every live Wayland output as `(id, x, y, width, height, scale, capturing)`; empty
6861    /// when no backend runs.
6862    fn list_outputs(&self, py: Python<'_>) -> PyResult<Vec<OutputDesc>> {
6863        wayland_backend_running(py)
6864            .map_or(Ok(Vec::new()), |be| be.bind(py).borrow().list_outputs(py))
6865    }
6866    /// Display capacity (see `WaylandBackend.output_capacity`); -1 when no backend runs.
6867    fn output_capacity(&self, py: Python<'_>) -> PyResult<i64> {
6868        wayland_backend_running(py)
6869            .map_or(Ok(-1), |be| be.bind(py).borrow().output_capacity(py))
6870    }
6871    /// Move a window onto an output (fullscreened there); false when no backend runs.
6872    fn move_window_to_output(&self, py: Python<'_>, window_id: u32, output_id: u32) -> PyResult<bool> {
6873        wayland_backend_running(py).map_or(Ok(false), |be| {
6874            be.bind(py).borrow().move_window_to_output(py, window_id, output_id)
6875        })
6876    }
6877    /// Every mapped window as `(window_id, title, app_id, output_id, waiting)`; empty when
6878    /// no backend runs.
6879    fn list_windows(&self, py: Python<'_>) -> PyResult<Vec<WindowDesc>> {
6880        wayland_backend_running(py)
6881            .map_or(Ok(Vec::new()), |be| be.bind(py).borrow().list_windows(py))
6882    }
6883}
6884
6885/// Best-effort teardown: flag the capture thread to exit without joining.
6886///
6887/// Joining would need the GIL (the thread calls back into Python), which `Drop` cannot safely take,
6888/// so this only sets the stop flag; the actual join is left to an explicit `stop_capture` or the
6889/// atexit sweep.
6890impl Drop for ScreenCapture {
6891    fn drop(&mut self) {
6892        if let Ok(mut st) = self.inner.lock() {
6893            if let Some(c) = &st.controls {
6894                c.stop.store(true, Ordering::Relaxed);
6895            }
6896            // A Wayland capture is owned by this instance: release it, or a GC'd handle
6897            // would leave the compositor encoding and delivering forever.
6898            if st.backend == 2 && !crate::PY_SHUTDOWN.load(Ordering::Relaxed) {
6899                let did = st.wl_display;
6900                let owned = {
6901                    let mut owners = wayland_owners().lock().unwrap();
6902                    if owners.get(&did) == Some(&self.id) {
6903                        owners.remove(&did);
6904                        true
6905                    } else {
6906                        false
6907                    }
6908                };
6909                if owned
6910                    && let Some(slot) = WAYLAND_BACKEND.get()
6911                    && let Some(be) = slot.lock().unwrap().as_ref() {
6912                            Python::attach(|py| {
6913                                let _ = be.bind(py).borrow().stop_capture(did);
6914                            });
6915                        }
6916            }
6917            // Pair the cursor-monitor acquire from start_capture: a dropped
6918            // handle that never got stop_capture would pin the refcount and its
6919            // monitor thread forever.
6920            if std::mem::take(&mut st.cursor_ref) && !crate::PY_SHUTDOWN.load(Ordering::Relaxed) {
6921                Python::attach(crate::x11::cursor::release);
6922            }
6923        }
6924    }
6925}
6926
6927/// Build a Python dict from a recorder status snapshot (one shape for status and stop).
6928fn recording_status_dict(py: Python<'_>, s: &crate::recorder::RecordingStatus) -> PyResult<Py<PyAny>> {
6929    let d = pyo3::types::PyDict::new(py);
6930    d.set_item("active", s.active)?;
6931    d.set_item("path", &s.path)?;
6932    d.set_item("backend", s.backend)?;
6933    d.set_item("mode", s.mode)?;
6934    d.set_item("frames", s.frames)?;
6935    d.set_item("sync_frames", s.sync_frames)?;
6936    d.set_item("dropped", s.dropped)?;
6937    d.set_item("skipped_non_h264", s.skipped_non_h264)?;
6938    d.set_item("bytes", s.bytes)?;
6939    d.set_item("duration_s", s.duration_s)?;
6940    d.set_item("width", s.width)?;
6941    d.set_item("height", s.height)?;
6942    d.set_item("error", s.error.as_deref())?;
6943    Ok(d.into_any().unbind())
6944}
6945
6946/// Start the built-in MP4 recorder. Works with no capture and no client running: the
6947/// recorder owns an independent capture (X11 root, or a Wayland output of the in-process
6948/// compositor) and taps a live streaming session instead of restarting it. `settings` is an
6949/// optional `CaptureSettings` for a recorder-owned capture (H.264 only; `display_id`
6950/// selects the Wayland output); when omitted, `PIXELFLUX_RECORD_*` environment variables
6951/// and full-screen defaults apply.
6952#[pyfunction]
6953#[pyo3(signature = (path, settings = None))]
6954fn start_recording(
6955    py: Python<'_>,
6956    path: String,
6957    settings: Option<&Bound<'_, PyAny>>,
6958) -> PyResult<Py<PyAny>> {
6959    let mut opts = crate::recorder::RecordOptions::from_env(path);
6960    if let Some(s) = settings {
6961        let rs = extract_settings(s)?;
6962        if rs.output_mode != 1 {
6963            return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
6964                "recording requires H.264 capture settings (output_mode=1); JPEG cannot be recorded",
6965            ));
6966        }
6967        opts.display_id = read_display_id(s);
6968        if let Some(explicit) = s
6969            .getattr("use_wayland")
6970            .ok()
6971            .and_then(|v| v.extract::<bool>().ok())
6972        {
6973            opts.backend = Some(if explicit {
6974                crate::recorder::PreferredBackend::Wayland
6975            } else {
6976                crate::recorder::PreferredBackend::X11
6977            });
6978        }
6979        // Explicit settings are authoritative over the env knobs they subsume.
6980        opts.fps = 0.0;
6981        opts.bitrate_kbps = 0;
6982        opts.capture = Some(rs);
6983    }
6984    let status = py
6985        .detach(|| crate::recorder::start(opts))
6986        .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)?;
6987    recording_status_dict(py, &status)
6988}
6989
6990/// Stop the active recording, finalize the MP4, and return the final status dict. Raises
6991/// when no recording is active or nothing recordable was captured.
6992#[pyfunction]
6993fn stop_recording(py: Python<'_>) -> PyResult<Py<PyAny>> {
6994    let status = py
6995        .detach(crate::recorder::stop)
6996        .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)?;
6997    recording_status_dict(py, &status)
6998}
6999
7000/// Status of the live recording (or the last finished one); `None` if this process has
7001/// never recorded.
7002#[pyfunction]
7003fn recording_status(py: Python<'_>) -> PyResult<Py<PyAny>> {
7004    match crate::recorder::status() {
7005        Some(s) => recording_status_dict(py, &s),
7006        None => Ok(py.None()),
7007    }
7008}
7009
7010/// Bring the Wayland compositor socket up before any capture so apps launched early can
7011/// connect (sets WAYLAND_DISPLAY for children of this process). Idempotent; the running
7012/// backend keeps its node/dimensions on later calls. `render_node` is an explicit
7013/// /dev/dri/renderD* path; `auto_gpu` is a truthy string or vendor/driver token; empty
7014/// values mean software rendering. Returns the compositor's actual socket name (the
7015/// auto-picked `wayland-N`, which need not match any configured index); empty string only
7016/// if the socket did not come up in time.
7017#[pyfunction]
7018#[pyo3(signature = (width = 0, height = 0, render_node = String::new(), auto_gpu = String::new(), cursor_size = -1))]
7019fn ensure_wayland_display(
7020    py: Python<'_>,
7021    width: i32,
7022    height: i32,
7023    render_node: String,
7024    auto_gpu: String,
7025    cursor_size: i32,
7026) -> PyResult<String> {
7027    ensure_wayland_backend(py, width, height, render_node, auto_gpu, String::new(), cursor_size)?;
7028    Ok(py
7029        .detach(|| wait_socket_name(Duration::from_secs(5)))
7030        .unwrap_or_default())
7031}
7032
7033/// Whether a Wayland session here would be hardware accelerated, and whether there is a GPU
7034/// to accelerate it.
7035///
7036/// Device paths do not answer this: a render node can exist with no working allocator or EGL
7037/// stack behind it, and a GPU can be present with no node at all (an NVIDIA container without
7038/// the graphics driver capability). So the compositor's own bring-up is run against the node
7039/// it would resolve, through allocating a render target and exporting it as a dmabuf. Nothing
7040/// is left running; a compositor started afterwards is unaffected.
7041///
7042/// A node that only reaches a software rasterizer is reported unaccelerated: Mesa answers
7043/// with llvmpipe rather than failing, so the bring-up succeeding is not on its own proof of
7044/// a GPU behind it.
7045///
7046/// `render_node` and `auto_gpu` take the same values as the capture settings of those names.
7047/// Returns `node` (the resolved render node, empty when none), `accelerated`, `gpu` (a GPU is
7048/// exposed here at all), `renderer` (the GL renderer reached, empty when none was), and
7049/// `error` (the step that failed; empty when accelerated).
7050#[pyfunction]
7051#[pyo3(signature = (render_node = String::new(), auto_gpu = String::new()))]
7052fn probe_wayland_gpu(
7053    py: Python<'_>,
7054    render_node: String,
7055    auto_gpu: String,
7056) -> PyResult<Py<PyAny>> {
7057    let (node, name, error) = py.detach(|| {
7058        let node = if render_node.is_empty() {
7059            parse_auto_gpu(&auto_gpu).and_then(|token| auto_select_render_node(token.as_deref()))
7060        } else {
7061            Some(render_node)
7062        };
7063        let Some(node) = node else {
7064            return (String::new(), String::new(), "No render node".to_string());
7065        };
7066        let mut name = String::new();
7067        let result = gpu_render_init(std::path::Path::new(&node)).and_then(|(gbm, mut renderer)| {
7068            let bo = gbm
7069                .create_buffer_object::<()>(64, 64, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
7070                .map_err(|_| "Failed to allocate GBM buffer")?;
7071            bo.fd().map_err(|e| format!("Failed to export dmabuf: {e:?}"))?;
7072            name = gl_renderer_name(&mut renderer);
7073            let lowered = name.to_lowercase();
7074            if ["llvmpipe", "softpipe", "swrast", "software rasterizer"]
7075                .iter()
7076                .any(|sw| lowered.contains(sw))
7077            {
7078                return Err(format!("Software rasterizer only ({name})"));
7079            }
7080            Ok(())
7081        });
7082        (node, name, result.err().unwrap_or_default())
7083    });
7084    let d = pyo3::types::PyDict::new(py);
7085    d.set_item("node", &node)?;
7086    d.set_item("accelerated", error.is_empty())?;
7087    d.set_item("gpu", gpu_exposed())?;
7088    d.set_item("renderer", &name)?;
7089    d.set_item("error", &error)?;
7090    Ok(d.into_any().unbind())
7091}
7092
7093/// The running compositor's Wayland socket name (e.g. "wayland-1"), or None when no
7094/// compositor thread has been started (this never creates one).
7095#[pyfunction]
7096fn get_wayland_display_name(py: Python<'_>) -> Option<String> {
7097    wayland_backend_running(py)?;
7098    py.detach(|| wait_socket_name(Duration::from_secs(2)))
7099}
7100
7101/// Stop every live capture (registered with atexit) before interpreter finalization.
7102///
7103/// The interpreter-teardown gate is set first so no detached thread may attach to a finalizing
7104/// interpreter, and the cursor callbacks (the X11 monitor's and a never-applied Wayland stash)
7105/// are dropped while the GIL is held. Every X11 capture's stop flag is set, and a live Wayland
7106/// capture is stopped over the command channel (the compositor thread clears its callback and
7107/// encoder on `StopCapture`). A brief grace sleep lets the stops be observed before Python
7108/// finalizes.
7109#[pyfunction]
7110fn _stop_all_captures(py: Python<'_>) {
7111    PY_SHUTDOWN.store(true, Ordering::Relaxed);
7112    // Finalize any active recording first so its last buffered MP4 sample is flushed and
7113    // its own capture (if any) is stopped through the normal path.
7114    py.detach(crate::recorder::finalize_on_exit);
7115    *PENDING_CURSOR_CALLBACK.lock().unwrap() = None;
7116    crate::x11::cursor::shutdown();
7117    crate::wayland::dcclient::unwatch_all();
7118    // Flag every live X11 capture to stop, then wait (bounded) for each thread to actually
7119    // return: it joins its own encode thread on the way out, dropping the NVENC/CUDA
7120    // session, and letting the interpreter finalize while that drop is in flight segfaults —
7121    // the same hazard the Wayland Barrier below fences. A snapshot is taken so the registry
7122    // mutex is not held across the wait.
7123    let x11: Vec<Arc<crate::x11::Controls>> = live_x11().lock().unwrap().iter().cloned().collect();
7124    for c in &x11 {
7125        c.stop.store(true, Ordering::Relaxed);
7126    }
7127    if !x11.is_empty() {
7128        let deadline = Instant::now() + Duration::from_secs(2);
7129        py.detach(|| {
7130            for c in &x11 {
7131                while !c.finished.load(Ordering::Acquire) && Instant::now() < deadline {
7132                    std::thread::sleep(Duration::from_millis(5));
7133                }
7134            }
7135        });
7136    }
7137    // Clone the backend handle out of the slot so the WAYLAND_BACKEND mutex is released
7138    // before the Barrier wait below: a GC-triggered ScreenCapture::Drop under the GIL takes
7139    // that same mutex, and holding it across the detached wait would block that Drop until
7140    // the Barrier times out into the unsafe exit.
7141    let be = WAYLAND_BACKEND
7142        .get()
7143        .and_then(|slot| slot.lock().unwrap().as_ref().map(|b| b.clone_ref(py)));
7144    if let Some(be) = be {
7145        let be = be.bind(py).borrow();
7146        let mut displays: Vec<u32> = wayland_alive().lock().unwrap().iter().copied().collect();
7147        if !displays.contains(&0) {
7148            displays.push(0);
7149        }
7150        for did in displays {
7151            let _ = be.stop_capture(did);
7152        }
7153        // Wait (bounded) until the calloop finished processing the stops: dropping a
7154        // hardware encoder session (NVENC/CUDA) mid-process-exit segfaults, so the
7155        // interpreter must not finalize while that teardown is still running.
7156        let (ack_tx, ack_rx) = std::sync::mpsc::channel::<()>();
7157        if be.send(ThreadCommand::Barrier { reply: ack_tx }).is_ok() {
7158            let _ = py.detach(move || ack_rx.recv_timeout(Duration::from_secs(2)));
7159        }
7160    }
7161    wayland_owners().lock().unwrap().clear();
7162    wayland_alive().lock().unwrap().clear();
7163    py.detach(|| std::thread::sleep(Duration::from_millis(50)));
7164}
7165
7166/// The `pixelflux` Python module: registers the exported classes and functions, and hooks
7167/// `_stop_all_captures` into `atexit` so every live capture is stopped before interpreter shutdown.
7168/// Socket path for a Wayland display name, with the ABI methods' error shape.
7169fn app_socket_path(display: &str) -> Result<String, String> {
7170    crate::wayland::wlclient::socket_path(display)
7171        .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())
7172}
7173
7174pyo3::create_exception!(
7175    pixelflux,
7176    VirtualKeyboardUnavailable,
7177    pyo3::exceptions::PyRuntimeError,
7178    "The target compositor does not advertise zwp_virtual_keyboard_manager_v1."
7179);
7180
7181/// Start the Computer-Use HTTP server: a bare port listens on all interfaces,
7182/// `host:port` scopes it. Idempotent; the PIXELFLUX_CU env var remains the
7183/// standalone fallback.
7184#[pyfunction]
7185fn start_computer_use(bind: String) {
7186    crate::computer_use::start_cu_server(&bind);
7187}
7188
7189/// `gil_used = true`: the module has not been audited for free-threaded Python. The
7190/// detached compositor, capture, encode and delivery threads attach to the interpreter
7191/// and several native encoder sessions (NVENC/CUDA, VA-API) assume the GIL serializes
7192/// their Python-facing access; until that is proven safe the interpreter re-enables the
7193/// GIL for this module on a free-threaded build rather than silently defaulting to the
7194/// thread-safe claim pyo3 0.28+ makes.
7195#[pymodule(gil_used = true)]
7196fn pixelflux(m: &Bound<'_, PyModule>) -> PyResult<()> {
7197    m.add_class::<WaylandBackend>()?;
7198    m.add_class::<StripeFrame>()?;
7199    m.add_class::<CaptureSettings>()?;
7200    m.add_class::<ScreenCapture>()?;
7201    m.add_class::<webcam::VirtualCamera>()?;
7202    m.add_class::<webcam::VirtualCameraSettings>()?;
7203    // Feature probe for consumers: this build delivers X11 cursors via set_cursor_callback.
7204    m.add("X11_CURSOR_CALLBACK", true)?;
7205    // The software H.264 encoder this build resolved to ("x264" | "openh264"): what a CPU
7206    // H.264 session encodes with, so a consumer can pick rate-control defaults and name it.
7207    m.add("SOFTWARE_H264_ENCODER", encoders::SOFTWARE_H264_ENCODER)?;
7208    m.add_function(wrap_pyfunction!(stripe_frame_from_buffer, m)?)?;
7209    m.add_function(wrap_pyfunction!(ensure_wayland_display, m)?)?;
7210    m.add_function(wrap_pyfunction!(get_wayland_display_name, m)?)?;
7211    m.add_function(wrap_pyfunction!(probe_wayland_gpu, m)?)?;
7212    m.add_function(wrap_pyfunction!(start_recording, m)?)?;
7213    m.add_function(wrap_pyfunction!(stop_recording, m)?)?;
7214    m.add_function(wrap_pyfunction!(recording_status, m)?)?;
7215    m.add_function(wrap_pyfunction!(start_computer_use, m)?)?;
7216    m.add(
7217        "VirtualKeyboardUnavailable",
7218        m.py().get_type::<VirtualKeyboardUnavailable>(),
7219    )?;
7220    m.add_function(wrap_pyfunction!(_stop_all_captures, m)?)?;
7221    if let Ok(atexit) = m.py().import("atexit") {
7222        let _ = atexit.call_method1("register", (m.getattr("_stop_all_captures")?,));
7223    }
7224    // Standalone CU entry point: with PIXELFLUX_CU set the server binds at import, serving
7225    // X11 (via DISPLAY) until a Wayland compositor registers itself as the backend.
7226    crate::computer_use::spawn_cu_from_env();
7227    // PIXELFLUX_RECORD=<path>: start recording from process start (X11 immediately, or as
7228    // soon as the in-process Wayland compositor comes up).
7229    crate::recorder::autostart_from_env();
7230
7231
7232    Ok(())
7233}
7234
7235#[cfg(test)]
7236mod shm_usage_tests {
7237    //! What the session reports as its /dev/shm usage: allocated blocks, so a sparse
7238    //! file is not counted as memory anyone holds.
7239    use super::shm_usage_in;
7240
7241    #[test]
7242    fn only_allocated_blocks_count() {
7243        let dir = std::env::temp_dir().join(format!("pixelflux-shm-{}", std::process::id()));
7244        std::fs::create_dir_all(&dir).unwrap();
7245        let path = dir.to_str().unwrap().to_string();
7246
7247        std::fs::File::create(dir.join("sparse"))
7248            .unwrap()
7249            .set_len(8 * 1024 * 1024 * 1024)
7250            .unwrap();
7251        let sparse = shm_usage_in(&path);
7252
7253        std::fs::write(dir.join("dense"), vec![0u8; 4 * 1024 * 1024]).unwrap();
7254        let dense = shm_usage_in(&path);
7255
7256        std::fs::remove_dir_all(&dir).ok();
7257        assert!(sparse < 1024 * 1024, "an 8 GiB sparse file counted {sparse} bytes");
7258        assert!(
7259            dense - sparse >= 4 * 1024 * 1024,
7260            "a 4 MiB written file added only {} bytes",
7261            dense - sparse
7262        );
7263    }
7264}
7265
7266#[cfg(test)]
7267mod capture_state_tests {
7268    //! The `(state, last_error)` a Wayland display's capture reports: a live pipeline is
7269    //! "running" (with a caveat surfaced alongside), a recorded failure with no live
7270    //! pipeline is "failed", and a clean stop is "idle".
7271    use super::{set_wayland_capture_err, wayland_alive, wayland_capture_state};
7272
7273    #[test]
7274    fn state_tracks_liveness_and_recorded_error() {
7275        // A display id unlikely to collide with any concurrent test's use of the globals.
7276        let did = 987_654;
7277        set_wayland_capture_err(did, None);
7278        wayland_alive().lock().unwrap().remove(&did);
7279        assert_eq!(wayland_capture_state(did), ("idle".to_string(), None));
7280
7281        // Recorded failure, no live pipeline -> failed with the reason.
7282        set_wayland_capture_err(did, Some("no output".to_string()));
7283        assert_eq!(
7284            wayland_capture_state(did),
7285            ("failed".to_string(), Some("no output".to_string()))
7286        );
7287
7288        // A live pipeline reads running even with a caveat still recorded.
7289        wayland_alive().lock().unwrap().insert(did);
7290        set_wayland_capture_err(did, Some("using CPU encode".to_string()));
7291        assert_eq!(
7292            wayland_capture_state(did),
7293            ("running".to_string(), Some("using CPU encode".to_string()))
7294        );
7295
7296        // Clean stop clears both.
7297        wayland_alive().lock().unwrap().remove(&did);
7298        set_wayland_capture_err(did, None);
7299        assert_eq!(wayland_capture_state(did), ("idle".to_string(), None));
7300    }
7301}
7302
7303#[cfg(test)]
7304mod host_layout_tests {
7305    //! How a host's verdict on a layout request resolves for the capture configured at the
7306    //! requested size: applied or already current -> nothing to do; kept a different mode
7307    //! -> follow it; mode unknown -> keep gating on the request.
7308    use super::host_layout_resolution;
7309
7310    #[test]
7311    fn verdict_resolves_to_a_follow_size_only_when_the_host_kept_another_mode() {
7312        let want = (1920, 1080);
7313        // Applied: nothing changes, whatever the (possibly not yet announced) mode reads.
7314        assert_eq!(host_layout_resolution(true, want, Some((1280, 720))), None);
7315        assert_eq!(host_layout_resolution(true, want, None), None);
7316        // Declined but already at the requested size (a re-assertion on a host without
7317        // layout management): converged.
7318        assert_eq!(host_layout_resolution(false, want, Some(want)), None);
7319        // Declined and running something else: the capture follows the host.
7320        assert_eq!(host_layout_resolution(false, want, Some((2560, 1440))), Some((2560, 1440)));
7321        // Declined with no mode known: nothing to follow, the request stands.
7322        assert_eq!(host_layout_resolution(false, want, None), None);
7323    }
7324}
7325
7326#[cfg(test)]
7327mod output_overlap_tests {
7328    //! Invariants of the output-placement intersection predicate: strict interior
7329    //! intersection (touching edges never overlap), containment and identity overlap,
7330    //! empty/negative rectangles never overlap, and extreme coordinates do not wrap.
7331    use super::rects_overlap;
7332
7333    #[test]
7334    fn disjoint_rects_do_not_overlap() {
7335        assert!(!rects_overlap((0, 0, 100, 100), (200, 0, 100, 100)));
7336        assert!(!rects_overlap((0, 0, 100, 100), (0, 200, 100, 100)));
7337    }
7338
7339    #[test]
7340    fn touching_edges_do_not_overlap() {
7341        // Right edge of a meets left edge of b, and bottom meets top.
7342        assert!(!rects_overlap((0, 0, 100, 100), (100, 0, 100, 100)));
7343        assert!(!rects_overlap((0, 0, 100, 100), (0, 100, 100, 100)));
7344        // Corner touch only.
7345        assert!(!rects_overlap((0, 0, 100, 100), (100, 100, 50, 50)));
7346    }
7347
7348    #[test]
7349    fn one_pixel_intrusion_overlaps() {
7350        assert!(rects_overlap((0, 0, 100, 100), (99, 0, 100, 100)));
7351        assert!(rects_overlap((0, 0, 100, 100), (0, 99, 100, 100)));
7352    }
7353
7354    #[test]
7355    fn containment_and_identity_overlap() {
7356        assert!(rects_overlap((0, 0, 100, 100), (25, 25, 10, 10)));
7357        assert!(rects_overlap((25, 25, 10, 10), (0, 0, 100, 100)));
7358        assert!(rects_overlap((5, 5, 50, 50), (5, 5, 50, 50)));
7359    }
7360
7361    #[test]
7362    fn empty_or_negative_rects_never_overlap() {
7363        assert!(!rects_overlap((10, 10, 0, 50), (0, 0, 100, 100)));
7364        assert!(!rects_overlap((10, 10, 50, 0), (0, 0, 100, 100)));
7365        assert!(!rects_overlap((10, 10, -5, 5), (0, 0, 100, 100)));
7366        assert!(!rects_overlap((0, 0, 100, 100), (10, 10, 0, 0)));
7367    }
7368
7369    #[test]
7370    fn negative_origins_overlap_correctly() {
7371        assert!(rects_overlap((-50, -50, 100, 100), (0, 0, 100, 100)));
7372        assert!(!rects_overlap((-100, -100, 100, 100), (0, 0, 100, 100)));
7373    }
7374
7375    #[test]
7376    fn extreme_coordinates_do_not_wrap() {
7377        assert!(!rects_overlap((i32::MAX - 10, 0, 10, 10), (i32::MIN, 0, 10, 10)));
7378        assert!(rects_overlap((i32::MAX - 10, 0, 10, 10), (i32::MAX - 5, 0, 10, 10)));
7379    }
7380}
7381
7382#[cfg(test)]
7383mod wl_frame_pool_tests {
7384    //! Invariants under test: try_begin is non-blocking and hands out a buffer ONLY while the
7385    //! publish slot is empty (so publish can never block the calloop); take blocks until a
7386    //! frame or shutdown; recycle/cancel return buffers for reuse; every published frame is
7387    //! observed exactly once and in order (the H.264 reference chain depends on it).
7388    use super::*;
7389
7390    fn frame(id: usize, buf: Vec<u8>, n: u16) -> WlFrame {
7391        WlFrame {
7392            id,
7393            buf,
7394            frame_id: n,
7395            damage: Vec::new(),
7396            is_animated: false,
7397        }
7398    }
7399
7400    #[test]
7401    fn begin_gated_on_slot_and_free_list() {
7402        let p = WlFramePool::new(2, 16);
7403        let (a, abuf) = p.try_begin().expect("first buffer");
7404        let (b, bbuf) = p.try_begin().expect("second buffer");
7405        assert_ne!(a, b);
7406        assert!(p.try_begin().is_none(), "free list exhausted");
7407        p.publish(frame(a, abuf, 0));
7408        p.cancel(b, bbuf);
7409        assert!(p.try_begin().is_none(), "slot occupied blocks begin");
7410        let f = p.take().expect("published frame");
7411        assert_eq!(f.frame_id, 0);
7412        p.recycle(f.id, f.buf);
7413        assert!(p.try_begin().is_some(), "drained slot re-enables begin");
7414    }
7415
7416    #[test]
7417    fn frames_flow_in_order_and_buffers_recycle() {
7418        let p = Arc::new(WlFramePool::new(2, 4));
7419        let p2 = p.clone();
7420        let consumer = thread::spawn(move || {
7421            let mut seen = Vec::new();
7422            while let Some(f) = p2.take() {
7423                seen.push(f.frame_id);
7424                p2.recycle(f.id, f.buf);
7425            }
7426            seen
7427        });
7428        let mut published = 0u16;
7429        while published < 50 {
7430            if let Some((id, buf)) = p.try_begin() {
7431                p.publish(frame(id, buf, published));
7432                published += 1;
7433            } else {
7434                thread::sleep(Duration::from_micros(50));
7435            }
7436        }
7437        thread::sleep(Duration::from_millis(50));
7438        p.shutdown();
7439        let seen = consumer.join().unwrap();
7440        assert_eq!(seen, (0..50).collect::<Vec<u16>>(), "every frame, in order");
7441    }
7442
7443    #[test]
7444    fn shutdown_unblocks_take() {
7445        let p = Arc::new(WlFramePool::new(1, 4));
7446        let p2 = p.clone();
7447        let t = thread::spawn(move || p2.take());
7448        thread::sleep(Duration::from_millis(30));
7449        p.shutdown();
7450        assert!(t.join().unwrap().is_none(), "take returns None on shutdown");
7451    }
7452
7453    #[test]
7454    fn cancel_returns_buffer_for_reuse() {
7455        let p = WlFramePool::new(1, 8);
7456        let (id, buf) = p.try_begin().expect("buffer");
7457        assert!(p.try_begin().is_none());
7458        p.cancel(id, buf);
7459        assert!(p.try_begin().is_some(), "cancelled reservation reusable");
7460    }
7461}
7462
7463#[cfg(test)]
7464mod auto_gpu_token_tests {
7465    //! Invariant: AUTO_GPU tokens match a card's kernel-reported identity —
7466    //! driver name exactly (no table), raw PCI vendor ID, devicetree compatible
7467    //! prefix literally, or a human vendor name via the small embedded aliases —
7468    //! so users may pass either "amd" or "amdgpu" (etc.) interchangeably.
7469    use super::{card_matches_token, CardIdentity};
7470
7471    fn pci(driver: &str, vendor: u32) -> CardIdentity {
7472        CardIdentity { driver: driver.into(), pci_vendor: Some(vendor), compatibles: vec![] }
7473    }
7474
7475    fn dt(driver: &str, compatibles: &[&str]) -> CardIdentity {
7476        CardIdentity {
7477            driver: driver.into(),
7478            pci_vendor: None,
7479            compatibles: compatibles.iter().map(|c| c.to_string()).collect(),
7480        }
7481    }
7482
7483    #[test]
7484    fn driver_names_match_without_any_table() {
7485        assert!(card_matches_token("amdgpu", &pci("amdgpu", 0x1002)));
7486        assert!(card_matches_token("panfrost", &dt("panfrost", &["rockchip,rk3399-mali"])));
7487        assert!(card_matches_token("nouveau", &pci("nouveau", 0x10de)));
7488        assert!(!card_matches_token("i915", &pci("amdgpu", 0x1002)));
7489    }
7490
7491    #[test]
7492    fn vendor_names_and_raw_ids_match_pci_identity() {
7493        let nv = pci("nouveau", 0x10de);
7494        assert!(card_matches_token("nvidia", &nv));
7495        assert!(card_matches_token("0x10de", &nv));
7496        assert!(card_matches_token("10de", &nv));
7497        assert!(!card_matches_token("amd", &nv));
7498        assert!(card_matches_token("ati", &pci("radeon", 0x1002)));
7499    }
7500
7501    #[test]
7502    fn devicetree_prefixes_match_literally_and_via_aliases() {
7503        let mali = dt("panfrost", &["rockchip,rk3399-mali", "arm,mali-t860"]);
7504        assert!(card_matches_token("rockchip", &mali));
7505        assert!(card_matches_token("arm", &mali));
7506        assert!(card_matches_token("mali", &mali));
7507        let adreno = dt("msm", &["qcom,adreno-630", "qcom,adreno"]);
7508        assert!(card_matches_token("qcom", &adreno));
7509        assert!(card_matches_token("qualcomm", &adreno));
7510        assert!(card_matches_token("adreno", &adreno));
7511        assert!(!card_matches_token("brcm", &adreno));
7512        assert!(card_matches_token("videocore", &dt("v3d", &["brcm,bcm2711-v3d"])));
7513    }
7514
7515    #[test]
7516    fn missing_identity_fields_never_false_match() {
7517        let bare = CardIdentity { driver: String::new(), pci_vendor: None, compatibles: vec![] };
7518        for t in ["nvidia", "amdgpu", "0x10de", "qcom"] {
7519            assert!(!card_matches_token(t, &bare));
7520        }
7521    }
7522}