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▙▌▌▞▖▙▖▐▖▐ ▐▖▙▌▞▖
11▌
12*/
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, H.264 through the build's
20//! software encoder — libx264 with the `gpl` feature, OpenH264 without — and the other
21//! codecs through the software encoders the build links: x265 or kvazaar, libvpx, SVT-AV1) or
22//! hardware (NVENC, VA-API) encoders based on the available GPU and operator settings.
23//!
24//! ## Crate structure
25//!
26//! | Module | Purpose |
27//! |--------|---------|
28//! | [`encoders`] | Encoder backends: software H.264 (libx264 or OpenH264) / JPEG, NVENC, VA-API, watermark overlay |
29//! | [`wayland`] | Headless Smithay compositor, cursor rendering |
30//! | [`x11`] | X11/XShm capture loop, XFixes out-of-band cursor monitor, and stripe dispatch |
31//! | [`pipeline`] | Frame-processing policy shared by both backends (send/QP/keyframe decisions) |
32//! | [`recording_sink`] | Unix-socket H.264 fan-out for external recording |
33//! | [`recorder`] | Built-in MP4 recorder (fMP4 muxer + Python/env/REST control surfaces) |
34//! | [`computer_use`] | HTTP API for AI-agent desktop control (screenshots, input injection) |
35//! | [`nvgpufilter`] | Multi-GPU NVENC device filtering via ioctl |
36//! | [`webcam`] | Virtual camera: client webcam uplink decoded into a V4L2 device (interposer ring, v4l2loopback, PipeWire node) |
37//!
38//! ## Data flow
39//!
40//! ```text
41//! Python  ──►  CaptureSettings  ──►  X11 / Wayland backend
42//!                                         │
43//!                                    frame pixels
44//!                                         │
45//!                                    ┌────┴────┐
46//!                                    │ Encoder │  (NVENC / VAAPI / x264 or OpenH264 / JPEG)
47//!                                    └────┬────┘
48//!                                         │
49//!                                   EncodedStripe(s)
50//!                                         │
51//!                                    Python callback
52//! ```
53
54#![allow(dead_code)]
55
56use std::fs::File;
57use std::sync::Arc;
58use std::thread;
59use std::time::{Duration, Instant};
60
61use gbm::{BufferObject, BufferObjectFlags, Device as RawGbmDevice, Format as GbmFormat};
62use pyo3::prelude::*;
63use pyo3::types::{PyAny, PyModule};
64
65use smithay::backend::egl::fence::EGLFence;
66use smithay::desktop::utils::{OutputPresentationFeedback, send_frames_surface_tree};
67use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
68use smithay::wayland::cursor_shape::CursorShapeManagerState;
69use smithay::wayland::image_capture_source::{ImageCaptureSourceState, OutputCaptureSourceState};
70use smithay::wayland::image_copy_capture::{CaptureFailureReason, ImageCopyCaptureState};
71use smithay::wayland::presentation::{PresentationState, Refresh};
72use smithay::wayland::selection::ext_data_control::DataControlState as ExtDataControlState;
73use smithay::wayland::selection::wlr_data_control::DataControlState;
74use smithay::wayland::single_pixel_buffer::SinglePixelBufferState;
75use smithay::wayland::viewporter::ViewporterState;
76use smithay::{
77    backend::{
78        allocator::{
79            Fourcc, Modifier,
80            dmabuf::{Dmabuf, DmabufFlags},
81            gbm::GbmDevice,
82        },
83        drm::DrmNode,
84        egl::{EGLContext, EGLDisplay},
85        input::{Axis, AxisSource, KeyState, Keycode},
86        renderer::{
87            Bind, ExportMem, Frame as _, ImportAll, ImportDma, ImportEgl, ImportMem, Renderer as _,
88            damage::OutputDamageTracker,
89            element::{
90                AsRenderElements, Element, RenderElement, Wrap,
91                memory::MemoryRenderBufferRenderElement, surface::WaylandSurfaceRenderElement,
92            },
93            gles::GlesRenderer,
94            pixman::PixmanRenderer,
95            sync::SyncPoint,
96        },
97    },
98    desktop::{LayerMap, PopupManager, layer_map_for_output},
99    desktop::{Space, space::SpaceRenderElements},
100    input::{
101        SeatState,
102        keyboard::{FilterResult, XkbConfig},
103        pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent, RelativeMotionEvent},
104    },
105    output::{Mode as OutputMode, Output, PhysicalProperties, Scale as OutputScale, Subpixel},
106    reexports::{
107        calloop::{
108            EventLoop, Interest, Mode, PostAction,
109            generic::Generic,
110            timer::{TimeoutAction, Timer},
111        },
112        pixman,
113        wayland_server::{Display, DisplayHandle},
114    },
115    utils::{Clock, Physical, Point, Rectangle, Scale, Transform},
116    wayland::selection::primary_selection::PrimarySelectionState,
117    wayland::shell::wlr_layer::WlrLayerShellState,
118    wayland::xdg_activation::XdgActivationState,
119    wayland::{
120        compositor::{CompositorState, with_states},
121        dmabuf::{DmabufFeedbackBuilder, DmabufState},
122        foreign_toplevel_list::ForeignToplevelListState,
123        fractional_scale::FractionalScaleManagerState,
124        output::OutputManagerState,
125        pointer_constraints::PointerConstraintsState,
126        pointer_warp::PointerWarpManager,
127        relative_pointer::RelativePointerManagerState,
128        seat::WaylandFocus,
129        selection::data_device::DataDeviceState,
130        shell::xdg::XdgShellState,
131        shell::xdg::decoration::XdgDecorationState,
132        shm::ShmState,
133        socket::ListeningSocketSource,
134    },
135};
136
137/// Encoder backends and the codec identities, wire framing, and rate-control policy they share.
138pub mod encoders;
139/// The debug switch behind every backend's tagged line.
140pub mod log;
141
142/// HTTP server implementing the Anthropic Computer Use spec for AI agent desktop control.
143pub mod computer_use;
144/// When a capture is due a frame, shared by the X11 and Wayland backends.
145pub mod pace;
146/// Built-in MP4 recorder: independent capture-to-file with Python/env/REST control.
147pub mod recorder;
148/// Unix-socket H.264 recording fan-out for external capture tools.
149pub mod recording_sink;
150/// What each capture streams and how it got there, as values a caller reads.
151pub mod report;
152/// Kernel uinput devices, the first rung of host-capture input injection.
153pub mod uinput;
154/// Headless Wayland compositor and cursor rendering.
155pub mod wayland;
156
157#[cfg(test)]
158mod cleanup_bench;
159/// Multi-GPU NVENC device filtering via kernel ioctl.
160pub mod nvgpufilter;
161/// Frame-processing policy shared by the X11 and Wayland backends.
162pub mod pipeline;
163/// Run-time libpipewire binding and SPA pod encoding shared by the webcam sink and host capture.
164pub mod pipewire;
165/// X11/XShm capture loop, stripe dispatch, and per-stripe change detection.
166pub mod x11;
167
168pub mod webcam;
169
170pub use encoders::nvenc;
171pub use encoders::software::StripeState;
172
173fn get_process_rss_bytes() -> usize {
174    if let Ok(contents) = std::fs::read_to_string("/proc/self/statm")
175        && let Some(rss_pages) = contents.split_whitespace().nth(1)
176        && let Ok(pages) = rss_pages.parse::<usize>()
177    {
178        return pages * 4096;
179    }
180    0
181}
182
183/// Blocks actually allocated, not apparent size, so a sparse file is not reported as
184/// memory the process took. Feeds the debug log line only.
185fn shm_usage_in(dir: &str) -> u64 {
186    use std::os::unix::fs::MetadataExt;
187    let mut total_size = 0;
188    if let Ok(entries) = std::fs::read_dir(dir) {
189        for entry in entries.flatten() {
190            if let Ok(metadata) = entry.metadata() {
191                total_size += metadata.blocks() * 512;
192            }
193        }
194    }
195    total_size
196}
197
198fn get_shm_usage_bytes() -> u64 {
199    shm_usage_in("/dev/shm")
200}
201
202use encoders::overlay::OverlayState;
203use encoders::reference::Reference;
204use encoders::software::MAX_STRIPE_CAPACITY;
205use encoders::{Codec, FrameEncoder, FrameSource};
206
207use smithay::reexports::wayland_protocols_misc::zwp_virtual_keyboard_v1::server::zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1;
208
209use pace::{FramePace, TickTrigger};
210use wayland::cursor::{Cursor, CursorJob};
211use wayland::frontend::{AppState, ClientState, FocusTarget, next_serial, wayland_time};
212
213smithay::backend::renderer::element::render_elements! {
214    pub CompositionElements<R, E> where R: ImportAll + ImportMem;
215    Space=SpaceRenderElements<R, E>,
216    Window=Wrap<E>,
217    Cursor=MemoryRenderBufferRenderElement<R>,
218    Surface=WaylandSurfaceRenderElement<R>,
219}
220
221/// Push an output's wlr-layer surfaces on `target_layer` into `elements`, top-most first, the
222/// order `layer_map.layers()` reversed gives, each with its popups and subsurfaces. Shared by
223/// both renderers and by both of a frame's layer passes, which differ only in the layers they
224/// name.
225fn push_layer_elements<R>(
226    renderer: &mut R,
227    elements: &mut Vec<CompositionElements<R, WaylandSurfaceRenderElement<R>>>,
228    layer_map: &LayerMap,
229    target_layer: smithay::wayland::shell::wlr_layer::Layer,
230    scale: f64,
231) where
232    R: smithay::backend::renderer::Renderer + ImportAll + ImportMem,
233    R::TextureId: Clone + 'static,
234{
235    for surface in layer_map.layers().rev() {
236        if surface.layer() == target_layer
237            && let Some(geo) = layer_map.layer_geometry(surface)
238        {
239            elements.extend(
240                AsRenderElements::<R>::render_elements::<WaylandSurfaceRenderElement<R>>(
241                    surface,
242                    renderer,
243                    geo.loc.to_physical_precise_round(scale),
244                    Scale::from(scale),
245                    1.0,
246                )
247                .into_iter()
248                .map(CompositionElements::Surface),
249            );
250        }
251    }
252}
253
254/// The buffer a display is rendered into and a hardware encoder reads in place: one plane, for
255/// rendering. Intel's video processor, converting to 4:2:0, reads a linear surface at a pitch
256/// rounded up to 64 bytes whatever pitch its import declares, and Mesa gives a linear buffer
257/// allocated for rendering alone the pitch of its width, so a width that is no multiple of 16
258/// reaches the encoder sheared. A buffer the display engine may scan out carries that engine's aligned pitch, so a
259/// linear allocation whose pitch is not a multiple of 64 is made again for scanout as well, and
260/// that one is kept only where it came out aligned.
261pub(crate) fn alloc_render_target<T: std::os::fd::AsFd>(
262    gbm: &RawGbmDevice<T>,
263    width: u32,
264    height: u32,
265    format: GbmFormat,
266) -> std::io::Result<BufferObject<()>> {
267    let aligned = |bo: &BufferObject<()>| {
268        Into::<u64>::into(bo.modifier()) != 0 || bo.stride().is_multiple_of(64)
269    };
270    let bo = gbm.create_buffer_object::<()>(width, height, format, BufferObjectFlags::RENDERING)?;
271    if aligned(&bo) {
272        return Ok(bo);
273    }
274    match gbm.create_buffer_object::<()>(
275        width,
276        height,
277        format,
278        BufferObjectFlags::RENDERING | BufferObjectFlags::SCANOUT,
279    ) {
280        Ok(scanout) if aligned(&scanout) => Ok(scanout),
281        _ => Ok(bo),
282    }
283}
284
285/// Export the offscreen GBM render target as a Dmabuf so the very same GPU pixels can be both
286/// rendered into and encoded with no intervening copy — the linchpin of the zero-copy capture path.
287/// The returned dmabuf is the one handle the GLES renderer binds as its framebuffer AND a hardware
288/// encoder (NVENC through CUDA, or VAAPI) imports to read those pixels directly, which only works if
289/// the buffer is described precisely enough (fd, stride, DRM modifier) for the importer to interpret
290/// it. One ARGB8888 plane is all that is carried because the compositor's offscreen target is exactly
291/// that single-plane format.
292pub(crate) fn create_dmabuf_from_bo(bo: &BufferObject<()>) -> Dmabuf {
293    let fd = bo.fd().expect("Failed to get FD from GBM BO");
294    let modifier = bo.modifier();
295    let stride = bo.stride();
296    let width = bo.width();
297    let height = bo.height();
298
299    let drm_modifier = Modifier::from(Into::<u64>::into(modifier));
300
301    let mut builder = Dmabuf::builder(
302        (width as i32, height as i32),
303        Fourcc::Argb8888,
304        drm_modifier,
305        DmabufFlags::empty(),
306    );
307
308    builder.add_plane(fd, 0, stride);
309    builder.build().expect("Failed to build Dmabuf from GBM BO")
310}
311
312/// The full set of capture + encode parameters the Python layer hands to the Rust backend.
313///
314/// A single value configures a capture session end to end: capture geometry and frame rate, the
315/// codec (striped JPEG, striped or full-frame H.264, or a full-frame video codec), the video
316/// quality and rate-control knobs,
317/// cursor and watermark options, the encode-device selection, and the optional recording socket.
318/// It derives `PartialEq` so the backend can detect when a live setting actually changed, and
319/// `Clone` so each capture pipeline can own its own copy.
320#[derive(Clone, Debug, PartialEq)]
321pub struct RustCaptureSettings {
322    pub width: i32,
323    pub height: i32,
324    pub scale: f64,
325    pub capture_x: i32,
326    pub capture_y: i32,
327    pub target_fps: f64,
328    pub jpeg_quality: i32,
329    pub paint_over_jpeg_quality: i32,
330    pub use_paint_over_quality: bool,
331    pub paint_over_trigger_frames: u32,
332    pub damage_block_threshold: u32,
333    pub damage_block_duration: u32,
334    pub codec: Codec,
335    pub video_crf: i32,
336    pub video_paintover_crf: i32,
337    pub video_paintover_burst_frames: i32,
338    pub video_fullcolor: bool,
339    /// Bits per sample asked of the encoder, 8 or 10; a session that cannot carry 10 stays at
340    /// 8 (`encoders::session_bit_depth` says which it runs).
341    pub video_bit_depth: i32,
342    pub video_fullframe: bool,
343    pub video_streaming_mode: bool,
344    pub capture_cursor: bool,
345    /// Longest cursor edge the out-of-band cursor callback delivers; larger images are
346    /// downscaled (`<= 0` = uncapped). Compositing via `capture_cursor` is unaffected.
347    pub cursor_size_cap: i32,
348    pub watermark_path: String,
349    pub watermark_location_enum: i32,
350    pub encode_node_index: i32,
351    pub use_cpu: bool,
352    pub debug_logging: bool,
353    pub auto_adjust_screen_capture_size: bool,
354    pub recording_socket: String,
355    /// Wayland display of an EXTERNAL compositor to capture (host-capture mode);
356    /// empty composites own clients as usual.
357    pub wayland_host_display: String,
358    /// When true, encoders emit the raw payload without the per-stripe header byte block;
359    /// stripe metadata is then carried only on the frame attributes.
360    pub omit_stripe_headers: bool,
361    /// The consumer acknowledges the frames every client holds (`acknowledge_reference`), so an
362    /// anchor predicts from a frame every client was sent, and where a session keeps two
363    /// long-term references (H.265, and AV1, which keeps one otherwise) the newest every client
364    /// holds stays pinned beside the one the schedule marks. H.264 keeps one.
365    pub acknowledge_references: bool,
366    /// The most frames an H.264 or H.265 stream keeps for reference (its decoded picture
367    /// buffer), 0 for as many as the level admits up to `REFERENCE_FRAMES`: a decoder that holds
368    /// its whole buffer before it shows a picture (GStreamer's hardware decoders in WebKitGTK and
369    /// WPE) shows each that many frames later.
370    pub video_reference_frames: u32,
371    pub video_cbr_mode: bool,
372    pub video_bitrate_kbps: i32,
373    /// VBV size as a multiple of one frame's bit budget (bitrate/framerate), so it rescales with
374    /// live bitrate/fps changes. Under CBR, `<= 0` selects the policy default: 1.5 on an
375    /// infinite GOP, 3 when scheduled keyframes are enabled. Under CRF, a positive value caps
376    /// the software x264 session at a peak of `video_bitrate_kbps` with this buffer, and `<= 0`
377    /// leaves it uncapped.
378    pub video_vbv_multiplier: f64,
379    /// Seconds between scheduled recovery keyframes; `<= 0` keeps the GOP infinite
380    /// (IDRs only on demand: client join / reset, recorder connect).
381    pub keyframe_interval_s: f64,
382    /// Rate-controlled (CBR) QP clamp: `video_max_qp` bounds the quality FLOOR (screen text stays
383    /// legible under motion at the cost of overshooting impossible targets), `video_min_qp` bounds
384    /// bit WASTE on easy content. 0 keeps the encoder's own default; CRF/CQP modes pin their QP
385    /// directly and ignore these.
386    pub video_min_qp: i32,
387    pub video_max_qp: i32,
388}
389
390/// The per-frame decision/quality knobs every encoder re-reads from the settings on each
391/// tick, so they retune a running capture: x264, x265, and libvpx reconfigure, NVENC and
392/// VA-API retarget their constant quantizer, kvazaar and SVT-AV1 re-open, JPEG is stateless.
393/// Applied on the thread that owns the settings copy. Structural switches (encoder, chroma,
394/// RC mode, device) still need a capture restart.
395#[derive(Clone, Copy, Debug)]
396pub struct LiveTunables {
397    pub jpeg_quality: i32,
398    pub paint_over_jpeg_quality: i32,
399    pub use_paint_over_quality: bool,
400    pub paint_over_trigger_frames: u32,
401    pub video_crf: i32,
402    pub video_paintover_crf: i32,
403    pub video_paintover_burst_frames: i32,
404    pub video_streaming_mode: bool,
405    pub keyframe_interval_s: f64,
406    pub capture_cursor: bool,
407    pub cursor_size_cap: i32,
408}
409
410impl LiveTunables {
411    /// Snapshot the live-tunable subset out of a full settings value.
412    pub fn from_settings(s: &RustCaptureSettings) -> Self {
413        Self {
414            jpeg_quality: s.jpeg_quality,
415            paint_over_jpeg_quality: s.paint_over_jpeg_quality,
416            use_paint_over_quality: s.use_paint_over_quality,
417            paint_over_trigger_frames: s.paint_over_trigger_frames,
418            video_crf: s.video_crf,
419            video_paintover_crf: s.video_paintover_crf,
420            video_paintover_burst_frames: s.video_paintover_burst_frames,
421            video_streaming_mode: s.video_streaming_mode,
422            keyframe_interval_s: s.keyframe_interval_s,
423            capture_cursor: s.capture_cursor,
424            cursor_size_cap: s.cursor_size_cap,
425        }
426    }
427
428    /// Write these live tunables back into a full settings value in place.
429    pub fn apply_to(&self, s: &mut RustCaptureSettings) {
430        s.jpeg_quality = self.jpeg_quality;
431        s.paint_over_jpeg_quality = self.paint_over_jpeg_quality;
432        s.use_paint_over_quality = self.use_paint_over_quality;
433        s.paint_over_trigger_frames = self.paint_over_trigger_frames;
434        s.video_crf = self.video_crf;
435        s.video_paintover_crf = self.video_paintover_crf;
436        s.video_paintover_burst_frames = self.video_paintover_burst_frames;
437        s.video_streaming_mode = self.video_streaming_mode;
438        s.keyframe_interval_s = self.keyframe_interval_s;
439        s.cursor_size_cap = self.cursor_size_cap;
440        s.capture_cursor = self.capture_cursor;
441    }
442}
443
444impl Default for RustCaptureSettings {
445    fn default() -> Self {
446        Self {
447            width: 1024,
448            height: 768,
449            scale: 1.0,
450            capture_x: 0,
451            capture_y: 0,
452            target_fps: 60.0,
453            jpeg_quality: 75,
454            paint_over_jpeg_quality: 95,
455            use_paint_over_quality: true,
456            paint_over_trigger_frames: 15,
457            damage_block_threshold: 10,
458            damage_block_duration: 30,
459            codec: Codec::Jpeg,
460            video_crf: 25,
461            video_paintover_crf: 18,
462            video_paintover_burst_frames: 5,
463            video_fullcolor: false,
464            video_bit_depth: 8,
465            video_fullframe: false,
466            video_streaming_mode: false,
467            capture_cursor: false,
468            cursor_size_cap: 128,
469            watermark_path: String::new(),
470            watermark_location_enum: 0,
471            encode_node_index: -2,
472            use_cpu: false,
473            debug_logging: false,
474            auto_adjust_screen_capture_size: false,
475            recording_socket: String::new(),
476            wayland_host_display: String::new(),
477            omit_stripe_headers: false,
478            acknowledge_references: false,
479            video_reference_frames: 0,
480            video_cbr_mode: false,
481            video_bitrate_kbps: 4000,
482            video_vbv_multiplier: 0.0,
483            keyframe_interval_s: 0.0,
484            video_min_qp: 0,
485            video_max_qp: 0,
486        }
487    }
488}
489
490/// Marshal a Python settings object into the plain owned Rust value both capture backends run on.
491///
492/// Fields are read by attribute name (`getattr`), not by position, so a caller can pass any object
493/// exposing the `CaptureSettings` attributes — including a subclass carrying extras. Newer/optional
494/// fields fall back to a default when absent rather than erroring, so a caller built against an
495/// older schema still starts. Both the Wayland and X11 entry points route through this one reader
496/// to prevent drift.
497///
498/// # Arguments
499///
500/// * `settings` - A Python object exposing `CaptureSettings` attributes (`capture_width`,
501///   `capture_height`, `target_fps`, `jpeg_quality`, `video_crf`, etc.).
502///
503/// # Returns
504///
505/// An owned [`RustCaptureSettings`] on success, or a Python exception if a required field is
506/// missing or has the wrong type.
507pub(crate) fn extract_settings(settings: &Bound<'_, PyAny>) -> PyResult<RustCaptureSettings> {
508    let watermark_path_obj = settings.getattr("watermark_path")?;
509    let watermark_path = if let Ok(s) = watermark_path_obj.extract::<String>() {
510        s
511    } else if let Ok(b) = watermark_path_obj.extract::<Vec<u8>>() {
512        String::from_utf8_lossy(&b).into_owned()
513    } else {
514        String::new()
515    };
516
517    let scale = settings
518        .getattr("scale")
519        .ok()
520        .and_then(|x| x.extract().ok())
521        .unwrap_or(1.0);
522
523    // These fields are public attributes any caller can set to anything an i32 or f64
524    // holds. Clamping here keeps a dimension from sizing an absurd frame buffer, and a
525    // quality outside turbojpeg's 1..=100 from failing set_quality on every stripe, which
526    // would emit no JPEG at all.
527    let sanitize_dim = |v: i32| -> i32 { if v <= 0 { 0 } else { v.min(MAX_CAPTURE_DIM) } };
528    let sanitize_fps = |v: f64| -> f64 {
529        if v.is_finite() && v > 0.0 {
530            v.min(MAX_FPS)
531        } else {
532            DEFAULT_FPS
533        }
534    };
535    let sanitize_scale = |v: f64| -> f64 {
536        if v.is_finite() && v > 0.0 {
537            v.min(MAX_SCALE)
538        } else {
539            1.0
540        }
541    };
542
543    Ok(RustCaptureSettings {
544        width: sanitize_dim(settings.getattr("capture_width")?.extract()?),
545        height: sanitize_dim(settings.getattr("capture_height")?.extract()?),
546        scale: sanitize_scale(scale),
547        capture_x: settings.getattr("capture_x")?.extract()?,
548        capture_y: settings.getattr("capture_y")?.extract()?,
549        target_fps: sanitize_fps(settings.getattr("target_fps")?.extract()?),
550        jpeg_quality: settings
551            .getattr("jpeg_quality")?
552            .extract::<i32>()?
553            .clamp(1, 100),
554        paint_over_jpeg_quality: settings
555            .getattr("paint_over_jpeg_quality")?
556            .extract::<i32>()?
557            .clamp(1, 100),
558        use_paint_over_quality: settings.getattr("use_paint_over_quality")?.extract()?,
559        paint_over_trigger_frames: settings.getattr("paint_over_trigger_frames")?.extract()?,
560        damage_block_threshold: settings.getattr("damage_block_threshold")?.extract()?,
561        damage_block_duration: settings.getattr("damage_block_duration")?.extract()?,
562        codec: {
563            let name: String = settings.getattr("codec")?.extract()?;
564            Codec::parse(&name).ok_or_else(|| {
565                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
566                    "unknown codec '{name}': expected jpeg, h264, h265, vp8, vp9, or av1"
567                ))
568            })?
569        },
570        video_crf: settings.getattr("video_crf")?.extract()?,
571        video_paintover_crf: settings.getattr("video_paintover_crf")?.extract()?,
572        video_paintover_burst_frames: settings
573            .getattr("video_paintover_burst_frames")?
574            .extract()?,
575        video_fullcolor: settings.getattr("video_fullcolor")?.extract()?,
576        video_bit_depth: settings
577            .getattr("video_bit_depth")
578            .ok()
579            .and_then(|v| v.extract::<i32>().ok())
580            .unwrap_or(8),
581        video_fullframe: settings.getattr("video_fullframe")?.extract()?,
582        video_streaming_mode: settings.getattr("video_streaming_mode")?.extract()?,
583        capture_cursor: settings.getattr("capture_cursor")?.extract()?,
584        cursor_size_cap: settings
585            .getattr("cursor_size_cap")
586            .ok()
587            .and_then(|v| v.extract::<i32>().ok())
588            .unwrap_or(RustCaptureSettings::default().cursor_size_cap),
589        watermark_path,
590        watermark_location_enum: settings.getattr("watermark_location_enum")?.extract()?,
591        encode_node_index: settings.getattr("encode_node_index")?.extract()?,
592        use_cpu: settings.getattr("use_cpu")?.extract()?,
593        debug_logging: {
594            let on: bool = settings.getattr("debug_logging")?.extract()?;
595            crate::log::set_debug(on);
596            on
597        },
598        auto_adjust_screen_capture_size: settings
599            .getattr("auto_adjust_screen_capture_size")
600            .ok()
601            .and_then(|v| v.extract::<bool>().ok())
602            .unwrap_or(false),
603        recording_socket: settings
604            .getattr("recording_socket")
605            .ok()
606            .and_then(|v| v.extract::<String>().ok())
607            .unwrap_or_default(),
608        wayland_host_display: settings
609            .getattr("wayland_host_display")
610            .ok()
611            .and_then(|v| v.extract::<String>().ok())
612            .unwrap_or_default(),
613        omit_stripe_headers: settings
614            .getattr("omit_stripe_headers")
615            .ok()
616            .and_then(|v| v.extract::<bool>().ok())
617            .unwrap_or(false),
618        acknowledge_references: settings
619            .getattr("acknowledge_references")
620            .ok()
621            .and_then(|v| v.extract::<bool>().ok())
622            .unwrap_or(false),
623        video_reference_frames: settings
624            .getattr("video_reference_frames")
625            .ok()
626            .and_then(|v| v.extract::<u32>().ok())
627            .unwrap_or(0),
628        video_cbr_mode: settings.getattr("video_cbr_mode")?.extract()?,
629        video_bitrate_kbps: settings.getattr("video_bitrate_kbps")?.extract()?,
630        video_vbv_multiplier: settings
631            .getattr("video_vbv_multiplier")
632            .ok()
633            .and_then(|v| v.extract::<f64>().ok())
634            .unwrap_or(0.0),
635        keyframe_interval_s: settings
636            .getattr("keyframe_interval_s")
637            .ok()
638            .and_then(|v| v.extract::<f64>().ok())
639            .unwrap_or(0.0),
640        video_min_qp: settings
641            .getattr("video_min_qp")
642            .ok()
643            .and_then(|v| v.extract::<i32>().ok())
644            .unwrap_or(0),
645        video_max_qp: settings
646            .getattr("video_max_qp")
647            .ok()
648            .and_then(|v| v.extract::<i32>().ok())
649            .unwrap_or(0),
650    })
651}
652
653/// One live output as `(id, x, y, width, height, scale, capturing)`. Sizes are physical
654/// pixels and `(x, y)` is the layout offset.
655pub type OutputDesc = (u32, i32, i32, i32, i32, f64, bool);
656
657/// One mapped window as `(window_id, title, app_id, output_id, parked)`. A parked window
658/// has no output yet — a nested session's spare screens.
659pub type WindowDesc = (u32, String, String, u32, bool);
660
661/// Control messages sent from the Python-facing methods to the capture thread.
662///
663/// Every interaction with a running capture crosses the thread boundary as one of these variants
664/// over the command channel: starting and stopping, injecting keyboard / pointer input, swapping
665/// the xkb keymap, serving the clipboard, changing live rate and per-frame tunables, and the
666/// computer-use queries that read back the screen, cursor, and geometry.
667pub enum ThreadCommand {
668    /// Start (or in-place reconfigure) the capture bound to output `display_id`.
669    /// `callback` is the Python per-frame delivery target; `None` starts an internal
670    /// capture with no Python consumer (the built-in recorder taps the delivery layer).
671    StartCapture {
672        display_id: u32,
673        callback: Option<Py<PyAny>>,
674        settings: RustCaptureSettings,
675    },
676    /// Stop the capture bound to output `display_id` (other displays keep running).
677    StopCapture {
678        display_id: u32,
679    },
680    /// Create an additional output: `WxH` physical pixels at fractional `scale`, mapped
681    /// into the layout at offset `(x, y)`. Replies false when the id is taken/reserved or
682    /// the GPU render target cannot be allocated.
683    CreateOutput {
684        id: u32,
685        width: i32,
686        height: i32,
687        x: i32,
688        y: i32,
689        scale: f64,
690        reply: std::sync::mpsc::Sender<bool>,
691    },
692    /// Create a view: another display over a rectangle of `owner`'s output, capturing it
693    /// without publishing a screen of its own. Replies false when the id is taken, the
694    /// owner is unknown or itself a view, or the rectangle leaves the owner's output.
695    CreateView {
696        id: u32,
697        owner: u32,
698        x: i32,
699        y: i32,
700        width: i32,
701        height: i32,
702        reply: std::sync::mpsc::Sender<bool>,
703    },
704    /// Resize a screen's output in place, keeping its position. Replies false for a view,
705    /// an unknown id, or a size that would leave one of the screen's views outside it.
706    ResizeOutput {
707        id: u32,
708        width: i32,
709        height: i32,
710        scale: f64,
711        reply: std::sync::mpsc::Sender<bool>,
712    },
713    /// Destroy a secondary output: its capture ends, its windows relocate to the primary
714    /// output. Replies false for the primary (id 0) or an unknown id.
715    DestroyOutput {
716        id: u32,
717        reply: std::sync::mpsc::Sender<bool>,
718    },
719    /// Remap an existing output (the primary included) to layout offset `(x, y)`: the
720    /// Space mapping, the offsets used for absolute input injection and cursor
721    /// compositing, and the windows placed on it all follow, and the output is damaged so
722    /// the next frames render correctly. Replies false for an unknown id.
723    RepositionOutput {
724        id: u32,
725        x: i32,
726        y: i32,
727        reply: std::sync::mpsc::Sender<bool>,
728    },
729    /// Reply with every live output as `(id, x, y, width, height, scale, capturing)`.
730    ListOutputs {
731        reply: std::sync::mpsc::Sender<Vec<OutputDesc>>,
732    },
733    /// Reply with how many displays this backend can back with real content: -1 when
734    /// self-compositing (outputs are created on demand), the host compositor's output
735    /// count in host-capture mode.
736    OutputCapacity {
737        reply: std::sync::mpsc::Sender<i64>,
738    },
739    /// Move the window with the given id onto output `output_id` (fullscreened there).
740    MoveWindowToOutput {
741        window_id: u32,
742        output_id: u32,
743        reply: std::sync::mpsc::Sender<bool>,
744    },
745    /// Reply with every mapped window as `(window_id, title, app_id, output_id)`.
746    ListWindows {
747        reply: std::sync::mpsc::Sender<Vec<WindowDesc>>,
748    },
749    SetCursorCallback(Option<Py<PyAny>>),
750    SetClipboardCallback(Py<PyAny>),
751    /// Server-side clipboard offer: the compositor owns the selection and serves one payload
752    /// per `(mime, data)` entry (plus text aliases), so a paste takes the flavor it asks for.
753    SetClipboard {
754        entries: Vec<(String, Vec<u8>)>,
755    },
756    KeyboardKey {
757        scancode: u32,
758        state: u32,
759    },
760    /// A whole ordered run of key events in one message. Typing a paste one event at a
761    /// time costs a channel send and a calloop wake per event, which competes with the
762    /// render loop on the same thread; the caller still decides the sequence.
763    KeyboardKeys {
764        events: Vec<(u32, u32)>,
765    },
766    /// Set the seat's BASE keymap from a full XKB_KEYMAP_FORMAT_TEXT_V1 string. The
767    /// compositor's keymap policy rebuilds on top: overlay binds are re-spliced onto the new
768    /// base (same keycodes) and the combined keymap is applied in one swap.
769    SetKeymapString(String),
770    /// Set the seat's BASE layout from RMLVO names (empty strings = xkbcommon defaults);
771    /// replies whether compilation succeeded. Overlay binds rebuild on top as for
772    /// `SetKeymapString`.
773    SetXkbLayout {
774        rules: String,
775        model: String,
776        layout: String,
777        variant: String,
778        options: String,
779        reply: std::sync::mpsc::Sender<bool>,
780    },
781    /// Resolve keysyms to `(keycode, level)` against the seat keymap, overlay-binding every
782    /// keysym the base cannot produce — ONE keymap swap for the whole batch, and a keycode that
783    /// is currently pressed is never recycled. `(0, 0)` marks an unbindable keysym. Serves
784    /// computer-use only; selkies resolves its own keysyms and injects plain keycodes.
785    BindKeysyms {
786        keysyms: Vec<u32>,
787        reply: std::sync::mpsc::Sender<Vec<(u32, u32)>>,
788    },
789    /// Bind explicit `(keycode, keysym)` pairs onto the CURRENT base keymap and deliver it,
790    /// in one swap. The caller decides the assignment; this only assembles and applies, so the
791    /// base is neither re-sent nor recompiled and its reverse map is not rebuilt. Ordered with
792    /// key events on the one command channel, so the keys that need the new binds cannot
793    /// overtake it — no reply is awaited, and the caller's loop is never blocked on the swap.
794    SetKeymapOverlay {
795        binds: Vec<(u32, u32)>,
796    },
797    /// Debug/verification readback: currently pressed xkb keycodes plus the modifier state
798    /// bitmask (1 ctrl, 2 shift, 4 alt, 8 logo, 16 caps, 32 num, 64 altgr, 128 level5).
799    GetKeyboardState {
800        reply: std::sync::mpsc::Sender<(Vec<u32>, u32)>,
801    },
802    /// Reply with the smithay keyboard's keymap as an XKB_KEYMAP_FORMAT_TEXT_V1 string so a
803    /// consumer (selkies) can build its reverse keysym map from the IDENTICAL keymap.
804    GetXkbKeymap {
805        reply: std::sync::mpsc::Sender<String>,
806    },
807    /// Ack once every previously queued command has been fully processed (the channel is
808    /// FIFO). The atexit sweep sends StopCapture + Barrier and waits, so the interpreter never
809    /// exits while the calloop thread is still mid-teardown (an NVENC/CUDA session drop racing
810    /// process exit segfaults).
811    Barrier {
812        reply: std::sync::mpsc::Sender<()>,
813    },
814    /// `motion` numbers the move for `pointer_location` (`WaylandBackend`); zero leaves it
815    /// unnumbered.
816    PointerMotion {
817        x: f64,
818        y: f64,
819        motion: u64,
820    },
821    PointerRelativeMotion {
822        dx: f64,
823        dy: f64,
824        motion: u64,
825    },
826    /// `btn` is an evdev `BTN_` code by contract (e.g. 272 = BTN_LEFT, 273 = BTN_RIGHT,
827    /// 274 = BTN_MIDDLE, 0x113 = BTN_SIDE / 0x114 = BTN_EXTRA for back/forward) and is passed
828    /// straight through to smithay's pointer.
829    PointerButton {
830        btn: u32,
831        state: u32,
832    },
833    PointerAxis {
834        x: f64,
835        y: f64,
836    },
837    /// A finger's scroll by (dx, dy) physical pixels, as a touchpad delivers it: no discrete
838    /// steps, ended by `PointerScrollEnd`.
839    PointerScroll {
840        dx: f64,
841        dy: f64,
842    },
843    /// The finger lifted: an axis stop, which a toolkit takes as the start of its kinetic
844    /// scroll.
845    PointerScrollEnd,
846    UpdateCursorConfig {
847        render_on_framebuffer: bool,
848    },
849    /// Recreate the cursor theme handles at a new pixel size — the calloop's compositing
850    /// helper (the burned-in cursor) and, through its job channel, the `wl-cursor` worker's
851    /// (named-cursor PNG delivery). Replies false for a non-positive size.
852    SetCursorSize {
853        size: i32,
854        reply: std::sync::mpsc::Sender<bool>,
855    },
856    /// On-demand keyframe request (client reconnect / decoder reset) for one display's
857    /// capture: forces a send and an IDR even on a static screen.
858    RequestIdr {
859        display_id: u32,
860    },
861    /// A client lost frame `frame_id` of one display's capture: the frames after it stop
862    /// predicting from it, so the next one decodes without a keyframe.
863    InvalidateReference {
864        display_id: u32,
865        frame_id: u16,
866    },
867    /// Every client holds frame `frame_id` of one display's capture, or where not `held` was
868    /// sent it.
869    AcknowledgeReference {
870        display_id: u32,
871        frame_id: u16,
872        held: bool,
873    },
874    /// Live rate-control change for one display's capture (parity with the X11 `rate_dirty`
875    /// path). Each field is `None` when that dimension is unchanged.
876    UpdateRate {
877        display_id: u32,
878        bitrate_kbps: Option<i32>,
879        vbv_multiplier: Option<f64>,
880        fps: Option<f64>,
881    },
882    /// Live per-frame tunables (quality / paint-over / streaming / cursor) for one
883    /// display's capture, mirrored to its readback encode thread — no restart.
884    UpdateTunables {
885        display_id: u32,
886        tunables: LiveTunables,
887    },
888    /// One-shot PNG of one output's next rendered frame (0 = primary); an unknown
889    /// display id replies with an error immediately.
890    CuScreenshot {
891        display_id: u32,
892        resp: std::sync::mpsc::Sender<Result<Vec<u8>, String>>,
893    },
894    CuCursorPosition {
895        resp: std::sync::mpsc::Sender<(f64, f64)>,
896    },
897    CuGetInfo {
898        display_id: u32,
899        resp: std::sync::mpsc::Sender<(i32, i32, f64)>,
900    },
901}
902
903/// Read the kernel driver bound to a render node for encoder routing.
904///
905/// An `nvidia` driver name routes to NVENC; anything else routes to VA-API. The name is
906/// lowercased for case-insensitive substring matching, and is empty when the node has no driver
907/// link (treated as "no detectable GPU" by the selection logic).
908///
909/// sysfs lists every card the kernel sees, not the ones this process was given: a container
910/// handed a single GPU still finds the host's others under `/sys/class/drm`. A driver read
911/// from a node with no device file would route the encoder at hardware it cannot open, so an
912/// absent node answers the same as an unknown one.
913///
914/// # Arguments
915///
916/// * `card_index` - DRM card index (maps to `/sys/class/drm/renderD{128 + card_index}`).
917///
918/// # Returns
919///
920/// Lowercased driver name, or an empty string if the node is unusable or has no driver link.
921pub(crate) fn get_gpu_driver(card_index: i32) -> String {
922    if !std::path::Path::new(&format!("/dev/dri/renderD{}", 128 + card_index)).exists() {
923        return String::new();
924    }
925    let path = format!("/sys/class/drm/renderD{}/device/driver", 128 + card_index);
926    match std::fs::read_link(&path) {
927        Ok(link_path) => link_path.to_string_lossy().to_lowercase(),
928        Err(_) => String::new(),
929    }
930}
931
932/// Whether an encode node's driver name routes to NVENC: NVIDIA, or unknown (no readable
933/// render-node sysfs entry, the usual shape of an NVIDIA container without /dev/dri), so the
934/// NVENC attempt runs before the CPU fallback. Any other named driver routes to VA-API. Shared
935/// by the X11 and Wayland paths so both pick the same encoder for the same node.
936pub(crate) fn driver_selects_nvenc(encode_driver: &str) -> bool {
937    encode_driver.is_empty() || encode_driver.contains("nvidia")
938}
939
940/// A DRM card's identity as the kernel reports it, read from the device's
941/// sysfs `uevent` (DRIVER=, PCI_ID=, OF_COMPATIBLE_n=) with per-file fallbacks
942/// (`vendor`, `modalias`, the `driver` symlink). uevent is uniform across buses
943/// (PCI, platform/devicetree, USB) and readable in unprivileged containers.
944struct CardIdentity {
945    driver: String,
946    pci_vendor: Option<u32>,
947    compatibles: Vec<String>,
948}
949
950/// Read a DRM card's `CardIdentity` from sysfs.
951///
952/// The device's `uevent` file is the primary source (`DRIVER=`, `PCI_ID=`, `OF_COMPATIBLE_n=`)
953/// because it is uniform across buses and readable in unprivileged containers. Each field has a
954/// fallback for cards whose `uevent` omits it: the `vendor` file for the PCI vendor, `modalias`
955/// (`of:...C<compatible>`) for the devicetree compatibles, and the `driver` symlink for the name.
956fn read_card_identity(device: &std::path::Path) -> CardIdentity {
957    let mut id = CardIdentity {
958        driver: String::new(),
959        pci_vendor: None,
960        compatibles: Vec::new(),
961    };
962    if let Ok(uevent) = std::fs::read_to_string(device.join("uevent")) {
963        for line in uevent.lines() {
964            if let Some(v) = line.strip_prefix("DRIVER=") {
965                id.driver = v.trim().to_lowercase();
966            } else if let Some(v) = line.strip_prefix("PCI_ID=") {
967                id.pci_vendor = v
968                    .split(':')
969                    .next()
970                    .and_then(|h| u32::from_str_radix(h, 16).ok());
971            } else if line.starts_with("OF_COMPATIBLE_")
972                && !line.starts_with("OF_COMPATIBLE_N")
973                && let Some(v) = line.split_once('=').map(|x| x.1)
974            {
975                id.compatibles.push(v.trim().to_lowercase());
976            }
977        }
978    }
979    if id.pci_vendor.is_none() {
980        id.pci_vendor = std::fs::read_to_string(device.join("vendor"))
981            .ok()
982            .and_then(|v| u32::from_str_radix(v.trim().trim_start_matches("0x"), 16).ok());
983    }
984    if id.compatibles.is_empty()
985        && let Ok(modalias) = std::fs::read_to_string(device.join("modalias"))
986    {
987        let modalias = modalias.trim();
988        if let Some(rest) = modalias.strip_prefix("of:") {
989            id.compatibles
990                .extend(rest.split('C').skip(1).map(|c| c.to_lowercase()));
991        }
992    }
993    if id.driver.is_empty() {
994        id.driver = std::fs::read_link(device.join("driver"))
995            .map(|p| {
996                p.file_name()
997                    .map(|n| n.to_string_lossy().to_lowercase())
998                    .unwrap_or_default()
999            })
1000            .unwrap_or_default();
1001    }
1002    id
1003}
1004
1005/// Human vendor name -> PCI vendor IDs. The kernel has no such table (the
1006/// grouping is conventional), and pci.ids/hwdata is often absent in containers,
1007/// so this is the one mapping that must be embedded — kept to the names only.
1008const VENDOR_PCI_IDS: &[(&str, &[u32])] = &[
1009    ("nvidia", &[0x10de, 0x12d2]),
1010    ("amd", &[0x1002, 0x1022]),
1011    ("ati", &[0x1002]),
1012    ("intel", &[0x8086, 0x8087]),
1013    ("arm", &[0x13b5]),
1014    ("qualcomm", &[0x5143, 0x17cb]),
1015    ("broadcom", &[0x14e4]),
1016    ("apple", &[0x106b]),
1017    ("mediatek", &[0x14c3]),
1018    ("samsung", &[0x144d]),
1019    ("vmware", &[0x15ad]),
1020    ("microsoft", &[0x1414]),
1021    ("virtio", &[0x1af4]),
1022];
1023
1024/// Human name -> devicetree vendor prefix, only where they differ (a token
1025/// equal to the prefix itself, e.g. "qcom" or "rockchip", matches directly).
1026const OF_PREFIX_ALIASES: &[(&str, &str)] = &[
1027    ("mali", "arm"),
1028    ("qualcomm", "qcom"),
1029    ("adreno", "qcom"),
1030    ("broadcom", "brcm"),
1031    ("videocore", "brcm"),
1032    ("imagination", "img"),
1033    ("powervr", "img"),
1034];
1035
1036/// Does a card match the requested token? Accepted token forms, checked against
1037/// the identity the kernel itself reports: a kernel DRIVER name (exact, no
1038/// table), a raw PCI vendor ID ("0x10de"/"10de"), a devicetree vendor prefix
1039/// (literal first segment of any compatible), or a human vendor name resolved
1040/// through the small embedded alias maps above.
1041fn card_matches_token(token: &str, id: &CardIdentity) -> bool {
1042    if !id.driver.is_empty() && token == id.driver {
1043        return true;
1044    }
1045    if let Some(vid) = id.pci_vendor {
1046        if u32::from_str_radix(token.trim_start_matches("0x"), 16) == Ok(vid) {
1047            return true;
1048        }
1049        if let Some((_, ids)) = VENDOR_PCI_IDS.iter().find(|(n, _)| *n == token)
1050            && ids.contains(&vid)
1051        {
1052            return true;
1053        }
1054    }
1055    if !id.compatibles.is_empty() {
1056        let prefix = OF_PREFIX_ALIASES
1057            .iter()
1058            .find(|(n, _)| *n == token)
1059            .map(|(_, p)| *p)
1060            .unwrap_or(token);
1061        let want = format!("{prefix},");
1062        if id.compatibles.iter().any(|c| c.starts_with(&want)) {
1063            return true;
1064        }
1065    }
1066    false
1067}
1068
1069/// Parse an auto-GPU request (the CaptureSettings `auto_gpu` field, which selkies
1070/// fills from --auto-gpu / SELKIES_AUTO_GPU). `None` = disabled; `Some(None)` =
1071/// pick the first GPU overall ("true"); `Some(Some(token))` = pick the first GPU
1072/// whose kernel identity matches the case-insensitive token (vendor name, kernel
1073/// driver name, devicetree vendor prefix, or raw PCI vendor id).
1074fn parse_auto_gpu(value: &str) -> Option<Option<String>> {
1075    let value = value.to_lowercase();
1076    match value.as_str() {
1077        "" | "false" | "0" | "off" | "no" => None,
1078        "true" | "1" | "on" | "yes" => Some(None),
1079        token => Some(Some(token.to_string())),
1080    }
1081}
1082
1083/// The render node an `auto_gpu` request picks: None where the request is off or matches
1084/// no node.
1085fn auto_render_node(auto_gpu: &str) -> Option<String> {
1086    auto_select_render_node(parse_auto_gpu(auto_gpu)?.as_deref())
1087}
1088
1089/// The index `encode_node_index` counts a render node path by (`/dev/dri/renderD{128 + idx}`).
1090fn render_node_index(path: &str) -> Option<i32> {
1091    Some(path.strip_prefix("/dev/dri/renderD")?.parse::<i32>().ok()? - 128)
1092}
1093
1094/// The index of the first render node present, which a capture that names none encodes on:
1095/// renderD128 need not exist, as in a container handed one GPU of several.
1096fn first_render_node_index() -> Option<i32> {
1097    render_node_index(&auto_select_render_node(None)?)
1098}
1099
1100/// Resolve a usable `/dev/dri/renderD*` node, optionally matching a vendor/driver token.
1101///
1102/// Cards under `/sys/class/drm` are walked in numeric order, skipping cards with no render node
1103/// (e.g. IPMI/VGA). When `/sys/class/drm` is unreadable (container without `/sys`), falls
1104/// through to scanning `/dev/dri` directly — that fallback has no device identity so a `token`
1105/// request cannot be satisfied there.
1106///
1107/// # Arguments
1108///
1109/// * `token` - Optional vendor/driver filter: a kernel driver name, PCI vendor ID (`"0x10de"`),
1110///   devicetree vendor prefix, or human vendor name (`"nvidia"`, `"intel"`). `None` picks the
1111///   first available node.
1112///
1113/// # Returns
1114///
1115/// A `/dev/dri/renderD*` path, or `None` if no matching node exists.
1116fn auto_select_render_node(token: Option<&str>) -> Option<String> {
1117    let mut cards: Vec<(u32, std::path::PathBuf)> = std::fs::read_dir("/sys/class/drm")
1118        .into_iter()
1119        .flatten()
1120        .flatten()
1121        .filter_map(|e| {
1122            let num = e
1123                .file_name()
1124                .into_string()
1125                .ok()?
1126                .strip_prefix("card")?
1127                .parse::<u32>()
1128                .ok()?;
1129            Some((num, e.path()))
1130        })
1131        .collect();
1132    cards.sort_by_key(|(n, _)| *n);
1133    for (_, path) in &cards {
1134        if let Some(t) = token
1135            && !card_matches_token(t, &read_card_identity(&path.join("device")))
1136        {
1137            continue;
1138        }
1139        if let Ok(drm_entries) = std::fs::read_dir(path.join("device/drm")) {
1140            for de in drm_entries.flatten() {
1141                let name = de.file_name().into_string().unwrap_or_default();
1142                if name.starts_with("renderD") {
1143                    let dev = format!("/dev/dri/{}", name);
1144                    if std::path::Path::new(&dev).exists() {
1145                        return Some(dev);
1146                    }
1147                }
1148            }
1149        }
1150    }
1151    if token.is_some() {
1152        return None;
1153    }
1154    let mut nodes: Vec<String> = std::fs::read_dir("/dev/dri")
1155        .ok()?
1156        .flatten()
1157        .filter_map(|e| e.file_name().into_string().ok())
1158        .filter(|n| n.starts_with("renderD"))
1159        .collect();
1160    nodes.sort();
1161    nodes.first().map(|n| format!("/dev/dri/{}", n))
1162}
1163
1164/// One captured host-pixel frame in flight from the calloop (render/readback) to the Wayland
1165/// encode thread: the pixels plus the per-frame inputs of the encode dispatch (damage,
1166/// overlay animation); the IDR request travels separately via the controls atomic.
1167pub struct WlFrame {
1168    /// Pool slot id; travels with the buffer so recycle returns it to the right slot.
1169    id: usize,
1170    buf: Vec<u8>,
1171    frame_id: u16,
1172    damage: Vec<Rectangle<i32, Physical>>,
1173    is_animated: bool,
1174    /// CLOCK_MONOTONIC nanoseconds at which the pixels were in hand.
1175    captured_ns: i64,
1176}
1177
1178/// Interior state of `WlFramePool`: the free-buffer list plus the single publish slot.
1179struct WlPoolInner {
1180    free: Vec<(usize, Vec<u8>)>,
1181    slot: Option<WlFrame>,
1182}
1183
1184/// Render->encode handoff for the Wayland readback paths, mirroring the X11 FramePool's
1185/// single-slot non-dropping design with one deliberate difference: the calloop thread is also
1186/// the compositor + input dispatcher, so it must NEVER block on the pool. `try_begin` hands
1187/// out a buffer only while the publish slot is empty, so `publish` cannot block, and a
1188/// saturated encoder throttles capture by SKIPPING ticks (compositor damage accumulates via
1189/// buffer age, so nothing is lost). Every published frame is encoded, in order: the H.264
1190/// reference chain stays contiguous exactly as on X11.
1191pub struct WlFramePool {
1192    inner: Mutex<WlPoolInner>,
1193    cv: Condvar,
1194    stop: AtomicBool,
1195}
1196
1197impl WlFramePool {
1198    /// Pre-allocate all `n` capture buffers up front (each `buf_len` bytes, all initially
1199    /// free) so the steady-state render/encode loop hands buffers around without ever allocating on
1200    /// the hot path.
1201    fn new(n: usize, buf_len: usize) -> Self {
1202        Self {
1203            inner: Mutex::new(WlPoolInner {
1204                free: (0..n).map(|i| (i, vec![0u8; buf_len])).collect(),
1205                slot: None,
1206            }),
1207            cv: Condvar::new(),
1208            stop: AtomicBool::new(false),
1209        }
1210    }
1211
1212    /// Calloop: reserve a buffer for the next render/readback, NON-blocking. None means the
1213    /// encoder is still behind (slot full or every buffer in flight) -- skip this tick.
1214    /// Because the calloop is the only producer, a successful reservation guarantees the
1215    /// following `publish` finds the slot empty (the consumer only ever drains it).
1216    fn try_begin(&self) -> Option<(usize, Vec<u8>)> {
1217        let mut g = self.inner.lock().unwrap();
1218        if g.slot.is_some() {
1219            return None;
1220        }
1221        g.free.pop()
1222    }
1223
1224    /// Calloop: hand the filled buffer to the encode thread. Never blocks (see `try_begin`).
1225    fn publish(&self, frame: WlFrame) {
1226        let mut g = self.inner.lock().unwrap();
1227        debug_assert!(g.slot.is_none());
1228        g.slot = Some(frame);
1229        drop(g);
1230        self.cv.notify_all();
1231    }
1232
1233    /// Calloop: return an unused reservation (render failed, readback skipped).
1234    fn cancel(&self, id: usize, buf: Vec<u8>) {
1235        self.inner.lock().unwrap().free.push((id, buf));
1236    }
1237
1238    /// Encode: block until a frame is published (Some) or the pool shuts down (None). The
1239    /// wait is bounded, re-checking `stop` as defense-in-depth against a lost wakeup.
1240    fn take(&self) -> Option<WlFrame> {
1241        let mut g = self.inner.lock().unwrap();
1242        loop {
1243            if let Some(f) = g.slot.take() {
1244                return Some(f);
1245            }
1246            if self.stop.load(Ordering::Acquire) {
1247                return None;
1248            }
1249            let (gg, _) = self.cv.wait_timeout(g, WL_POOL_WAKE_QUANTUM).unwrap();
1250            g = gg;
1251        }
1252    }
1253
1254    /// Encode: return an encoded frame's buffer so the calloop can capture into it again.
1255    /// No notify: the only waiter (take) waits on the slot, and try_begin never blocks.
1256    fn recycle(&self, id: usize, buf: Vec<u8>) {
1257        self.inner.lock().unwrap().free.push((id, buf));
1258    }
1259
1260    /// Store stop under the lock so take() can't check stop==false and then park after the
1261    /// notify already fired (lost wakeup); notify after unlocking.
1262    fn shutdown(&self) {
1263        let g = self.inner.lock().unwrap();
1264        self.stop.store(true, Ordering::Release);
1265        drop(g);
1266        self.cv.notify_all();
1267    }
1268}
1269
1270/// Cross-thread controls for the Wayland encode thread, the X11 `Controls` scheme: the
1271/// UpdateRate handler stores the current values then flips `rate_dirty` with Release; the
1272/// encode thread swaps it with Acquire and re-reads the payload, never seeing it half-applied.
1273/// `force_idr` is swapped just before each encode, so an on-demand keyframe lands on the
1274/// frame ALREADY in flight instead of waiting one pipeline stage for the next publish.
1275/// `reference_reports` are what clients said of frames (lost, or held by every one of them),
1276/// drained ahead of the same encode.
1277pub struct WlEncodeControls {
1278    rate_dirty: AtomicBool,
1279    bitrate_kbps: AtomicI32,
1280    vbv_mult_milli: AtomicI32,
1281    fps_bits: AtomicU64,
1282    force_idr: AtomicBool,
1283    reference_reports: Mutex<Vec<encoders::reference::ReferenceReport>>,
1284    /// Pending per-frame tunables for the encode thread (mutex, not atomics: one struct, set
1285    /// rarely, read only when the dirty flag says so).
1286    tunables_dirty: AtomicBool,
1287    tunables: Mutex<Option<LiveTunables>>,
1288}
1289
1290impl WlEncodeControls {
1291    fn new() -> Self {
1292        Self {
1293            rate_dirty: AtomicBool::new(false),
1294            bitrate_kbps: AtomicI32::new(0),
1295            vbv_mult_milli: AtomicI32::new(0),
1296            fps_bits: AtomicU64::new(0),
1297            force_idr: AtomicBool::new(false),
1298            reference_reports: Mutex::new(Vec::new()),
1299            tunables_dirty: AtomicBool::new(false),
1300            tunables: Mutex::new(None),
1301        }
1302    }
1303}
1304
1305/// Two buffers: one being encoded while the calloop fills the other. try_begin gates on the
1306/// publish slot, so a deeper pool would only add latency (staler frames), never overlap.
1307const WL_POOL_SURFACES: usize = 2;
1308/// The encode thread's bounded idle wait in `WlFramePool::take`.
1309const WL_POOL_WAKE_QUANTUM: Duration = Duration::from_millis(20);
1310/// How long a reconfigured output holds its frames while its clients answer the new size.
1311/// It is measured from the start, which rebuilds the encode path before there is any frame
1312/// to hold: the first one arrives roughly 300 ms later, so a window shorter than that
1313/// expires before it can suppress anything and the hold does nothing at all. This one
1314/// outlasts the rebuild and a client's own repaint, and is the whole delay an output that
1315/// no client ever draws on pays before its content reaches the viewer regardless.
1316const WL_CONTENT_HOLD: Duration = Duration::from_millis(500);
1317
1318/// Upper bounds for Python-supplied capture geometry: keeps a hostile or buggy setting from
1319/// turning into a multi-GB `vec![]` allocation that would abort the process. 16384 covers every
1320/// real display wall; the fps/scale bounds keep timing math finite and sane.
1321const MAX_CAPTURE_DIM: i32 = 16384;
1322const MAX_FPS: f64 = 1000.0;
1323/// How often frame callbacks go out while nothing is being captured. Fast enough that a
1324/// client blocked on one keeps making progress, slow enough that an unwatched session is
1325/// not drawing frames nobody asked for.
1326const IDLE_FRAME_INTERVAL: Duration = Duration::from_millis(250);
1327const DEFAULT_FPS: f64 = 60.0;
1328
1329/// The frame period for a target rate. Settings are sanitized at the Python boundary; a rate
1330/// outside the sane range takes the default here so a non-finite one never reaches
1331/// `Duration::from_secs_f64`.
1332fn frame_period(fps: f64) -> Duration {
1333    let fps = if fps.is_finite() && fps > 0.0 {
1334        fps.min(MAX_FPS)
1335    } else {
1336        DEFAULT_FPS
1337    };
1338    Duration::from_secs_f64(1.0 / fps)
1339}
1340
1341/// The frame period a capture renders at.
1342fn capture_period(cap: &wayland::frontend::WlCapture) -> Duration {
1343    frame_period(cap.settings.target_fps)
1344}
1345const MAX_SCALE: f64 = 8.0;
1346/// Wheel v120 units per unit of injected scroll value: selkies sends 10 per notch, and one
1347/// notch is 120, so both the seat's v120 and the host virtual pointer's discrete steps derive
1348/// from the same value.
1349pub(crate) const SCROLL_V120_PER_UNIT: f64 = 12.0;
1350
1351/// Shared capture stats: whichever thread owns the encoders counts frames/stripes and
1352/// composes `desc` + `n_stripes` (the encoder half of the 1 s debug log line); the calloop
1353/// log loads, prints, and resets the counters.
1354pub struct WlEncodeStats {
1355    frames: AtomicU32,
1356    stripes: AtomicU32,
1357    n_stripes: AtomicU32,
1358    desc: Mutex<String>,
1359}
1360
1361impl WlEncodeStats {
1362    fn new() -> Self {
1363        Self {
1364            frames: AtomicU32::new(0),
1365            stripes: AtomicU32::new(0),
1366            n_stripes: AtomicU32::new(1),
1367            desc: Mutex::new(String::new()),
1368        }
1369    }
1370}
1371
1372/// Everything the Wayland encode thread needs, fixed for the life of one capture (a
1373/// StartCapture reconfigure tears the thread down and spawns a fresh one). Rate changes
1374/// flow through `controls`; damage and the IDR request arrive per-frame in `WlFrame`.
1375struct WlEncodeConfig {
1376    settings: RustCaptureSettings,
1377    /// Output/display id this encode loop serves; keys the recorder's delivery-layer tap.
1378    display_id: u32,
1379    /// GLES readback is RGBA; the pixman framebuffer is BGRA. Selects CSC + encoder input kind.
1380    use_gpu: bool,
1381    /// Attempt a HW (NVENC/VAAPI) readback session before falling back to the CPU encoders.
1382    try_gpu: bool,
1383    /// HW session handed back by the previous encode thread; reconfigured in place and
1384    /// reused when still compatible, sparing the stream a session rebuild.
1385    prior: Option<FrameEncoder>,
1386    /// The outgoing encode thread of a capture being restarted, joined here — off the
1387    /// calloop — before this thread builds anything. Everything the predecessor did
1388    /// therefore precedes everything this thread does, and its readback hardware session is
1389    /// inherited rather than rebuilt.
1390    predecessor: Option<std::thread::JoinHandle<Option<FrameEncoder>>>,
1391    /// Weak, so the calloop's teardown owns the sink's lifetime: a strong handle held here
1392    /// could outlive the capture and unlink a successor's freshly bound socket path.
1393    recording_sink: Option<std::sync::Weak<crate::recording_sink::RecordingSink>>,
1394    deliver_tx: std::sync::mpsc::SyncSender<Vec<EncodedStripe>>,
1395    controls: Arc<WlEncodeControls>,
1396    stats: Arc<WlEncodeStats>,
1397    report: Arc<report::StreamReport>,
1398}
1399
1400/// Build the readback-mode frame encoder on the thread that will own and drive it: the shared
1401/// ladder with host frames (`rgba` for a GLES readback, BGRA otherwise), with the GPU rungs
1402/// skipped when `try_gpu` is off (the recovery ladder's demotion). `None` is the striped
1403/// software path, where `encode_cpu` builds its own per-stripe state. A compatible NVENC
1404/// session handed over from the previous encode thread is reconfigured in place rather than
1405/// rebuilt, which would stall the stream.
1406fn build_readback_encoders(
1407    settings: &mut RustCaptureSettings,
1408    try_gpu: bool,
1409    rgba: bool,
1410    prior: Option<FrameEncoder>,
1411) -> Option<FrameEncoder> {
1412    let mut attempt = settings.clone();
1413    if !try_gpu {
1414        attempt.use_cpu = true;
1415    }
1416    let encoder =
1417        encoders::select_frame_encoder(&mut attempt, FrameSource::Host { rgba }, prior, "Wayland");
1418    settings.codec = attempt.codec;
1419    encoder
1420}
1421
1422/// Encode-thread body for the Wayland readback paths: drain published frames and run the
1423/// full encode dispatch, owning the encoders for the life of the capture.
1424///
1425/// Owning the encoders on this one thread (created, driven, and dropped here) is what lets the
1426/// calloop overlap the next render/readback with this encode — the same capture‖encode split used
1427/// on X11, minus the renderer, which is genuinely calloop-affine (EGL/GBM/dmabuf and the pixman
1428/// targets). Each published frame is processed in order so the H.264 reference chain stays
1429/// contiguous:
1430///
1431/// 1. **Apply cross-thread changes**: live tunables and rate/VBV/fps updates are read here, on the
1432///    thread that owns the encoders (each `Acquire` swap pairs with the command handler's `Release`
1433///    store, so a payload is never seen half-applied). The IDR request is swapped as late as
1434///    possible, so a request that arrived while this frame was in flight is honored one pipeline
1435///    stage earlier than the next publish.
1436/// 2. **Dispatch by encoder**: a full-frame session runs `decide_hw_fullframe`, then hands only
1437///    the frames actually being encoded to it through `FrameEncoder::encode_host` with the packed
1438///    rows as they are (RGBA vs BGRA source chosen by the renderer) — NVENC uploads and converts
1439///    in hardware, a VA-API session uploads and converts on the GPU, a software session converts
1440///    on its encode threads; otherwise `encode_cpu` runs the striped software path with
1441///    compositor damage — JPEG, or H.264 through the build's software encoder, striped or
1442///    full-frame. The software H.264 path keeps an infinite GOP, forcing an IDR only on an
1443///    explicit request or the configured interval, and an explicit request also forces a full
1444///    JPEG resend for joiners.
1445/// 3. **Recycle then deliver**: the capture buffer is recycled BEFORE delivery so a slow consumer
1446///    never pins one, then the stripes go to the delivery thread through a single-slot `send` whose
1447///    blocking is the backpressure that overlaps delivery with the next render + encode.
1448///
1449/// On exit the hardware session is handed back to the calloop so a restart can reuse it in place
1450/// when the new settings stay compatible (a plain `StopCapture` just drops it).
1451fn wayland_encode_loop(pool: &WlFramePool, cfg: WlEncodeConfig) -> Option<FrameEncoder> {
1452    crate::boost_thread_priority(-10);
1453    let _report = report::enter(&cfg.report);
1454    let mut settings = cfg.settings;
1455    let inherited = cfg.predecessor.and_then(|h| h.join().ok().flatten());
1456    let mut video_encoder = build_readback_encoders(
1457        &mut settings,
1458        cfg.try_gpu,
1459        cfg.use_gpu,
1460        cfg.prior.or(inherited),
1461    );
1462    if cfg.try_gpu && video_encoder.is_none() {
1463        println!(
1464            "[Wayland] Readback encode: no hardware encoder opened; encoding in software ({}).",
1465            encoders::software_library(settings.codec)
1466        );
1467    }
1468    let n_stripes = wayland_stripe_count(&settings, video_encoder.is_some());
1469    cfg.stats
1470        .n_stripes
1471        .store(n_stripes as u32, Ordering::Relaxed);
1472    *cfg.stats.desc.lock().unwrap() = encoder_desc(&settings, video_encoder.as_ref(), false);
1473    set_wayland_active_codec(cfg.display_id, Some(settings.codec));
1474    log_stream_settings("Wayland", &settings, n_stripes, video_encoder.as_ref());
1475
1476    let width = settings.width;
1477    let height = settings.height;
1478    let mut stripes: Vec<StripeState> = Vec::with_capacity(MAX_STRIPE_CAPACITY);
1479    // Smoothed number of stripes carrying the encode budget (see stripe_rate_control).
1480    let mut stripes_carrying: f32 = 1.0;
1481    let mut hw_state = StripeState::default();
1482    // Mid-stream recovery state for the readback hardware session, mirroring the zero-copy tick.
1483    let mut hw_error_streak: u32 = 0;
1484    let mut hw_rebuilt = false;
1485
1486    while let Some(mut f) = pool.take() {
1487        if cfg.controls.tunables_dirty.swap(false, Ordering::Acquire)
1488            && let Some(t) = cfg.controls.tunables.lock().unwrap().take()
1489        {
1490            t.apply_to(&mut settings);
1491        }
1492        if cfg.controls.rate_dirty.swap(false, Ordering::Acquire) {
1493            settings.video_bitrate_kbps = cfg.controls.bitrate_kbps.load(Ordering::Relaxed);
1494            settings.video_vbv_multiplier =
1495                cfg.controls.vbv_mult_milli.load(Ordering::Relaxed) as f64 / 1000.0;
1496            let fps = f64::from_bits(cfg.controls.fps_bits.load(Ordering::Relaxed));
1497            if fps > 0.0 {
1498                settings.target_fps = fps;
1499            }
1500            if let Some(enc) = video_encoder.as_mut()
1501                && let Err(e) = enc.reconfigure_rate(&settings)
1502            {
1503                // The failed re-open left no codec context: the next encode fails, and a
1504                // full streak makes that failure run the recovery ladder at once instead
1505                // of after a window of dead frames.
1506                eprintln!("[Wayland] rate reconfigure failed: {e}");
1507                hw_error_streak = HW_ERROR_RECOVERY_THRESHOLD - 1;
1508            }
1509        }
1510
1511        // A recorder connecting counts as a request, so the decision layer sends a
1512        // decodable frame even when the screen is static. A sink whose capture has been
1513        // torn down is already gone, and its last frames are not recorded.
1514        let recording_sink = cfg.recording_sink.as_ref().and_then(|w| w.upgrade());
1515        for report in std::mem::take(&mut *cfg.controls.reference_reports.lock().unwrap()) {
1516            let taken = match (video_encoder.as_mut(), report) {
1517                (Some(encoder), report) => encoder.take_report(report),
1518                (None, encoders::reference::ReferenceReport::Lost(frame_id)) => {
1519                    encoders::software::invalidate_reference(&mut stripes, frame_id)
1520                }
1521                (None, encoders::reference::ReferenceReport::Held(frame_id)) => {
1522                    encoders::software::acknowledge_reference(&mut stripes, frame_id, true);
1523                    true
1524                }
1525                (None, encoders::reference::ReferenceReport::Sent(frame_id)) => {
1526                    encoders::software::acknowledge_reference(&mut stripes, frame_id, false);
1527                    true
1528                }
1529            };
1530            if !taken {
1531                cfg.controls.force_idr.store(true, Ordering::Relaxed);
1532            }
1533        }
1534        let requested_idr = cfg.controls.force_idr.swap(false, Ordering::Relaxed)
1535            || recording_sink
1536                .as_ref()
1537                .is_some_and(|s| s.should_force_idr());
1538
1539        let mut out: Vec<EncodedStripe> = Vec::new();
1540        if let Some(ref mut encoder) = video_encoder {
1541            let decision = crate::pipeline::decide_hw_fullframe(
1542                &mut hw_state,
1543                &settings,
1544                f.frame_id,
1545                crate::pipeline::Damage::of_rects(&f.damage, width, height),
1546                f.is_animated,
1547                requested_idr,
1548                crate::pipeline::EncoderQuality::of(encoder),
1549            );
1550            if decision.send || encoder.holds_frame() {
1551                let w = width as u32;
1552                let force_idr = decision.force_idr;
1553                decision.prepare(encoder);
1554                // The readback rows go to the encoder as they are — BGRA from the pixman
1555                // framebuffer or a host frame, RGBA from a GLES readback: a hardware session
1556                // converts on the GPU and a software one on its own threads, so no color
1557                // conversion runs here.
1558                let encode_start_ns = wayland::host::now_ns();
1559                let outcome = if decision.send {
1560                    encoder.encode_host(
1561                        &f.buf,
1562                        (w * 4) as usize,
1563                        cfg.use_gpu,
1564                        f.frame_id as u64,
1565                        decision.target_qp,
1566                        force_idr,
1567                    )
1568                } else {
1569                    encoder.push_held(f.frame_id as u64)
1570                };
1571                match outcome {
1572                    Ok(data) => {
1573                        hw_error_streak = 0;
1574                        hw_rebuilt = false;
1575                        if !data.is_empty() {
1576                            let encode_end_ns = wayland::host::now_ns();
1577                            for (data, id, reference) in encoder.delivered_units(data, f.frame_id) {
1578                                out.push(EncodedStripe {
1579                                    data: Arc::new(data),
1580                                    codec: settings.codec,
1581                                    stripe_y_start: 0,
1582                                    stripe_height: height,
1583                                    frame_id: id as i32,
1584                                    timing: FrameTiming {
1585                                        capture_ns: f.captured_ns,
1586                                        encode_start_ns,
1587                                        encode_end_ns,
1588                                    },
1589                                    reference,
1590                                });
1591                            }
1592                        }
1593                    }
1594                    Err(e) => {
1595                        // One line per recovery window: a session failing at frame rate would
1596                        // otherwise write a line per frame for the life of the capture.
1597                        if hw_error_streak.is_multiple_of(HW_ERROR_RECOVERY_THRESHOLD) {
1598                            eprintln!("[Wayland] HW encode error: {e}");
1599                        }
1600                        hw_error_streak = hw_error_streak.saturating_add(1);
1601                        if hw_error_streak >= HW_ERROR_RECOVERY_THRESHOLD {
1602                            // The readback session persistently fails after having worked:
1603                            // rebuild it once with the startup selection, then demote to the
1604                            // software encoders. A session whose encodes keep failing still
1605                            // constructs, so the rebuild only counts as recovery until the
1606                            // next streak; otherwise the stream would rebuild in a loop.
1607                            hw_error_streak = 0;
1608                            let try_gpu = !hw_rebuilt;
1609                            if try_gpu {
1610                                eprintln!(
1611                                    "[Wayland] rebuilding readback HW encoder after repeated encode errors."
1612                                );
1613                            } else {
1614                                eprintln!(
1615                                    "[Wayland] readback HW encoder unrecoverable; demoting to software encoding ({}).",
1616                                    encoders::software_library(settings.codec)
1617                                );
1618                            }
1619                            // The broken session is released before its replacement is opened:
1620                            // the failure it recovers from is usually device memory pressure,
1621                            // and holding both at once is what would make the rebuild fail too.
1622                            drop(video_encoder.take());
1623                            video_encoder =
1624                                build_readback_encoders(&mut settings, try_gpu, cfg.use_gpu, None);
1625                            if !try_gpu {
1626                                report::encoder_reason(
1627                                    "the hardware encoder failed repeatedly and was given up",
1628                                );
1629                            }
1630                            hw_rebuilt = try_gpu && video_encoder.is_some();
1631                            cfg.controls.force_idr.store(true, Ordering::Relaxed);
1632                            let n = wayland_stripe_count(&settings, video_encoder.is_some());
1633                            cfg.stats.n_stripes.store(n as u32, Ordering::Relaxed);
1634                            *cfg.stats.desc.lock().unwrap() =
1635                                encoder_desc(&settings, video_encoder.as_ref(), false);
1636                            set_wayland_active_codec(cfg.display_id, Some(settings.codec));
1637                            log_stream_settings("Wayland", &settings, n, video_encoder.as_ref());
1638                        }
1639                    }
1640                }
1641            }
1642        } else {
1643            let mut damage = std::mem::take(&mut f.damage);
1644            if f.is_animated {
1645                damage.push(Rectangle::new((0, 0).into(), (width, height).into()));
1646            }
1647            let force_idr_all = requested_idr
1648                || (settings.codec.is_video()
1649                    && crate::pipeline::periodic_idr_due(&settings, f.frame_id));
1650            let encode_start_ns = wayland::host::now_ns();
1651            out = encoders::software::encode_cpu(
1652                &mut stripes,
1653                &mut stripes_carrying,
1654                &f.buf,
1655                width,
1656                height,
1657                &damage,
1658                &settings,
1659                f.frame_id,
1660                cfg.use_gpu,
1661                false,
1662                force_idr_all,
1663            );
1664            FrameTiming::stamp(&mut out, f.captured_ns, encode_start_ns);
1665        }
1666
1667        let WlFrame { id, buf, .. } = f;
1668        pool.recycle(id, buf);
1669        // An unserved request stays armed: on an infinite GOP an IDR lost to an encode
1670        // error or skip would never self-heal.
1671        if requested_idr && out.is_empty() {
1672            cfg.controls.force_idr.store(true, Ordering::Relaxed);
1673        }
1674        if !out.is_empty() {
1675            cfg.stats.frames.fetch_add(1, Ordering::Relaxed);
1676            cfg.stats
1677                .stripes
1678                .fetch_add(out.len() as u32, Ordering::Relaxed);
1679            if let Some(ref socket) = recording_sink {
1680                socket.write_frame(&out, settings.width, settings.height);
1681            }
1682            crate::recorder::wayland_tap(cfg.display_id, &out);
1683            let _ = cfg.deliver_tx.send(out);
1684        }
1685    }
1686    crate::log::debug!(
1687        "[Wayland] Encode thread exiting (hw={}, stripes={}).",
1688        video_encoder.is_some(),
1689        stripes.len()
1690    );
1691    video_encoder
1692}
1693
1694/// Compose the encoder half of the 1 s debug log line (backend, colorspace, frame mode, rate
1695/// control) for whichever thread owns the encoders.
1696fn encoder_desc(
1697    settings: &RustCaptureSettings,
1698    video_encoder: Option<&FrameEncoder>,
1699    zero_copy: bool,
1700) -> String {
1701    if !settings.codec.is_video() {
1702        return format!("JPEG Q:{}", settings.jpeg_quality);
1703    }
1704    let copy_mode = if zero_copy { "ZeroCopy" } else { "Readback" };
1705    let backend = match video_encoder {
1706        Some(enc) if enc.is_hardware() => format!("{} ({})", enc.backend_name(), copy_mode),
1707        Some(enc) => format!("CPU {}", enc.backend_name()),
1708        None => format!("CPU {}", encoders::software_library(Codec::H264)),
1709    };
1710    let is_444 = encoders::session_fullcolor(video_encoder, settings);
1711    let cs_str = if is_444 { "CS_IN:I444" } else { "CS_IN:I420" };
1712    let range_str = if encoders::session_full_range(video_encoder, settings) {
1713        "FR"
1714    } else {
1715        "LR"
1716    };
1717    let frame_str = if video_encoder.is_some() || settings.video_fullframe {
1718        "FF"
1719    } else {
1720        "Striped"
1721    };
1722    format!(
1723        "{} ({}) {} {} {} {}",
1724        settings.codec.display(),
1725        backend,
1726        cs_str,
1727        range_str,
1728        frame_str,
1729        encoders::rate_desc(
1730            settings,
1731            video_encoder.and_then(FrameEncoder::fixed_rate_control)
1732        )
1733    )
1734}
1735
1736/// How many horizontal stripes a frame is split into, for the settings line and the stats.
1737///
1738/// A full-frame session (`fullframe_encoder`: a HW encoder, or a forced `video_fullframe`) is
1739/// one contiguous H.264 stream and so a single stripe. Everything else asks the encoder's own
1740/// rule, so what is reported is what is encoded.
1741fn wayland_stripe_count(settings: &RustCaptureSettings, fullframe_encoder: bool) -> usize {
1742    crate::encoders::software::stripe_count(
1743        settings.height,
1744        settings.codec,
1745        fullframe_encoder || settings.video_fullframe,
1746    )
1747}
1748
1749/// One-shot "Stream settings active" line, printed by the thread that owns the encoders
1750/// once the selection is final (calloop for zero-copy, encode thread for readback, the
1751/// X11 zero-copy backends at their start), tagged for the backend that prints it.
1752pub(crate) fn log_stream_settings(
1753    tag: &str,
1754    settings: &RustCaptureSettings,
1755    n_stripes: usize,
1756    video_encoder: Option<&FrameEncoder>,
1757) {
1758    let backend = video_encoder.map(|enc| (enc.backend_name(), enc.is_hardware()));
1759    let fixed_rate = video_encoder.and_then(FrameEncoder::fixed_rate_control);
1760    let holds = video_encoder.map_or_else(
1761        || encoders::software::stripes_hold_quantizer(settings),
1762        FrameEncoder::holds_quantizer,
1763    );
1764    let fullcolor = encoders::session_fullcolor(video_encoder, settings);
1765    let full_range = encoders::session_full_range(video_encoder, settings);
1766    log_stream_settings_of(
1767        tag, settings, n_stripes, backend, fixed_rate, holds, fullcolor, full_range,
1768    );
1769    report::bit_depth(encoders::session_bit_depth(video_encoder, settings));
1770}
1771
1772/// The "Stream settings active" line for a backend named outright, `(name, hardware)`, where
1773/// the session is not a `FrameEncoder` (NvFBC's own NVENC session); `None` is the striped
1774/// software path, `fixed_rate` is the backend's one rate control where it has one
1775/// (`FrameEncoder::fixed_rate_control`), and `holds` says whether the session holds a cleanup
1776/// at the quantizer asked for (`FrameEncoder::holds_quantizer`).
1777#[allow(clippy::too_many_arguments)]
1778pub(crate) fn log_stream_settings_of(
1779    tag: &str,
1780    settings: &RustCaptureSettings,
1781    n_stripes: usize,
1782    backend: Option<(&str, bool)>,
1783    fixed_rate: Option<&str>,
1784    holds: bool,
1785    fullcolor: bool,
1786    full_range: bool,
1787) {
1788    report::stream(settings, n_stripes, backend, fullcolor, full_range);
1789    println!(
1790        "{}",
1791        stream_settings_line(
1792            tag, settings, n_stripes, backend, fixed_rate, holds, fullcolor, full_range
1793        )
1794    );
1795}
1796
1797/// The text of the "Stream settings active" line, which says what the session applies rather
1798/// than what it was asked for (`encoders::rate_desc`, `encoders::paint_over_desc`).
1799#[allow(clippy::too_many_arguments)]
1800fn stream_settings_line(
1801    tag: &str,
1802    settings: &RustCaptureSettings,
1803    n_stripes: usize,
1804    backend: Option<(&str, bool)>,
1805    fixed_rate: Option<&str>,
1806    holds: bool,
1807    fullcolor: bool,
1808    full_range: bool,
1809) -> String {
1810    let mut log_msg = format!(
1811        "[{tag}] Stream settings active -> Res: {}x{} | FPS: {:.1} | Stripes: {}",
1812        settings.width, settings.height, settings.target_fps, n_stripes
1813    );
1814
1815    if !settings.codec.is_video() {
1816        log_msg.push_str(&format!(
1817            " | Mode: JPEG | Quality: {}",
1818            settings.jpeg_quality
1819        ));
1820        if let Some(paint_over) = encoders::paint_over_desc(settings, fixed_rate, holds) {
1821            log_msg.push_str(&format!(" | {paint_over}"));
1822        }
1823    } else {
1824        let encoder_type = match backend {
1825            Some((name, _)) => name,
1826            None => encoders::software_library(Codec::H264),
1827        };
1828        log_msg.push_str(&format!(
1829            " | Mode: {} ({})",
1830            settings.codec.display(),
1831            encoder_type
1832        ));
1833
1834        if backend.is_some() || settings.video_fullframe {
1835            log_msg.push_str(" FullFrame");
1836        } else {
1837            log_msg.push_str(" Striped");
1838        }
1839
1840        if settings.video_streaming_mode {
1841            log_msg.push_str(" Streaming");
1842        }
1843
1844        log_msg.push_str(&format!(" | {}", encoders::rate_desc(settings, fixed_rate)));
1845        if let Some(paint_over) = encoders::paint_over_desc(settings, fixed_rate, holds) {
1846            log_msg.push_str(&format!(" | {paint_over}"));
1847        }
1848
1849        log_msg.push_str(&format!(
1850            " | Colorspace: {}",
1851            encoders::colorspace_desc(fullcolor, full_range)
1852        ));
1853    }
1854
1855    log_msg.push_str(&format!(
1856        " | Damage Thresh: {}f | Damage Dur: {}f",
1857        settings.damage_block_threshold, settings.damage_block_duration
1858    ));
1859    log_msg
1860}
1861
1862#[cfg(test)]
1863mod stream_settings_line_tests {
1864    use super::*;
1865
1866    /// The line never states a rate control the encoder does not run: a CRF session names no
1867    /// bitrate, a backend with one rate control names that one in either mode, and a JPEG
1868    /// paint-over that cannot raise the quality is not named.
1869    #[test]
1870    fn the_line_states_what_the_session_applies() {
1871        let crf = RustCaptureSettings {
1872            codec: Codec::H264,
1873            video_crf: 25,
1874            video_bitrate_kbps: 8000,
1875            use_paint_over_quality: false,
1876            ..Default::default()
1877        };
1878        let line = |s: &RustCaptureSettings, fixed: Option<&str>| {
1879            stream_settings_line(
1880                "X11",
1881                s,
1882                1,
1883                Some(("NVENC", true)),
1884                fixed,
1885                fixed.is_none(),
1886                false,
1887                false,
1888            )
1889        };
1890        let nvenc = line(&crf, None);
1891        assert!(
1892            nvenc.contains("| CRF: 25 |") && !nvenc.contains("VBV") && !nvenc.contains("8000"),
1893            "{nvenc}"
1894        );
1895        let v4l2 = line(&crf, Some("VBR"));
1896        assert!(
1897            v4l2.contains("| VBR 8000 |") && !v4l2.contains("CRF"),
1898            "{v4l2}"
1899        );
1900        assert!(line(&crf, Some("CBR")).contains("| CBR 8000 |"));
1901        assert!(
1902            line(
1903                &RustCaptureSettings {
1904                    video_cbr_mode: true,
1905                    ..crf.clone()
1906                },
1907                None
1908            )
1909            .contains("| CBR 8000 |")
1910        );
1911        let jpeg = RustCaptureSettings {
1912            codec: Codec::Jpeg,
1913            jpeg_quality: 75,
1914            paint_over_jpeg_quality: 95,
1915            ..Default::default()
1916        };
1917        assert!(line(&jpeg, None).contains("| PaintOver Q: 95 "));
1918        assert!(
1919            !line(
1920                &RustCaptureSettings {
1921                    paint_over_jpeg_quality: 60,
1922                    ..jpeg
1923                },
1924                None
1925            )
1926            .contains("PaintOver")
1927        );
1928    }
1929}
1930
1931/// Tear down a capture's encode/delivery threads and pools, returning both join handles as
1932/// `(deliver, encode)` for the caller to place. NEITHER is joined here: this runs on the
1933/// calloop thread, which also dispatches Wayland clients and input, and either thread can be
1934/// parked behind a consumer still inside Python. A restart hands each handle to its
1935/// successor thread, which joins it off the event loop — the encode successor inheriting the
1936/// readback hardware session, the deliver successor keeping stripes in capture order across
1937/// the restart — and a plain stop hands both to the reaper. The zero-copy session (if any)
1938/// is left on the capture for the caller to reuse or drop.
1939fn teardown_capture(
1940    cap: &mut wayland::frontend::WlCapture,
1941) -> (
1942    Option<std::thread::JoinHandle<()>>,
1943    Option<std::thread::JoinHandle<Option<FrameEncoder>>>,
1944) {
1945    if let Some(p) = cap.encode_pool.take() {
1946        p.shutdown();
1947    }
1948    if let Some(flag) = cap.deliver_discard.take() {
1949        flag.store(true, Ordering::Relaxed);
1950    }
1951    let encode_join = cap.encode_join.take();
1952    if let Some(tx) = cap.deliver_tx.take() {
1953        drop(tx);
1954    }
1955    let join = cap.deliver_join.take();
1956    cap.pending_hw_delivery = None;
1957    cap.pending_hw_damage = false;
1958    // The last strong reference: dropping it here, before a following start binds its own
1959    // sink, is what keeps the outgoing sink's unlink from stripping the successor's socket
1960    // path. The encode thread holds only a Weak.
1961    cap.recording_sink = None;
1962    (join, encode_join)
1963}
1964
1965/// Drop a host-capture session whose compositor connection has ended, stopping every
1966/// capture it fed: their displays leave the alive set, so `is_capturing` turns false and the
1967/// consumer rebuilds them (the next start reconnects), and no input is written to a dead
1968/// connection. The same signal X11 gives when its capture thread ends.
1969fn reap_dead_host(state: &mut AppState) {
1970    if !state.host.as_ref().is_some_and(|h| !h.alive()) {
1971        return;
1972    }
1973    eprintln!("[HostCapture] host compositor connection lost; captures stop until restarted.");
1974    let ids: Vec<u32> = state
1975        .output_nodes
1976        .iter()
1977        .filter(|n| n.capture.is_some())
1978        .map(|n| n.id)
1979        .collect();
1980    for id in ids {
1981        stop_capture_on_display(state, id);
1982        // stop_capture_on_display cleared this display's outcome; overwrite it with the
1983        // host-death reason so capture_state reports why the pipeline went down (input is
1984        // black-holed to a dead connection until a fresh start rebuilds against a new one).
1985        set_wayland_capture_err(
1986            id,
1987            Some("host compositor connection lost; capture stopped".to_string()),
1988        );
1989    }
1990    state.host = None;
1991    state.host_mode_refusals.clear();
1992    // Nothing will answer the requests still in flight: their geometry readers get
1993    // what the outputs show now.
1994    let pending: Vec<_> = state.host_layout_pending.drain().collect();
1995    for (id, p) in pending {
1996        answer_geometry_waiters(state, id, p.geometry_waiters);
1997    }
1998}
1999
2000/// Stop the capture bound to `display_id`, leaving the output (and every other display's
2001/// capture) running.
2002fn stop_capture_on_display(state: &mut AppState, display_id: u32) {
2003    let Some(idx) = state.node_idx_for_id(display_id) else {
2004        return;
2005    };
2006    if let Some(mut cap) = state.output_nodes[idx].capture.take() {
2007        crate::log::debug!("[Wayland] Capture loop stopped (display {display_id}).");
2008        cap.video_encoder = None;
2009        let (join, encode_join) = teardown_capture(&mut cap);
2010        state.deliver_reaper.extend(join);
2011        state.encode_reaper.extend(encode_join);
2012    }
2013    wayland_alive().lock().unwrap().remove(&display_id);
2014    set_wayland_capture_err(display_id, None);
2015    set_wayland_active_codec(display_id, None);
2016    report::wayland_reports()
2017        .lock()
2018        .unwrap()
2019        .remove(&display_id);
2020    if let Some(p) = state.host_layout_pending.remove(&display_id) {
2021        answer_geometry_waiters(state, display_id, p.geometry_waiters);
2022    }
2023}
2024
2025/// The geometry `get_realized_geometry` reports for `display_id`: the live capture's size
2026/// and scale, else the output's current mode, else zeros for an unknown display.
2027fn realized_geometry(state: &AppState, display_id: u32) -> (i32, i32, f64) {
2028    state
2029        .node_idx_for_id(display_id)
2030        .map(|idx| {
2031            let node = &state.output_nodes[idx];
2032            match node.capture.as_ref() {
2033                Some(c) => (c.settings.width, c.settings.height, c.settings.scale),
2034                None => node
2035                    .output
2036                    .current_mode()
2037                    .map(|m| {
2038                        (
2039                            m.size.w,
2040                            m.size.h,
2041                            node.output.current_scale().fractional_scale(),
2042                        )
2043                    })
2044                    .unwrap_or((0, 0, 0.0)),
2045            }
2046        })
2047        .unwrap_or((0, 0, 0.0))
2048}
2049
2050/// Answer geometry readers parked behind a host layout request with what `display_id`
2051/// captures now.
2052fn answer_geometry_waiters(
2053    state: &AppState,
2054    display_id: u32,
2055    waiters: Vec<std::sync::mpsc::Sender<(i32, i32, f64)>>,
2056) {
2057    if waiters.is_empty() {
2058        return;
2059    }
2060    let info = realized_geometry(state, display_id);
2061    for w in waiters {
2062        let _ = w.send(info);
2063    }
2064}
2065
2066/// What the mode a host announced means for a capture configured at `want`, once the host
2067/// has ruled on the layout request: `Some(size)` when the capture must follow a different
2068/// mode the host runs, `None` when nothing changes — the host runs the requested size, or
2069/// its mode is unknown and the capture keeps gating on the size it asked for. The verdict
2070/// itself is not consulted: a host may answer `succeeded` for a mode it did not take
2071/// (Hyprland acknowledges a custom mode it rejected), so the announced size decides. It has
2072/// arrived by the time any verdict does, both riding the one host connection in order.
2073fn host_layout_resolution(want: (i32, i32), current: Option<(i32, i32)>) -> Option<(i32, i32)> {
2074    current.filter(|&c| c != want)
2075}
2076
2077/// Periods without a fresh host frame before the timer treats the host as quiet and, in
2078/// streaming mode, re-encodes the retained frame to hold the rate. A live host's frames come
2079/// about a period apart, so a tick landing a hair ahead of one must not publish a duplicate
2080/// the fresh frame then waits behind.
2081const HOST_QUIET_PERIODS: f64 = 1.5;
2082
2083/// Refused mode requests in a row for one display before the log says plainly what to do: a
2084/// host that keeps its own mode is asked again on every client resize, and each refusal
2085/// restarts the capture at the mode the host runs.
2086const HOST_MODE_REFUSALS_BEFORE_HINT: u32 = 3;
2087
2088/// Settle the host layout requests answered since the last tick, against the mode each host
2089/// announced. A host already running the requested size needs nothing: the capture was
2090/// configured for it and its frames flow. A host running anything else — a request it
2091/// refused, acknowledged without applying, or never answered by the deadline, and a host
2092/// with no layout management at all — has the capture re-sized to that mode through the same
2093/// in-place reconfigure a resize takes, so it never gates on a size the host will not
2094/// produce, and the mismatch becomes the capture's caveat. Geometry readers parked behind a
2095/// request answer once it is settled, with the size actually captured.
2096fn reconcile_host_layouts(state: &mut AppState) {
2097    if state.host_layout_pending.is_empty() {
2098        return;
2099    }
2100    let ids: Vec<u32> = state.host_layout_pending.keys().copied().collect();
2101    for id in ids {
2102        // An earlier iteration's restart may have reaped the host (and every request).
2103        let Some(pending) = state.host_layout_pending.get(&id) else {
2104            continue;
2105        };
2106        let host_mode = match state.host.as_ref() {
2107            Some(host) => match host.layout_outcome(pending.epoch) {
2108                // Unanswered: the host has not ruled, so its announced mode may still be
2109                // the one from before the request.
2110                None => continue,
2111                Some(_) => host_layout_resolution(pending.want, host.current_output_size(id)),
2112            },
2113            None => None,
2114        };
2115        let Some(pending) = state.host_layout_pending.remove(&id) else {
2116            continue;
2117        };
2118        if host_mode.is_none() {
2119            state.host_mode_refusals.remove(&id);
2120        }
2121        if let Some((rw, rh)) = host_mode {
2122            let (w, h) = pending.want;
2123            let restart = state
2124                .node_idx_for_id(id)
2125                .and_then(|idx| state.output_nodes[idx].capture.as_ref())
2126                .map(|cap| (cap.callback.clone(), cap.settings.clone()));
2127            if let Some((cb, mut settings)) = restart {
2128                settings.width = rw;
2129                settings.height = rh;
2130                // Video codecs even-mask their dimensions, so an odd host mode is followed as
2131                // closely as the encoder can; asking again would only loop.
2132                let followed = if settings.codec.is_video() {
2133                    (rw & !1, rh & !1)
2134                } else {
2135                    (rw, rh)
2136                };
2137                if followed != (w, h) {
2138                    eprintln!(
2139                        "[HostCapture] host runs {rw}x{rh} for display {id} ({w}x{h} not applied); capturing at that size."
2140                    );
2141                    let refusals = {
2142                        let n = state.host_mode_refusals.entry(id).or_insert(0);
2143                        *n += 1;
2144                        *n
2145                    };
2146                    if refusals == HOST_MODE_REFUSALS_BEFORE_HINT {
2147                        eprintln!(
2148                            "[HostCapture] display {id}: the host keeps its own mode; a manual resolution of {rw}x{rh} stops the capture restarting on every resize."
2149                        );
2150                    }
2151                    start_capture_on_display(state, id, cb, settings);
2152                    let mismatch = if refusals >= HOST_MODE_REFUSALS_BEFORE_HINT {
2153                        format!(
2154                            "host runs {rw}x{rh} ({w}x{h} not applied; set a manual resolution of {rw}x{rh})"
2155                        )
2156                    } else {
2157                        format!("host runs {rw}x{rh} ({w}x{h} not applied)")
2158                    };
2159                    let own = wayland_capture_err().lock().unwrap().get(&id).cloned();
2160                    set_wayland_capture_err(
2161                        id,
2162                        Some(match own {
2163                            Some(e) => format!("{mismatch}; {e}"),
2164                            None => mismatch,
2165                        }),
2166                    );
2167                }
2168            }
2169        }
2170        // Settled either way: the restart, if any, runs at the size the host has, so the
2171        // readers need not wait for the host to acknowledge it again.
2172        answer_geometry_waiters(state, id, pending.geometry_waiters);
2173    }
2174}
2175
2176/// Start (or in-place reconfigure) the capture bound to output `display_id`: reprogram the
2177/// output's mode/scale/refresh, size the render targets, fullscreen the display's windows at
2178/// the new logical size, resolve the encode path (zero-copy vs readback), and spawn the
2179/// delivery (and readback-mode encode) threads. The single-display behavior of the former
2180/// global StartCapture is preserved exactly for display 0.
2181/// Bring up the readback encode path (pixman readback → pool → encode thread) for a
2182/// capture whose zero-copy session is absent or just died. Mirrors the start-capture
2183/// bootstrap: u64::MAX content generations mark every pool slot stale so each one is
2184/// read back before its first publish, whatever the damage says.
2185fn bootstrap_readback_pool(
2186    cap: &mut wayland::frontend::WlCapture,
2187    display_id: u32,
2188    use_gpu: bool,
2189    try_gpu: bool,
2190    prior: Option<FrameEncoder>,
2191    predecessor: Option<std::thread::JoinHandle<Option<FrameEncoder>>>,
2192) {
2193    let Some(deliver_tx) = cap.deliver_tx.clone() else {
2194        return;
2195    };
2196    let settings = cap.settings.clone();
2197    let pool = Arc::new(WlFramePool::new(
2198        WL_POOL_SURFACES,
2199        (settings.width.max(0) as usize) * (settings.height.max(0) as usize) * 4,
2200    ));
2201    cap.pool_last_render = vec![0; WL_POOL_SURFACES];
2202    cap.render_seq = 0;
2203    cap.pool_content_gen = vec![u64::MAX; WL_POOL_SURFACES];
2204    cap.content_gen = 0;
2205    let c = &cap.encode_controls;
2206    c.bitrate_kbps
2207        .store(settings.video_bitrate_kbps, Ordering::Relaxed);
2208    c.vbv_mult_milli.store(
2209        (settings.video_vbv_multiplier * 1000.0).round() as i32,
2210        Ordering::Relaxed,
2211    );
2212    c.fps_bits
2213        .store(settings.target_fps.max(1.0).to_bits(), Ordering::Relaxed);
2214    let cfg = WlEncodeConfig {
2215        settings: settings.clone(),
2216        display_id,
2217        use_gpu,
2218        try_gpu,
2219        prior,
2220        predecessor,
2221        recording_sink: cap.recording_sink.as_ref().map(Arc::downgrade),
2222        deliver_tx,
2223        controls: cap.encode_controls.clone(),
2224        stats: cap.encode_stats.clone(),
2225        report: cap.report.clone(),
2226    };
2227    let pool2 = pool.clone();
2228    cap.encode_join = Some(
2229        thread::Builder::new()
2230            .name(format!("wl-encode-{display_id}"))
2231            .spawn(move || wayland_encode_loop(&pool2, cfg))
2232            .expect("failed to spawn wl-encode thread"),
2233    );
2234    cap.encode_pool = Some(pool);
2235}
2236
2237/// Rebuild a broken zero-copy hardware session with the startup construction (driver
2238/// match, EGL display hand-over, the same chroma negotiation). `None` means unrecoverable —
2239/// the caller demotes to readback.
2240fn rebuild_zerocopy_encoder(
2241    cap: &wayland::frontend::WlCapture,
2242    state: &mut AppState,
2243) -> Option<FrameEncoder> {
2244    let egl_display = state
2245        .gles_renderer
2246        .as_ref()
2247        .map(|r| r.egl_context().display().get_display_handle().handle)
2248        .unwrap_or(std::ptr::null());
2249    let mut settings = cap.settings.clone();
2250    encoders::select_frame_encoder(
2251        &mut settings,
2252        FrameSource::Dmabuf { egl_display },
2253        None,
2254        "Wayland",
2255    )
2256}
2257
2258/// Consecutive encode failures before a hardware path recovers (~0.5s at 60fps): a hiccup
2259/// outlasts it, anything longer starts to look like a dead session. Shared by every hardware
2260/// encode path (Wayland zero-copy, Wayland readback, X11) so recovery timing matches.
2261pub(crate) const HW_ERROR_RECOVERY_THRESHOLD: u32 = 30;
2262
2263fn start_capture_on_display(
2264    state: &mut AppState,
2265    display_id: u32,
2266    cb: Option<Arc<Py<PyAny>>>,
2267    mut settings: RustCaptureSettings,
2268) {
2269    // The cursor worker outlives individual captures, so the starting settings have to
2270    // reach it here — same point X11 applies the cap — or it keeps the previous capture's.
2271    let _ = state
2272        .cursor_tx
2273        .send(CursorJob::SetSizeCap(settings.cursor_size_cap));
2274
2275    // Fresh attempt: drop any prior outcome so a stale caveat cannot read as this start's.
2276    set_wayland_capture_err(display_id, None);
2277
2278    let Some(node_idx) = state.node_idx_for_id(display_id) else {
2279        eprintln!("[Wayland] StartCapture: no output with display id {display_id}.");
2280        set_wayland_capture_err(
2281            display_id,
2282            Some(format!("no output with display id {display_id}")),
2283        );
2284        return;
2285    };
2286    let stream_report = report::StreamReport::new("wayland");
2287    report::wayland_reports()
2288        .lock()
2289        .unwrap()
2290        .insert(display_id, stream_report.clone());
2291    let _report = report::enter(&stream_report);
2292    let mut node = state.output_nodes.remove(node_idx);
2293    // Geometry readers parked behind a layout request this start supersedes: they ride
2294    // on to this start's request, or answer at its end if no host is involved.
2295    let mut geometry_waiters = state
2296        .host_layout_pending
2297        .remove(&display_id)
2298        .map(|p| p.geometry_waiters)
2299        .unwrap_or_default();
2300
2301    if settings.encode_node_index < -1 {
2302        let picked = if state.auto_gpu_selected {
2303            render_node_index(&state.render_node_path)
2304        } else {
2305            first_render_node_index()
2306        };
2307        if let Some(idx) = picked {
2308            settings.encode_node_index = idx;
2309        }
2310    }
2311
2312    if settings.codec.is_video() {
2313        settings.width &= !1;
2314        settings.height &= !1;
2315    }
2316
2317    // Tear down this display's previous capture first; its hardware sessions are the
2318    // reuse candidates below (zero-copy inline, readback via the encode config).
2319    let mut prior_zero_copy: Option<FrameEncoder> = None;
2320    let mut prior_encode_join: Option<std::thread::JoinHandle<Option<FrameEncoder>>> = None;
2321    let mut prior_deliver_join: Option<std::thread::JoinHandle<()>> = None;
2322    if let Some(mut old) = node.capture.take() {
2323        prior_zero_copy = old.video_encoder.take();
2324        (prior_deliver_join, prior_encode_join) = teardown_capture(&mut old);
2325    }
2326
2327    // Bind only after the old capture is gone: its sink unlinks the socket path in
2328    // Drop, which would strip a fresh bind's filesystem name and leave every later
2329    // recorder connect with ENOENT.
2330    let recording_sink = crate::recording_sink::RecordingSink::try_bind(
2331        &settings.recording_socket,
2332        settings.target_fps,
2333    );
2334
2335    // Host-capture mode: connect on first use. The display's mode is requested from
2336    // the host further down, without waiting for an answer; a host that keeps its own
2337    // mode (refusal, or no layout management — KWin) has this capture re-sized to it
2338    // when the verdict arrives (`reconcile_host_layouts`), the same way a failed GBM
2339    // resize falls back to the live mode.
2340    let host_capture = !settings.wayland_host_display.is_empty();
2341    reap_dead_host(state);
2342    if host_capture && state.host.is_none() {
2343        // Capture buffers come from the same render node the encoder imports
2344        // from, resolved via its live fd (the path string is not retained in
2345        // auto mode); each capture thread opens its own device handle.
2346        let gbm_path = if state.use_gpu {
2347            state.gbm_device.as_ref().and_then(|dev| {
2348                use std::os::fd::{AsFd as _, AsRawFd as _};
2349                let fd = dev.as_fd().as_raw_fd();
2350                std::fs::read_link(format!("/proc/self/fd/{fd}")).ok()
2351            })
2352        } else {
2353            None
2354        };
2355        // The dmabuf formats the encoder's display imports are what a portal stream may
2356        // offer the compositor for zero-copy frames; a CPU session offers none.
2357        let dma_formats: Vec<(u32, Vec<u64>)> = match (&state.gles_renderer, state.use_gpu) {
2358            (Some(renderer), true) => {
2359                let formats = renderer.egl_context().display().dmabuf_texture_formats();
2360                [Fourcc::Xrgb8888, Fourcc::Argb8888]
2361                    .into_iter()
2362                    .map(|code| {
2363                        (
2364                            code as u32,
2365                            formats
2366                                .iter()
2367                                .filter(|f| f.code == code)
2368                                .map(|f| u64::from(f.modifier))
2369                                .collect(),
2370                        )
2371                    })
2372                    .collect()
2373            }
2374            _ => Vec::new(),
2375        };
2376        match crate::wayland::host::HostSession::connect(
2377            &settings.wayland_host_display,
2378            gbm_path,
2379            state.host_frame_tx.clone(),
2380            dma_formats,
2381            state.cursor_tx.clone(),
2382        ) {
2383            Ok(h) => {
2384                println!(
2385                    "[HostCapture] capturing host compositor '{}' ({} outputs).",
2386                    settings.wayland_host_display,
2387                    h.output_count()
2388                );
2389                state.host = Some(h);
2390            }
2391            Err(e) => {
2392                eprintln!(
2393                    "[HostCapture] connect '{}' failed: {e}",
2394                    settings.wayland_host_display
2395                );
2396                set_wayland_capture_err(
2397                    display_id,
2398                    Some(format!(
2399                        "host compositor '{}' connect failed ({e}); capturing locally",
2400                        settings.wayland_host_display
2401                    )),
2402                );
2403            }
2404        }
2405    }
2406    // A failed connect leaves this display compositing locally, so its frames follow the
2407    // local renderer again.
2408    let host_capture = host_capture && state.host.is_some();
2409    if host_capture && let Some(host) = &state.host {
2410        // The node's layout offset rides along so the host's heads mirror
2411        // selkies' union layout (input coordinates already assume it).
2412        host.set_layout(display_id, node.pos.0, node.pos.1);
2413    }
2414
2415    // A view captures a rectangle of a screen it does not own, so its size is the
2416    // rectangle's and the screen keeps the mode its owner gave it. Resizing here would
2417    // shrink the session to one display's worth of desk, which is the whole point of
2418    // cutting several out of one screen.
2419    if node.owner.is_none() {
2420        // Never panic the compositor thread: an output momentarily without a current
2421        // mode falls back to the requested geometry so the reconfigure below is a
2422        // no-op for size/refresh instead of unwrap-panicking.
2423        let target_refresh = (settings.target_fps * 1000.0).round() as i32;
2424        let (current_w, current_h, current_refresh) = match node.output.current_mode() {
2425            Some(m) => (m.size.w, m.size.h, m.refresh),
2426            None => (settings.width, settings.height, target_refresh),
2427        };
2428        let current_scale = node.output.current_scale().fractional_scale();
2429
2430        if current_w != settings.width
2431            || current_h != settings.height
2432            || (current_scale - settings.scale).abs() > 0.001
2433            || current_refresh != target_refresh
2434        {
2435            // Allocate the GPU backing for the new dimensions BEFORE committing
2436            // anything: if the driver refuses (VRAM exhaustion, dimensions it will
2437            // not back), the whole reconfigure is skipped and the previous mode +
2438            // buffers stay live. A failed resize must degrade to "no resize", never
2439            // panic the compositor thread.
2440            let mut new_offscreen = None;
2441            let mut gbm_resize_failed = false;
2442            if state.use_gpu
2443                && let Some(gbm) = state.gbm_device.as_mut()
2444            {
2445                match alloc_render_target(
2446                    gbm,
2447                    settings.width as u32,
2448                    settings.height as u32,
2449                    GbmFormat::Argb8888,
2450                ) {
2451                    Ok(bo) => {
2452                        let dmabuf = create_dmabuf_from_bo(&bo);
2453                        new_offscreen = Some((bo, dmabuf));
2454                    }
2455                    Err(e) => {
2456                        eprintln!(
2457                            "[Wayland] GBM buffer resize to {}x{} failed ({:?}); keeping previous output mode.",
2458                            settings.width, settings.height, e
2459                        );
2460                        gbm_resize_failed = true;
2461                    }
2462                }
2463            }
2464            if gbm_resize_failed {
2465                // The mode commit below is skipped wholesale, so the rest of this
2466                // StartCapture (encoder setup, stored settings) must see the
2467                // dimensions actually live.
2468                set_wayland_capture_err(
2469                    display_id,
2470                    Some(format!(
2471                        "GPU buffer resize to {}x{} refused; kept {current_w}x{current_h}",
2472                        settings.width, settings.height
2473                    )),
2474                );
2475                settings.width = current_w;
2476                settings.height = current_h;
2477                settings.scale = current_scale;
2478                settings.target_fps = current_refresh as f64 / 1000.0;
2479            } else {
2480                println!(
2481                    "[Wayland] Configuring Output {} ({}): {}x{} @ {:.2} FPS (Scale {:.2})",
2482                    display_id,
2483                    node.output.name(),
2484                    settings.width,
2485                    settings.height,
2486                    settings.target_fps,
2487                    settings.scale
2488                );
2489                let new_mode = OutputMode {
2490                    size: (settings.width, settings.height).into(),
2491                    refresh: target_refresh,
2492                };
2493                node.output.change_current_state(
2494                    Some(new_mode),
2495                    Some(Transform::Normal),
2496                    Some(OutputScale::Fractional(settings.scale)),
2497                    Some(Point::from(node.pos)),
2498                );
2499                node.output.set_preferred(new_mode);
2500                // Capture clients allocate to our announced size; a stale size means
2501                // every frame they submit from now on fails buffer validation.
2502                for cs in state
2503                    .copy_sessions
2504                    .iter()
2505                    .filter(|cs| cs.output.upgrade().as_ref() == Some(&node.output))
2506                {
2507                    if let Some(c) = wayland::frontend::output_capture_constraints(
2508                        &node.output,
2509                        state.gles_renderer.as_ref(),
2510                        &state.render_node_path,
2511                    ) {
2512                        cs.session.update_constraints(c);
2513                    }
2514                }
2515
2516                let pixel_count =
2517                    (settings.width.max(0) as usize) * (settings.height.max(0) as usize);
2518                node.frame_buffer = vec![0u8; pixel_count * 4];
2519                node.target_seeded = false;
2520
2521                if let Some(off) = new_offscreen.take() {
2522                    node.offscreen_buffer = Some(off);
2523                }
2524            }
2525        }
2526
2527        let out = node.output.clone();
2528        configure_windows_for_mode(
2529            state,
2530            display_id,
2531            &out,
2532            settings.width,
2533            settings.height,
2534            settings.scale,
2535        );
2536    } else if state.use_gpu {
2537        // A view keeps the rectangle its capture asks for, and its GPU render
2538        // target has to follow: it was allocated at the view's creation size,
2539        // and a zero-copy encoder handed a stale-size dmabuf fails every frame
2540        // (the EGLImage and VAAPI imports both refuse it) while readback
2541        // delivers the old rectangle's content. Same degrade-to-no-resize
2542        // semantics as the screen branch above.
2543        let have = node
2544            .offscreen_buffer
2545            .as_ref()
2546            .map(|(bo, _)| (bo.width() as i32, bo.height() as i32));
2547        if have != Some((settings.width, settings.height))
2548            && let Some(gbm) = state.gbm_device.as_mut()
2549        {
2550            match alloc_render_target(
2551                gbm,
2552                settings.width as u32,
2553                settings.height as u32,
2554                GbmFormat::Argb8888,
2555            ) {
2556                Ok(bo) => {
2557                    let dmabuf = create_dmabuf_from_bo(&bo);
2558                    node.offscreen_buffer = Some((bo, dmabuf));
2559                    node.damage_tracker = OutputDamageTracker::new(
2560                        (settings.width, settings.height),
2561                        settings.scale,
2562                        Transform::Normal,
2563                    );
2564                    node.content_tracker = None;
2565                    node.view_size = (settings.width, settings.height);
2566                    node.view_scale = settings.scale;
2567                    node.frame_buffer = vec![
2568                        0u8;
2569                        (settings.width.max(0) as usize)
2570                            * (settings.height.max(0) as usize)
2571                            * 4
2572                    ];
2573                    node.target_seeded = false;
2574                    crate::log::debug!(
2575                        "[Wayland] View {display_id} render target resized to {}x{}.",
2576                        settings.width,
2577                        settings.height
2578                    );
2579                }
2580                Err(e) => {
2581                    eprintln!(
2582                        "[Wayland] GBM view buffer resize to {}x{} failed ({:?}); keeping previous target.",
2583                        settings.width, settings.height, e
2584                    );
2585                    set_wayland_capture_err(
2586                        display_id,
2587                        Some(format!(
2588                            "GPU view buffer resize to {}x{} refused",
2589                            settings.width, settings.height
2590                        )),
2591                    );
2592                }
2593            }
2594        }
2595    }
2596
2597    let use_cpu_explicit = settings.use_cpu || settings.encode_node_index == -1;
2598    let gpu_intent = settings.codec.is_video() && !use_cpu_explicit;
2599
2600    let mut different_gpu = false;
2601    if gpu_intent {
2602        let encode_node_idx = settings.encode_node_index.max(0);
2603        if !state.render_node_path.is_empty()
2604            && !state
2605                .render_node_path
2606                .contains(&format!("renderD{}", 128 + encode_node_idx))
2607        {
2608            different_gpu = true;
2609        }
2610    }
2611
2612    let mut video_encoder: Option<FrameEncoder> = None;
2613    if gpu_intent && state.use_gpu && !different_gpu {
2614        let egl_display = state
2615            .gles_renderer
2616            .as_ref()
2617            .map(|r| r.egl_context().display().get_display_handle().handle)
2618            .unwrap_or(std::ptr::null());
2619        video_encoder = encoders::select_frame_encoder(
2620            &mut settings,
2621            FrameSource::Dmabuf { egl_display },
2622            prior_zero_copy.take(),
2623            "Wayland",
2624        );
2625        if video_encoder.is_none() {
2626            set_wayland_capture_err(
2627                display_id,
2628                Some(format!(
2629                    "no zero-copy {} encoder on the render node; using readback encode",
2630                    settings.codec.display()
2631                )),
2632            );
2633        }
2634    }
2635    drop(prior_zero_copy);
2636
2637    let rendered = if state.use_gpu {
2638        format!("rendered on {}", state.render_node_path)
2639    } else {
2640        "rendered in software (Pixman)".to_string()
2641    };
2642    match video_encoder.as_ref() {
2643        Some(enc) => {
2644            report::capture("dmabuf", true);
2645            println!(
2646                "[Wayland] Zero-copy capture: output {display_id} {}x{} {rendered}, encoded in place on {}.",
2647                settings.width,
2648                settings.height,
2649                enc.backend_name()
2650            );
2651        }
2652        None => {
2653            let why = if different_gpu {
2654                "the encode node is another GPU"
2655            } else if !settings.codec.is_video() {
2656                "JPEG encodes on the CPU"
2657            } else if use_cpu_explicit {
2658                "software encoding selected"
2659            } else if !state.use_gpu {
2660                "no GPU renderer"
2661            } else {
2662                "no zero-copy encoder on the render node"
2663            };
2664            report::capture("readback", false);
2665            report::zero_copy_available(gpu_intent && state.use_gpu && !different_gpu);
2666            report::capture_reason(why);
2667            println!(
2668                "[Wayland] Readback capture: output {display_id} {}x{} {rendered}, read back for the encode thread ({why}).",
2669                settings.width, settings.height
2670            );
2671        }
2672    }
2673
2674    // Point this display's host capture thread at the size the encoder was just
2675    // configured for and ask the host for that mode. The compositor keeps running (CU,
2676    // clipboard callbacks, input fallbacks) but its renderer is bypassed. The buffer
2677    // type follows the consumer settled on above: dmabufs only for a zero-copy encoder,
2678    // shm frames for every CPU path (use_cpu, JPEG, readback). The host's
2679    // answer is not waited for here — this thread carries input and every other
2680    // display — but polled by the render tick, which re-sizes this capture should the
2681    // host keep a different mode; frames gate until the sizes agree.
2682    if host_capture && let Some(host) = &state.host {
2683        let epoch = host.start_capture(
2684            display_id,
2685            settings.width,
2686            settings.height,
2687            video_encoder.is_some(),
2688            settings.capture_cursor,
2689            settings.target_fps,
2690        );
2691        state.host_layout_pending.insert(
2692            display_id,
2693            wayland::frontend::PendingHostLayout {
2694                epoch,
2695                want: (settings.width, settings.height),
2696                geometry_waiters: std::mem::take(&mut geometry_waiters),
2697            },
2698        );
2699    }
2700
2701    if recording_sink.is_some() && !settings.codec.is_video() {
2702        eprintln!(
2703            "[recording_sink] WARNING: recording_socket is set but the codec is JPEG. The \
2704             recording socket requires a video stream; set a video codec on the Python \
2705             CaptureSettings to produce a recordable output."
2706        );
2707    }
2708
2709    // Every display's capture composites its own watermark, uploaded at this output's
2710    // scale and placed against this output's frame dimensions.
2711    let watermark_output_scale = node.output.current_scale().fractional_scale();
2712    node.overlay_state
2713        .load_watermark(&settings.watermark_path, watermark_output_scale);
2714    if display_id == 0 {
2715        state.settings = settings.clone();
2716        if state.cursor_callback_set
2717            && let Some(icon) = state.current_cursor_icon.clone()
2718        {
2719            state.send_cursor_image(&icon);
2720        }
2721    }
2722    state.render_cursor_on_framebuffer = settings.capture_cursor;
2723
2724    let mut cap = wayland::frontend::WlCapture {
2725        settings: settings.clone(),
2726        callback: cb.clone(),
2727        video_encoder,
2728        vaapi_state: StripeState::default(),
2729        recording_sink,
2730        deliver_tx: None,
2731        deliver_join: None,
2732        deliver_discard: None,
2733        pending_hw_delivery: None,
2734        pending_hw_damage: false,
2735        encode_pool: None,
2736        encode_join: None,
2737        encode_controls: Arc::new(WlEncodeControls::new()),
2738        encode_stats: Arc::new(WlEncodeStats::new()),
2739        report: stream_report.clone(),
2740        pool_last_render: Vec::new(),
2741        render_seq: 0,
2742        pool_content_gen: Vec::new(),
2743        content_gen: 0,
2744        frame_counter: 0,
2745        pending_force_idr: false,
2746        needs_full_render: true,
2747        pace: FramePace::default(),
2748        hw_error_streak: 0,
2749        hw_rebuilt: false,
2750    };
2751
2752    {
2753        let (tx, rx) = std::sync::mpsc::sync_channel::<Vec<EncodedStripe>>(1);
2754        let discard = Arc::new(AtomicBool::new(false));
2755        let thread_discard = discard.clone();
2756        let predecessor = prior_deliver_join;
2757        // With no Python callback (internal recorder-owned capture) the delivery thread
2758        // only drains the channel: the recorder already consumed the frames at the
2759        // delivery-layer tap, upstream of this per-consumer handoff.
2760        let join = thread::spawn(move || {
2761            // The predecessor capture's deliver thread finishes first, off the
2762            // event loop: encoded stripes reach Python in capture order across
2763            // a reconfigure, and a stale pre-teardown stripe can never land
2764            // after this capture's first frame.
2765            if let Some(handle) = predecessor {
2766                let _ = handle.join();
2767            }
2768            match cb {
2769                Some(cb) => {
2770                    crate::boost_thread_priority(-10);
2771                    while let Ok(stripes) = rx.recv() {
2772                        if thread_discard.load(Ordering::Relaxed)
2773                            || PY_SHUTDOWN.load(Ordering::Relaxed)
2774                        {
2775                            continue;
2776                        }
2777                        stream_report.tally(&stripes);
2778                        Python::attach(|py| {
2779                            for s in stripes {
2780                                match Py::new(
2781                                    py,
2782                                    StripeFrame::new_owned_meta(
2783                                        s.data,
2784                                        s.codec.data_type(),
2785                                        s.stripe_y_start,
2786                                        s.stripe_height,
2787                                        s.frame_id,
2788                                        s.timing,
2789                                        s.reference,
2790                                    ),
2791                                ) {
2792                                    Ok(f) => {
2793                                        if let Err(e) = cb.call1(py, (f,)) {
2794                                            e.print(py);
2795                                        }
2796                                    }
2797                                    Err(e) => eprintln!("[Wayland] frame alloc error: {e:?}"),
2798                                }
2799                            }
2800                        });
2801                    }
2802                }
2803                None => while rx.recv().is_ok() {},
2804            }
2805        });
2806        cap.deliver_tx = Some(tx);
2807        cap.deliver_discard = Some(discard);
2808        cap.deliver_join = Some(join);
2809    }
2810
2811    if cap.video_encoder.is_none() {
2812        bootstrap_readback_pool(
2813            &mut cap,
2814            display_id,
2815            // Host frames land in the pool as BGRA whatever the local renderer is; only
2816            // a GLES readback of our own compositing produces RGBA.
2817            state.use_gpu && !host_capture,
2818            gpu_intent && (!state.use_gpu || different_gpu),
2819            None,
2820            prior_encode_join.take(),
2821        );
2822    } else {
2823        cap.encode_stats.n_stripes.store(1, Ordering::Relaxed);
2824        *cap.encode_stats.desc.lock().unwrap() =
2825            encoder_desc(&settings, cap.video_encoder.as_ref(), true);
2826        set_wayland_active_codec(display_id, Some(settings.codec));
2827        log_stream_settings("Wayland", &settings, 1, cap.video_encoder.as_ref());
2828    }
2829    // A zero-copy start has no successor encode thread to inherit the outgoing readback
2830    // session, so the outgoing thread is reaped instead.
2831    state.encode_reaper.extend(prior_encode_join);
2832    // Force the keyframe unconditionally: the damage tracker and offscreen buffer
2833    // stay warm across stop/start, so a restarted capture on a static screen
2834    // otherwise produces no damage, no first frame, and no IDR in either path.
2835    cap.request_idr();
2836
2837    node.capture = Some(cap);
2838    // The start reprogrammed this output, and until a client answers at the new size the
2839    // compositor paints its clear color over whatever the client does not cover — a
2840    // freshly created output, whose session window is still parked at a placeholder size,
2841    // is covered by none of it. Those frames are held rather than streamed as a blank
2842    // screen; the deadline releases an output no client ever draws on.
2843    node.content_hold_until = Some(Instant::now() + WL_CONTENT_HOLD);
2844    state.output_nodes.insert(node_idx, node);
2845    wayland_alive().lock().unwrap().insert(display_id);
2846    answer_geometry_waiters(state, display_id, geometry_waiters);
2847}
2848
2849/// One output's render + capture tick: composite the elements overlapping this output
2850/// (positions made output-local by subtracting its layout origin), track damage, feed the
2851/// display's own encode path, and answer a pending screenshot on the primary. Returns true
2852/// when the tick was skipped because this display's encode pool was still busy (the caller
2853/// then retries shortly instead of waiting a full frame interval).
2854/// Stamp the watermark element onto `target` in place — no clear, so the
2855/// captured content underneath stays — returning the draw's sync point for the
2856/// encoder to wait on.
2857fn draw_host_watermark(
2858    renderer: &mut GlesRenderer,
2859    overlay: &crate::encoders::overlay::OverlayState,
2860    target: &mut Dmabuf,
2861    size: (i32, i32),
2862) -> Result<SyncPoint, String> {
2863    let elem = overlay
2864        .get_watermark_element(renderer)
2865        .ok_or("watermark element unavailable")?;
2866    let mut fb = renderer.bind(target).map_err(|e| format!("bind: {e:?}"))?;
2867    let mut frame = renderer
2868        .render(&mut fb, (size.0, size.1).into(), Transform::Normal)
2869        .map_err(|e| format!("render: {e:?}"))?;
2870    let dst = elem.geometry(1.0.into());
2871    let local = Rectangle::from_size(dst.size);
2872    elem.draw(&mut frame, elem.src(), dst, &[local], &[], None)
2873        .map_err(|e| format!("draw: {e:?}"))?;
2874    frame.finish().map_err(|e| format!("finish: {e:?}"))
2875}
2876
2877/// Re-compose `src` (the retained host frame) plus the watermark into `target`:
2878/// the path a moving watermark needs, since re-drawing over the same retained
2879/// buffer would leave trails.
2880fn compose_host_watermark(
2881    renderer: &mut GlesRenderer,
2882    overlay: &crate::encoders::overlay::OverlayState,
2883    src: &Dmabuf,
2884    target: &mut Dmabuf,
2885    size: (i32, i32),
2886) -> Result<SyncPoint, String> {
2887    let elem = overlay
2888        .get_watermark_element(renderer)
2889        .ok_or("watermark element unavailable")?;
2890    let tex = renderer
2891        .import_dmabuf(src, None)
2892        .map_err(|e| format!("import: {e:?}"))?;
2893    let mut fb = renderer.bind(target).map_err(|e| format!("bind: {e:?}"))?;
2894    let full: Rectangle<i32, Physical> = Rectangle::from_size((size.0, size.1).into());
2895    let mut frame = renderer
2896        .render(&mut fb, (size.0, size.1).into(), Transform::Normal)
2897        .map_err(|e| format!("render: {e:?}"))?;
2898    frame
2899        .render_texture_from_to(
2900            &tex,
2901            Rectangle::from_size((size.0 as f64, size.1 as f64).into()),
2902            full,
2903            &[full],
2904            // Opaque: the capture format's undefined alpha must not blend, it
2905            // would leave the target's previous content (and stamped
2906            // watermarks) underneath.
2907            &[full],
2908            Transform::Normal,
2909            1.0,
2910            None,
2911            &[],
2912        )
2913        .map_err(|e| format!("texture: {e:?}"))?;
2914    let dst = elem.geometry(1.0.into());
2915    let local = Rectangle::from_size(dst.size);
2916    elem.draw(&mut frame, elem.src(), dst, &[local], &[], None)
2917        .map_err(|e| format!("draw: {e:?}"))?;
2918    frame.finish().map_err(|e| format!("finish: {e:?}"))
2919}
2920
2921fn warn_once_host_watermark(e: &str) {
2922    static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
2923    if !WARNED.swap(true, Ordering::Relaxed) {
2924        eprintln!("[HostCapture] watermark compositing failed: {e}");
2925    }
2926}
2927
2928/// Copy full rows from `src` (tight `width*4` stride) into an Argb8888/Xrgb8888 shm
2929/// buffer, honoring the destination's own offset and stride.
2930fn copy_rows_into_shm(
2931    buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer,
2932    src: &[u8],
2933    width: i32,
2934    height: i32,
2935) -> Result<(), String> {
2936    use smithay::wayland::shm::with_buffer_contents_mut;
2937    let src_stride = (width.max(0) as usize) * 4;
2938    if src.len() < src_stride * (height.max(0) as usize) {
2939        return Err("source smaller than advertised".into());
2940    }
2941    with_buffer_contents_mut(buffer, |ptr, len, spec| {
2942        if spec.width < width || spec.height < height {
2943            return Err("shm buffer smaller than the output".to_string());
2944        }
2945        let dst_stride = spec.stride as usize;
2946        let offset = spec.offset as usize;
2947        if dst_stride < src_stride || len < offset + dst_stride * (height as usize) {
2948            return Err("shm stride or length mismatch".to_string());
2949        }
2950        for y in 0..height as usize {
2951            unsafe {
2952                std::ptr::copy_nonoverlapping(
2953                    src.as_ptr().add(y * src_stride),
2954                    ptr.add(offset + y * dst_stride),
2955                    src_stride,
2956                );
2957            }
2958        }
2959        Ok(())
2960    })
2961    .map_err(|e| format!("{e:?}"))?
2962}
2963
2964/// Complete parked ext-image-copy-capture frames for this output from the frame the
2965/// render pass just produced. A dmabuf target is filled by one GPU blit from the
2966/// output's composited buffer; an shm target by one readback (GLES) or one row copy
2967/// (pixman). With no damage, a session that has already received content keeps its
2968/// frame parked, so a static screen costs capture clients nothing.
2969fn service_copy_frames(
2970    state: &mut AppState,
2971    node: &mut wayland::frontend::OutputNode,
2972    width: i32,
2973    height: i32,
2974    damage_rects: &[Rectangle<i32, Physical>],
2975) {
2976    use smithay::backend::renderer::{Blit, BufferType, ExportMem, TextureFilter, buffer_type};
2977    use smithay::utils::Buffer as BufferCoords;
2978
2979    if state.copy_sessions.is_empty() {
2980        return;
2981    }
2982    let time = state.clock.now();
2983    for i in 0..state.copy_sessions.len() {
2984        {
2985            let cs = &state.copy_sessions[i];
2986            if cs.output.upgrade().as_ref() != Some(&node.output)
2987                || cs.pending.is_none()
2988                || (damage_rects.is_empty() && cs.delivered_once)
2989            {
2990                continue;
2991            }
2992        }
2993        let first = !state.copy_sessions[i].delivered_once;
2994        let frame = state.copy_sessions[i].pending.take().unwrap();
2995        let buffer = frame.buffer();
2996        let full = Rectangle::<i32, Physical>::new((0, 0).into(), (width, height).into());
2997        let result: Result<(), String> = match buffer_type(&buffer) {
2998            Some(BufferType::Dma) => (|| {
2999                let renderer = state.gles_renderer.as_mut().ok_or("no GLES renderer")?;
3000                let mut client = smithay::wayland::dmabuf::get_dmabuf(&buffer)
3001                    .map_err(|e| e.to_string())?
3002                    .clone();
3003                let (_bo, offscreen) = node
3004                    .offscreen_buffer
3005                    .as_mut()
3006                    .ok_or("no composited buffer")?;
3007                let src = renderer.bind(offscreen).map_err(|e| format!("{e:?}"))?;
3008                let mut dst = renderer.bind(&mut client).map_err(|e| format!("{e:?}"))?;
3009                // ready() promises readable contents, and implicit dmabuf sync cannot
3010                // be relied on cross-process on every driver: wait out the blit fence
3011                // (microseconds) before declaring the frame done.
3012                let sync = renderer
3013                    .blit(&src, &mut dst, full, full, TextureFilter::Linear)
3014                    .map_err(|e| format!("{e:?}"))?;
3015                if !wait_render_fence(&sync, node.id) {
3016                    return Err("blit fence not signaled".to_string());
3017                }
3018                Ok(())
3019            })(),
3020            Some(BufferType::Shm) => (|| {
3021                if let Some(renderer) = state.gles_renderer.as_mut() {
3022                    let (_bo, offscreen) = node
3023                        .offscreen_buffer
3024                        .as_mut()
3025                        .ok_or("no composited buffer")?;
3026                    let fb = renderer.bind(offscreen).map_err(|e| format!("{e:?}"))?;
3027                    let mapping = renderer
3028                        .copy_framebuffer(
3029                            &fb,
3030                            Rectangle::new((0, 0).into(), (width, height).into()),
3031                            Fourcc::Argb8888,
3032                        )
3033                        .map_err(|e| format!("{e:?}"))?;
3034                    let data = renderer
3035                        .map_texture(&mapping)
3036                        .map_err(|e| format!("{e:?}"))?;
3037                    copy_rows_into_shm(&buffer, data, width, height)
3038                } else {
3039                    copy_rows_into_shm(&buffer, &node.frame_buffer, width, height)
3040                }
3041            })(),
3042            _ => Err("unsupported buffer type".into()),
3043        };
3044        match result {
3045            Ok(()) => {
3046                let damage: Option<Vec<Rectangle<i32, BufferCoords>>> = if first {
3047                    None
3048                } else {
3049                    Some(
3050                        damage_rects
3051                            .iter()
3052                            .map(|r| {
3053                                Rectangle::new(
3054                                    (r.loc.x, r.loc.y).into(),
3055                                    (r.size.w, r.size.h).into(),
3056                                )
3057                            })
3058                            .collect(),
3059                    )
3060                };
3061                frame.success(Transform::Normal, damage, time);
3062                state.copy_sessions[i].delivered_once = true;
3063            }
3064            Err(e) => {
3065                eprintln!("[Wayland] copy-capture frame failed: {e}");
3066                frame.fail(CaptureFailureReason::Unknown);
3067            }
3068        }
3069    }
3070}
3071
3072/// Answer the frame callbacks of a surface-backed cursor. Xwayland and libwayland-cursor
3073/// clients wait for the cursor surface's frame callback before attaching their next
3074/// sprite, so a cursor surface that never gets one stops updating after its first sprite.
3075fn send_cursor_frame(state: &AppState, output: &Output, time: impl Into<Duration>) {
3076    if let Some(CursorImageStatus::Surface(surface)) = &state.current_cursor_icon {
3077        send_frames_surface_tree(surface, output, time, Some(Duration::ZERO), |_, _| {
3078            Some(output.clone())
3079        });
3080    }
3081}
3082
3083/// Hotspot of a surface-backed cursor, in the surface's logical coordinates (zero when the
3084/// client set none). The sprite is placed at pointer - hotspot, like named and X11 cursors.
3085fn cursor_surface_hotspot(
3086    surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
3087) -> Point<i32, smithay::utils::Logical> {
3088    with_states(surface, |states| {
3089        states
3090            .data_map
3091            .get::<std::sync::Mutex<smithay::input::pointer::CursorImageAttributes>>()
3092            .and_then(|attrs| attrs.lock().ok().map(|guard| guard.hotspot))
3093            .unwrap_or_default()
3094    })
3095}
3096
3097/// Longest a render tick waits for the GPU to finish a frame before giving that frame up.
3098const RENDER_FENCE_TIMEOUT: Duration = Duration::from_millis(500);
3099
3100/// Render ticks in a row that draw nothing before one closes an empty frame on its target.
3101/// smithay binds a dmabuf target through a texture with a framebuffer made for that bind, and
3102/// the NVIDIA driver keeps the state of each one never drawn to until a frame retires it (a
3103/// fence or a flush does not): about 15 KB a tick while the screen stands still.
3104const UNDRAWN_TICKS_PER_FRAME: u32 = 60;
3105
3106/// Reads `target` back as RGBA into `dst`. The read runs in a frame of the target: a dmabuf
3107/// bound as a texture gets a framebuffer of its own on each bind, which only a frame makes
3108/// current, so a bare read on a tick that drew nothing would read another framebuffer.
3109fn read_back_rgba(
3110    renderer: &mut GlesRenderer,
3111    target: &mut smithay::backend::renderer::gles::GlesTarget<'_>,
3112    width: i32,
3113    height: i32,
3114    dst: &mut [u8],
3115) {
3116    let read = renderer
3117        .render(target, (width, height).into(), Transform::Normal)
3118        .and_then(|mut frame| {
3119            frame.with_context(|gl| unsafe {
3120                gl.ReadPixels(
3121                    0,
3122                    0,
3123                    width,
3124                    height,
3125                    smithay::backend::renderer::gles::ffi::RGBA,
3126                    smithay::backend::renderer::gles::ffi::UNSIGNED_BYTE,
3127                    dst.as_mut_ptr() as *mut std::ffi::c_void,
3128                );
3129            })
3130        });
3131    if let Err(e) = read {
3132        eprintln!("[Wayland] Readback failed: {e:?}");
3133    }
3134}
3135
3136/// Wait, bounded, for a render's fence to signal before the tick reuses or releases the
3137/// buffers it sampled.
3138///
3139/// A client buffer a render read can be released and destroyed by the client's next commit
3140/// (a nested session swaps its swapchain across a mode change); without implicit dmabuf
3141/// fencing the driver then frees it under an unfinished read and faults the channel, after
3142/// which no fence of the context signals. The bound turns that into a logged, dropped frame
3143/// rather than a compositor thread parked forever. The wait is one `eglClientWaitSync` to the
3144/// deadline, flushing whatever commands still queue ahead of the fence; a fence-less sync
3145/// point is reached at once.
3146fn wait_render_fence(sync: &SyncPoint, display_id: u32) -> bool {
3147    let reached = match sync.get::<EGLFence>() {
3148        Some(fence) => fence
3149            .client_wait(Some(RENDER_FENCE_TIMEOUT), true)
3150            .unwrap_or(false),
3151        None => sync.wait().is_ok(),
3152    };
3153    if !reached {
3154        eprintln!(
3155            "[Wayland] Display {display_id}: render fence not signaled within {:?}; frame dropped.",
3156            RENDER_FENCE_TIMEOUT
3157        );
3158    }
3159    reached
3160}
3161
3162/// Render every output that is due for `trigger`, and say whether a readback pool was still
3163/// busy. The nodes are taken out of the state so each per-output render can borrow the shared
3164/// renderer/space alongside its own damage tracker and buffers.
3165fn render_pass(state: &mut AppState, trigger: TickTrigger) -> bool {
3166    let mut nodes = std::mem::take(&mut state.output_nodes);
3167    let mut any_pool_busy = false;
3168    for node in nodes.iter_mut() {
3169        if render_node_tick(state, node, trigger) {
3170            any_pool_busy = true;
3171        }
3172    }
3173    state.output_nodes = nodes;
3174    any_pool_busy
3175}
3176
3177/// One display's render tick, for a trigger that concerns that display alone.
3178fn render_display(state: &mut AppState, display_id: u32, trigger: TickTrigger) {
3179    let mut nodes = std::mem::take(&mut state.output_nodes);
3180    if let Some(node) = nodes.iter_mut().find(|n| n.id == display_id) {
3181        render_node_tick(state, node, trigger);
3182    }
3183    state.output_nodes = nodes;
3184}
3185
3186fn render_node_tick(
3187    state: &mut AppState,
3188    node: &mut wayland::frontend::OutputNode,
3189    trigger: TickTrigger,
3190) -> bool {
3191    let take_screenshot = state
3192        .pending_screenshot
3193        .as_ref()
3194        .is_some_and(|(id, _)| *id == node.id);
3195    let copy_frame_wanted = state.copy_frame_pending_for(&node.output);
3196    if node.capture.is_none() && !take_screenshot && !copy_frame_wanted {
3197        return false;
3198    }
3199
3200    // Per-display frame pacing under the one shared timer, and under input.
3201    if let Some(cap) = node.capture.as_ref()
3202        && !take_screenshot
3203        && !cap.pace.due(trigger, capture_period(cap), Instant::now())
3204    {
3205        return false;
3206    }
3207
3208    let output = node.output.clone();
3209    let origin: Point<i32, smithay::utils::Logical> = node.pos.into();
3210    let output_scale_val = output.current_scale().fractional_scale();
3211    let (width, height) = match node.capture.as_ref() {
3212        Some(c) => (c.settings.width, c.settings.height),
3213        None => output
3214            .current_mode()
3215            .map(|m| (m.size.w, m.size.h))
3216            .unwrap_or((0, 0)),
3217    };
3218    if width <= 0 || height <= 0 {
3219        return false;
3220    }
3221    if node.frame_buffer.len() < (width as usize) * (height as usize) * 4 {
3222        node.frame_buffer = vec![0u8; (width as usize) * (height as usize) * 4];
3223    }
3224    // A view's tracker is static, so a display that changed resolution, or whose
3225    // screen changed scale, needs a new one; a screen's follows its output on its own.
3226    if node.owner.is_some()
3227        && (node.view_size != (width, height) || (node.view_scale - output_scale_val).abs() > 1e-6)
3228    {
3229        node.damage_tracker =
3230            OutputDamageTracker::new((width, height), output_scale_val, Transform::Normal);
3231        node.content_tracker = None;
3232        node.view_size = (width, height);
3233        node.view_scale = output_scale_val;
3234    }
3235    let logical_w = (width as f64 / output_scale_val).round();
3236    let logical_h = (height as f64 / output_scale_val).round();
3237
3238    // A reconfigured or freshly created output composites its clear color wherever a client
3239    // has not yet answered the new size; publishing is held until one covers the output so
3240    // that gray never reaches the stream. Compositing continues, so the frame callbacks the
3241    // waited-on clients redraw on keep flowing and the hold cannot deadlock on itself. Host
3242    // capture streams the host compositor's own frames and has no such gap.
3243    let hold_frame = match node.content_hold_until {
3244        Some(deadline)
3245            if state.host.is_none()
3246                && Instant::now() < deadline
3247                && !wayland::frontend::output_content_covers(
3248                    &state.space,
3249                    node.owner.unwrap_or(node.id),
3250                    logical_w,
3251                    logical_h,
3252                ) =>
3253        {
3254            true
3255        }
3256        Some(_) => {
3257            node.content_hold_until = None;
3258            false
3259        }
3260        None => false,
3261    };
3262
3263    // A recorder connecting counts as an IDR request, kept armed across skipped ticks.
3264    if let Some(cap) = node.capture.as_mut()
3265        && cap
3266            .recording_sink
3267            .as_ref()
3268            .map(|s| s.should_force_idr())
3269            .unwrap_or(false)
3270    {
3271        cap.request_idr();
3272    }
3273    let requested_idr = node
3274        .capture
3275        .as_ref()
3276        .map(|c| c.pending_force_idr)
3277        .unwrap_or(false);
3278    // A client keyframe request lands on the hardware path's atomic (RequestIdr sets
3279    // it whenever an encode pool exists); host mode consults it — without consuming —
3280    // to decide whether a static screen must re-encode its retained frame.
3281    let hw_idr_pending = node
3282        .capture
3283        .as_ref()
3284        .map(|c| c.encode_controls.force_idr.load(Ordering::Relaxed))
3285        .unwrap_or(false);
3286    let want_idr_for_host = requested_idr || hw_idr_pending;
3287
3288    let mut pool_slot: Option<(usize, Vec<u8>)> = None;
3289    if !hold_frame
3290        && let Some(cap) = node.capture.as_ref()
3291        && let Some(ref pool) = cap.encode_pool
3292    {
3293        pool_slot = pool.try_begin();
3294        if pool_slot.is_none() {
3295            return true;
3296        }
3297    }
3298
3299    let loc_enum = node
3300        .capture
3301        .as_ref()
3302        .map(|c| c.settings.watermark_location_enum)
3303        .unwrap_or(state.settings.watermark_location_enum);
3304    node.overlay_state.update_position(width, height, loc_enum);
3305
3306    // Host-capture mode: the host compositor already blitted this display's frame
3307    // into one of our buffers (screencopy); adopt it in place of compositing. Its tick
3308    // is recorded below, once it is known a frame will be published.
3309    let host_mode = state
3310        .host
3311        .as_ref()
3312        .map(|h| h.has_output_for(node.id))
3313        .unwrap_or(false);
3314    if !host_mode && let Some(cap) = node.capture.as_mut() {
3315        let period = capture_period(cap);
3316        let paced = FramePace::input_paced(state.input_interval, period);
3317        cap.pace.ticked(trigger, period, Instant::now(), paced);
3318    }
3319
3320    // The cursor is composited only on the output the pointer is on, at that output's
3321    // scale; its position is output-local.
3322    let pointer_local: Option<Point<f64, smithay::utils::Logical>> = state
3323        .seat
3324        .get_pointer()
3325        .map(|p| p.current_location())
3326        .and_then(|pos| {
3327            let rect = Rectangle::<f64, smithay::utils::Logical>::new(
3328                origin.to_f64(),
3329                (logical_w, logical_h).into(),
3330            );
3331            if rect.contains(pos) {
3332                Some(pos - origin.to_f64())
3333            } else {
3334                None
3335            }
3336        });
3337
3338    // The picture's capture time for the stats: compositing is this path's capture.
3339    let composite_ns = wayland::host::now_ns();
3340    let mut render_success = false;
3341    let mut render_sync = None;
3342    let mut damage_rects: Vec<Rectangle<i32, Physical>> = Vec::new();
3343    let needs_full = node
3344        .capture
3345        .as_ref()
3346        .map(|c| c.needs_full_render)
3347        .unwrap_or(!node.target_seeded);
3348
3349    if state.host.is_some() && !host_mode {
3350        // No host output backs this display (start_capture already warned):
3351        // produce nothing rather than the compositor's own empty content.
3352        if let Some((id, buf)) = pool_slot.take()
3353            && let Some(cap) = node.capture.as_ref()
3354            && let Some(ref pool) = cap.encode_pool
3355        {
3356            pool.cancel(id, buf);
3357        }
3358        return false;
3359    }
3360    // Dmabuf handed to the GPU encoder in host mode (from the new or retained frame).
3361    let mut host_enc_dmabuf: Option<Dmabuf> = None;
3362    let mut new_stamp: Option<i64> = None;
3363    // Host software frames arrive BGRA, so anything reading this display's frame buffer
3364    // back has to know it is not the GLES readback's RGBA.
3365    let mut host_cpu_frame = false;
3366    if host_mode {
3367        const RETAINED_OK: u8 = 0;
3368        const RETAINED_NONE: u8 = 1;
3369        // The host produced the buffer type this display's consumer cannot take; each
3370        // direction has its own recovery below.
3371        const RETAINED_CPU_FRAME: u8 = 2;
3372        const RETAINED_GPU_FRAME: u8 = 3;
3373        let host_idx = node.id;
3374        let gpu_encoder = node
3375            .capture
3376            .as_ref()
3377            .map(|c| c.video_encoder.is_some())
3378            .unwrap_or(false);
3379        // The session steps out of `state` while frames are adopted so the
3380        // renderer can composite the watermark / serve screenshot readbacks.
3381        let host = state.host.take().unwrap();
3382        // Stale-geometry rejection: a frame captured before a mode change is
3383        // useless at the new size (the CPU path would blit old-pitch rows into
3384        // a new-size buffer, and HW encoders cannot take a mismatched dmabuf),
3385        // so it goes back to the pool instead of being retained or encoded.
3386        let expect = node
3387            .capture
3388            .as_ref()
3389            .map(|c| (c.settings.width, c.settings.height));
3390        let new_frame = match (host.try_take_frame(host_idx), expect) {
3391            (Some(f), Some((w, h))) if f.width != w || f.height != h => {
3392                host.release_frame(host_idx, f);
3393                None
3394            }
3395            (f, _) => f,
3396        };
3397        let have_new = new_frame.is_some();
3398        new_stamp = new_frame.as_ref().map(|f| f.stamp_ns);
3399        // Streaming mode wants a constant-rate stream (the client's decoder pipeline
3400        // is built for it), so re-encode the retained frame every tick like the
3401        // compositor path does. Outside streaming mode, stay damage-driven, waking
3402        // only for a pending IDR (a viewer opening its keyframe gate), a screenshot
3403        // request, or a bouncing watermark that must keep moving.
3404        let streaming = node
3405            .capture
3406            .as_ref()
3407            .map(|c| c.settings.video_streaming_mode)
3408            .unwrap_or(false);
3409        let wm_active = node.overlay_state.is_active();
3410        let wm_animated = wm_active && node.overlay_state.is_animated();
3411        // Without a fresh frame, retained content is published for a reason of its own: a
3412        // keyframe request, a screenshot, a moving watermark, or streaming mode's constant
3413        // rate once the host has gone quiet. A live host's next frame is imminent and
3414        // renders through its own wake, so the timer is held off rather than publishing a
3415        // duplicate that frame would then queue behind.
3416        let now = Instant::now();
3417        let period = node.capture.as_ref().map(capture_period);
3418        let host_quiet = node.capture.as_ref().is_none_or(|cap| {
3419            cap.pace
3420                .since_last_tick(now)
3421                .is_none_or(|since| since >= capture_period(cap).mul_f64(HOST_QUIET_PERIODS))
3422        });
3423        if !have_new
3424            && !want_idr_for_host
3425            && !take_screenshot
3426            && !wm_animated
3427            && !(streaming && host_quiet)
3428        {
3429            if let Some((id, buf)) = pool_slot.take()
3430                && let Some(cap) = node.capture.as_ref()
3431                && let Some(ref pool) = cap.encode_pool
3432            {
3433                pool.cancel(id, buf);
3434            }
3435            if let Some(cap) = node.capture.as_mut()
3436                && let Some(period) = period
3437            {
3438                let held = if streaming {
3439                    cap.pace
3440                        .last_tick
3441                        .map_or(now, |last| last + period.mul_f64(HOST_QUIET_PERIODS))
3442                } else {
3443                    now + period
3444                };
3445                cap.pace.defer(held.max(now + Duration::from_millis(1)));
3446            }
3447            state.host = Some(host);
3448            return false;
3449        }
3450        if let Some(cap) = node.capture.as_mut()
3451            && let Some(period) = period
3452        {
3453            cap.pace.ticked(trigger, period, now, false);
3454        }
3455        if let Some(f) = new_frame {
3456            host.retain_frame(host_idx, f);
3457        }
3458        // Consume the (new or prior) retained frame's content into the pool slot /
3459        // GPU dmabuf. The frame stays retained for the next IDR. The CPU path
3460        // blends the watermark in place; the GPU path composites it below. Damage
3461        // is the fresh blit's; a re-encode of retained content has none of its own.
3462        let mut wm_drawn = false;
3463        let outcome = host.with_retained(host_idx, |r| {
3464            let Some(f) = r else { return RETAINED_NONE };
3465            damage_rects = if have_new {
3466                f.damage.clone()
3467            } else {
3468                Vec::new()
3469            };
3470            if let Some(cpu) = f.cpu.as_ref() {
3471                if gpu_encoder {
3472                    return RETAINED_CPU_FRAME;
3473                }
3474                host_cpu_frame = true;
3475                if let Some((_, ref mut buf)) = pool_slot {
3476                    cpu.write_bgra(f.width, f.height, buf);
3477                    if wm_active {
3478                        node.overlay_state.blend_bgra(
3479                            buf,
3480                            (f.width as usize) * 4,
3481                            f.width,
3482                            f.height,
3483                        );
3484                        wm_drawn = true;
3485                    }
3486                }
3487                cpu.write_bgra(f.width, f.height, &mut node.frame_buffer);
3488                if wm_active {
3489                    node.overlay_state.blend_bgra(
3490                        &mut node.frame_buffer,
3491                        (f.width as usize) * 4,
3492                        f.width,
3493                        f.height,
3494                    );
3495                }
3496            } else if let Some(dmabuf) = f.dmabuf.as_ref() {
3497                if !gpu_encoder {
3498                    return RETAINED_GPU_FRAME;
3499                }
3500                host_enc_dmabuf = Some(dmabuf.clone());
3501            }
3502            RETAINED_OK
3503        });
3504        // A fresh blit may still be in flight on the GPU when the compositor announces it.
3505        if have_new && let Some(src) = host_enc_dmabuf.as_ref() {
3506            wayland::host::wait_gpu_done(src);
3507        }
3508        // GPU path: composite the watermark and serve screenshot readbacks with
3509        // the renderer. A bouncing watermark re-composes retained content into
3510        // this display's offscreen target every tick (drawing in place would
3511        // trail); anchored watermarks are stamped once onto each fresh blit and
3512        // ride along with retained re-encodes.
3513        if let Some(src) = host_enc_dmabuf.clone() {
3514            if wm_active && let Some(renderer) = state.gles_renderer.as_mut() {
3515                if wm_animated {
3516                    if let Some((_, target)) = node.offscreen_buffer.as_mut() {
3517                        match compose_host_watermark(
3518                            renderer,
3519                            &node.overlay_state,
3520                            &src,
3521                            target,
3522                            (width, height),
3523                        ) {
3524                            Ok(sync) => {
3525                                render_sync = Some(sync);
3526                                host_enc_dmabuf = Some(target.clone());
3527                                wm_drawn = true;
3528                            }
3529                            Err(e) => warn_once_host_watermark(&e),
3530                        }
3531                    }
3532                } else if have_new {
3533                    let mut target = src.clone();
3534                    match draw_host_watermark(
3535                        renderer,
3536                        &node.overlay_state,
3537                        &mut target,
3538                        (width, height),
3539                    ) {
3540                        Ok(sync) => {
3541                            render_sync = Some(sync);
3542                            wm_drawn = true;
3543                        }
3544                        Err(e) => warn_once_host_watermark(&e),
3545                    }
3546                }
3547            }
3548            if take_screenshot && let Some(renderer) = state.gles_renderer.as_mut() {
3549                let mut shot = host_enc_dmabuf.clone().unwrap_or(src);
3550                match renderer.bind(&mut shot) {
3551                    Ok(fb) => {
3552                        let rect = Rectangle::new((0, 0).into(), (width, height).into());
3553                        match renderer.copy_framebuffer(&fb, rect, Fourcc::Abgr8888) {
3554                            Ok(mapping) => match renderer.map_texture(&mapping) {
3555                                Ok(data) => {
3556                                    let n = data.len().min(node.frame_buffer.len());
3557                                    node.frame_buffer[..n].copy_from_slice(&data[..n]);
3558                                }
3559                                Err(e) => eprintln!("[HostCapture] screenshot map: {e:?}"),
3560                            },
3561                            Err(e) => eprintln!("[HostCapture] screenshot copy: {e:?}"),
3562                        }
3563                    }
3564                    Err(e) => eprintln!("[HostCapture] screenshot bind: {e:?}"),
3565                };
3566            }
3567        }
3568        if wm_drawn && let Some(rect) = node.overlay_state.damage_rect(width, height) {
3569            damage_rects.push(rect);
3570        }
3571        state.host = Some(host);
3572        if outcome != RETAINED_OK {
3573            if let Some((id, buf)) = pool_slot.take()
3574                && let Some(cap) = node.capture.as_ref()
3575                && let Some(ref pool) = cap.encode_pool
3576            {
3577                pool.cancel(id, buf);
3578            }
3579            // A frame type this display's consumer cannot take is recovered from rather
3580            // than warned about: either side of the mismatch would otherwise stream
3581            // nothing for the life of the capture.
3582            if outcome == RETAINED_GPU_FRAME {
3583                // The host is blitting into dmabufs a CPU encoder cannot read; ask it
3584                // for shm frames. Frames resume once the capture thread reallocates,
3585                // and the request is idempotent, so repeating it per tick costs nothing.
3586                if let Some(h) = state.host.as_ref() {
3587                    h.set_buffer_type(node.id, false);
3588                }
3589            } else if outcome == RETAINED_CPU_FRAME
3590                && let Some(cap) = node.capture.as_mut()
3591            {
3592                // The host hands out software frames only (no zwp_linux_dmabuf v3),
3593                // so the zero-copy session has nothing to import: demote it to the
3594                // readback path, which encodes those frames as they arrive.
3595                eprintln!(
3596                    "[HostCapture] host delivers software frames; demoting the zero-copy encoder to readback encode."
3597                );
3598                let _report = report::enter(&cap.report);
3599                report::capture("readback", false);
3600                report::zero_copy_available(false);
3601                report::capture_reason("the host compositor delivers software frames only");
3602                cap.video_encoder = None;
3603                let s = &cap.settings;
3604                let try_gpu = s.codec.is_video() && !(s.use_cpu || s.encode_node_index == -1);
3605                bootstrap_readback_pool(cap, node.id, false, try_gpu, None, None);
3606                cap.request_idr();
3607                // The consumer is now a CPU one, so the host stops preferring GPU slots.
3608                if let Some(h) = state.host.as_ref() {
3609                    h.set_buffer_type(node.id, false);
3610                }
3611            }
3612            return false;
3613        }
3614        render_success = true;
3615    }
3616
3617    if !host_mode && state.use_gpu {
3618        if let Some(renderer) = state.gles_renderer.as_mut() {
3619            let mut cap = node.capture.as_mut();
3620            // A screen resized ahead of the capture restart that follows has a target for the
3621            // frame the restart will ask for, not for this one.
3622            if let Some((bo, dmabuf)) = node.offscreen_buffer.as_mut()
3623                && (bo.width() as i32, bo.height() as i32) == (width, height)
3624            {
3625                let render_age = if node.overlay_state.is_animated() || needs_full {
3626                    0
3627                } else {
3628                    1
3629                };
3630                match renderer.bind(dmabuf) {
3631                    Ok(mut frame) => {
3632                        let mut elements: Vec<
3633                            CompositionElements<
3634                                GlesRenderer,
3635                                WaylandSurfaceRenderElement<GlesRenderer>,
3636                            >,
3637                        > = Vec::new();
3638
3639                        if state.render_cursor_on_framebuffer
3640                            && let Some(pos) = pointer_local
3641                        {
3642                            let scale = Scale::from(output_scale_val);
3643
3644                            if let Some(CursorImageStatus::Named(icon)) = &state.current_cursor_icon
3645                            {
3646                                let name = wayland::frontend::cursor_icon_to_str(icon);
3647                                let time =
3648                                    Duration::from_millis(state.clock.now().as_millis() as u64);
3649                                if let Some(image) = state.cursor_helper.get_image_by_name(
3650                                    name,
3651                                    output_scale_val.round() as u32,
3652                                    time,
3653                                ) && let Some(elem) = node.overlay_state.get_cursor_element(
3654                                    renderer,
3655                                    image,
3656                                    pos,
3657                                    output_scale_val,
3658                                ) {
3659                                    elements.push(CompositionElements::Cursor(elem));
3660                                }
3661                            } else if let Some(CursorImageStatus::Surface(surface)) =
3662                                &state.current_cursor_icon
3663                            {
3664                                let hot = cursor_surface_hotspot(surface).to_f64();
3665                                let phys_pos = (pos - hot).to_physical(scale);
3666                                let elem_result = with_states(surface, |states| {
3667                                    WaylandSurfaceRenderElement::from_surface(
3668                                        renderer,
3669                                        surface,
3670                                        states,
3671                                        phys_pos,
3672                                        1.0,
3673                                        smithay::backend::renderer::element::Kind::Cursor,
3674                                    )
3675                                });
3676                                if let Ok(Some(cursor_elem)) = elem_result {
3677                                    elements.push(CompositionElements::Surface(cursor_elem));
3678                                }
3679                            } else if state.current_cursor_icon.is_none() {
3680                                let time =
3681                                    Duration::from_millis(state.clock.now().as_millis() as u64);
3682                                let image = state
3683                                    .cursor_helper
3684                                    .get_image(output_scale_val.round() as u32, time);
3685                                if let Some(elem) = node.overlay_state.get_cursor_element(
3686                                    renderer,
3687                                    image,
3688                                    pos,
3689                                    output_scale_val,
3690                                ) {
3691                                    elements.push(CompositionElements::Cursor(elem));
3692                                }
3693                            }
3694                        }
3695
3696                        if let Some(elem) = node.overlay_state.get_watermark_element(renderer) {
3697                            elements.push(CompositionElements::Cursor(elem));
3698                        }
3699
3700                        {
3701                            let layer_map = layer_map_for_output(&output);
3702
3703                            push_layer_elements(
3704                                renderer,
3705                                &mut elements,
3706                                &layer_map,
3707                                smithay::wayland::shell::wlr_layer::Layer::Overlay,
3708                                output_scale_val,
3709                            );
3710                            push_layer_elements(
3711                                renderer,
3712                                &mut elements,
3713                                &layer_map,
3714                                smithay::wayland::shell::wlr_layer::Layer::Top,
3715                                output_scale_val,
3716                            );
3717                        }
3718
3719                        for window in state.space.elements_for_output(&output).rev() {
3720                            let window_loc =
3721                                state.space.element_location(window).unwrap_or_default() - origin;
3722
3723                            elements.extend(
3724                                window
3725                                    .render_elements(
3726                                        renderer,
3727                                        window_loc.to_physical_precise_round(output_scale_val),
3728                                        Scale::from(output_scale_val),
3729                                        1.0,
3730                                    )
3731                                    .into_iter()
3732                                    .map(CompositionElements::Space),
3733                            );
3734                        }
3735
3736                        {
3737                            let layer_map = layer_map_for_output(&output);
3738
3739                            push_layer_elements(
3740                                renderer,
3741                                &mut elements,
3742                                &layer_map,
3743                                smithay::wayland::shell::wlr_layer::Layer::Bottom,
3744                                output_scale_val,
3745                            );
3746                            push_layer_elements(
3747                                renderer,
3748                                &mut elements,
3749                                &layer_map,
3750                                smithay::wayland::shell::wlr_layer::Layer::Background,
3751                                output_scale_val,
3752                            );
3753                        }
3754                        match node.damage_tracker.render_output(
3755                            renderer,
3756                            &mut frame,
3757                            render_age,
3758                            &elements,
3759                            [0.1, 0.1, 0.1, 1.0],
3760                        ) {
3761                            Ok(result) => {
3762                                render_success = true;
3763                                if let Some(damage) = result.damage {
3764                                    damage_rects = damage.clone();
3765                                    node.undrawn_ticks = 0;
3766                                } else {
3767                                    node.undrawn_ticks += 1;
3768                                    if node.undrawn_ticks >= UNDRAWN_TICKS_PER_FRAME {
3769                                        node.undrawn_ticks = 0;
3770                                        let _ = renderer
3771                                            .render(
3772                                                &mut frame,
3773                                                (width, height).into(),
3774                                                Transform::Normal,
3775                                            )
3776                                            .and_then(|f| f.finish());
3777                                    }
3778                                }
3779                                if wait_render_fence(&result.sync, node.id) {
3780                                    if let Some(c) = cap.as_deref_mut() {
3781                                        c.needs_full_render = false;
3782                                    }
3783                                } else {
3784                                    // The tracker counts the unfinished frame as drawn, so the
3785                                    // target is published to nobody and redrawn whole next tick.
3786                                    render_success = false;
3787                                    damage_rects.clear();
3788                                    if let Some(c) = cap.as_deref_mut() {
3789                                        c.needs_full_render = true;
3790                                    }
3791                                }
3792                            }
3793                            Err(e) => eprintln!("Render error: {:?}", e),
3794                        }
3795                        if let Some(c) = cap {
3796                            if !damage_rects.is_empty() {
3797                                c.content_gen += 1;
3798                            }
3799                            if let Some((id, ref mut buf)) = pool_slot {
3800                                // No-damage ticks skip the readback, so a pooled buffer can
3801                                // lag the offscreen target whenever the encoder held the
3802                                // other slot across a tick; one catch-up readback keeps
3803                                // every published buffer current.
3804                                if render_success && c.pool_content_gen[id] != c.content_gen {
3805                                    read_back_rgba(renderer, &mut frame, width, height, buf);
3806                                    c.pool_content_gen[id] = c.content_gen;
3807                                }
3808                            }
3809                        }
3810                        if pool_slot.is_none() && take_screenshot {
3811                            read_back_rgba(
3812                                renderer,
3813                                &mut frame,
3814                                width,
3815                                height,
3816                                &mut node.frame_buffer,
3817                            );
3818                        }
3819                    }
3820                    Err(e) => eprintln!("Failed to bind buffer: {:?}", e),
3821                }
3822            }
3823        }
3824    } else if !host_mode && let Some(renderer) = state.pixman_renderer.as_mut() {
3825        let mut cap = node.capture.as_mut();
3826        let (ptr, buf_age) = match pool_slot {
3827            Some((id, ref mut buf)) => {
3828                let age = cap
3829                    .as_ref()
3830                    .map(|c| {
3831                        if c.pool_last_render[id] == 0 {
3832                            0
3833                        } else {
3834                            (c.render_seq + 1 - c.pool_last_render[id]) as usize
3835                        }
3836                    })
3837                    .unwrap_or(0);
3838                (buf.as_mut_ptr() as *mut u32, age)
3839            }
3840            None => (node.frame_buffer.as_mut_ptr() as *mut u32, 0),
3841        };
3842        let mut image = unsafe {
3843            pixman::Image::from_raw_mut(
3844                pixman::FormatCode::A8R8G8B8,
3845                width as usize,
3846                height as usize,
3847                ptr,
3848                (width as usize) * 4,
3849                false,
3850            )
3851            .expect("Failed to create pixman image")
3852        };
3853        match renderer.bind(&mut image) {
3854            Ok(mut frame) => {
3855                let mut elements: Vec<
3856                    CompositionElements<
3857                        PixmanRenderer,
3858                        WaylandSurfaceRenderElement<PixmanRenderer>,
3859                    >,
3860                > = Vec::new();
3861
3862                if state.render_cursor_on_framebuffer
3863                    && let Some(pos) = pointer_local
3864                {
3865                    let scale = Scale::from(output_scale_val);
3866
3867                    if let Some(CursorImageStatus::Named(icon)) = &state.current_cursor_icon {
3868                        let name = wayland::frontend::cursor_icon_to_str(icon);
3869                        let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3870                        if let Some(image) = state.cursor_helper.get_image_by_name(
3871                            name,
3872                            output_scale_val.round() as u32,
3873                            time,
3874                        ) && let Some(elem) = node.overlay_state.get_cursor_element(
3875                            renderer,
3876                            image,
3877                            pos,
3878                            output_scale_val,
3879                        ) {
3880                            elements.push(CompositionElements::Cursor(elem));
3881                        }
3882                    } else if let Some(CursorImageStatus::Surface(surface)) =
3883                        &state.current_cursor_icon
3884                    {
3885                        let hot = cursor_surface_hotspot(surface).to_f64();
3886                        let phys_pos = (pos - hot).to_physical(scale);
3887                        let elem_result = with_states(surface, |states| {
3888                            WaylandSurfaceRenderElement::from_surface(
3889                                renderer,
3890                                surface,
3891                                states,
3892                                phys_pos,
3893                                1.0,
3894                                smithay::backend::renderer::element::Kind::Cursor,
3895                            )
3896                        });
3897                        if let Ok(Some(cursor_elem)) = elem_result {
3898                            elements.push(CompositionElements::Surface(cursor_elem));
3899                        }
3900                    } else if state.current_cursor_icon.is_none() {
3901                        let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3902                        let image = state
3903                            .cursor_helper
3904                            .get_image(output_scale_val.round() as u32, time);
3905                        if let Some(elem) = node.overlay_state.get_cursor_element(
3906                            renderer,
3907                            image,
3908                            pos,
3909                            output_scale_val,
3910                        ) {
3911                            elements.push(CompositionElements::Cursor(elem));
3912                        }
3913                    }
3914                }
3915
3916                if let Some(elem) = node.overlay_state.get_watermark_element(renderer) {
3917                    elements.push(CompositionElements::Cursor(elem));
3918                }
3919
3920                {
3921                    let layer_map = layer_map_for_output(&output);
3922
3923                    push_layer_elements(
3924                        renderer,
3925                        &mut elements,
3926                        &layer_map,
3927                        smithay::wayland::shell::wlr_layer::Layer::Overlay,
3928                        output_scale_val,
3929                    );
3930                    push_layer_elements(
3931                        renderer,
3932                        &mut elements,
3933                        &layer_map,
3934                        smithay::wayland::shell::wlr_layer::Layer::Top,
3935                        output_scale_val,
3936                    );
3937                }
3938
3939                for window in state.space.elements_for_output(&output).rev() {
3940                    let loc = state.space.element_location(window).unwrap_or_default() - origin;
3941
3942                    elements.extend(
3943                        window
3944                            .render_elements(
3945                                renderer,
3946                                loc.to_physical_precise_round(output_scale_val),
3947                                Scale::from(output_scale_val),
3948                                1.0,
3949                            )
3950                            .into_iter()
3951                            .map(CompositionElements::Space),
3952                    );
3953                }
3954
3955                {
3956                    let layer_map = layer_map_for_output(&output);
3957
3958                    push_layer_elements(
3959                        renderer,
3960                        &mut elements,
3961                        &layer_map,
3962                        smithay::wayland::shell::wlr_layer::Layer::Bottom,
3963                        output_scale_val,
3964                    );
3965                    push_layer_elements(
3966                        renderer,
3967                        &mut elements,
3968                        &layer_map,
3969                        smithay::wayland::shell::wlr_layer::Layer::Background,
3970                        output_scale_val,
3971                    );
3972                }
3973
3974                let render_age = if node.overlay_state.is_animated() || needs_full {
3975                    0
3976                } else {
3977                    buf_age
3978                };
3979                let changed = node
3980                    .content_tracker
3981                    .get_or_insert_with(|| {
3982                        OutputDamageTracker::from_mode_source(node.damage_tracker.mode().clone())
3983                    })
3984                    .damage_output(1, &elements)
3985                    .ok()
3986                    .map(|(damage, _)| damage.cloned().unwrap_or_default());
3987                match node.damage_tracker.render_output(
3988                    renderer,
3989                    &mut frame,
3990                    render_age,
3991                    &elements,
3992                    [0.1, 0.1, 0.1, 1.0],
3993                ) {
3994                    Ok(result) => {
3995                        render_success = true;
3996                        if let Some(c) = cap.as_deref_mut() {
3997                            c.needs_full_render = false;
3998                        }
3999                        if let Some(damage) = result.damage {
4000                            damage_rects = damage.clone();
4001                        }
4002                        if let Some(changed) = changed.filter(|_| render_age > 1) {
4003                            damage_rects = changed;
4004                        }
4005                    }
4006                    Err(e) => eprintln!("Render error: {:?}", e),
4007                }
4008                if let Some(c) = cap {
4009                    c.render_seq += 1;
4010                    if render_success && let Some((id, _)) = pool_slot {
4011                        c.pool_last_render[id] = c.render_seq;
4012                    }
4013                }
4014            }
4015            Err(e) => eprintln!("Failed to bind pixman image: {:?}", e),
4016        }
4017    }
4018
4019    if render_success {
4020        node.target_seeded = true;
4021    }
4022    // Views share one output, so its clients are driven once a frame, by the
4023    // fastest display capturing that screen (ties go to the lowest number) -- a
4024    // client asked to draw once per view would render as many times a frame as
4025    // there are displays over it, one asked by nobody (the screen itself is often
4026    // captured by views alone) would freeze on the frame it first painted, and one
4027    // driven by a slower sibling would stream stale frames on the faster display.
4028    let screen = node.owner.unwrap_or(node.id);
4029    let own_fps = node
4030        .capture
4031        .as_ref()
4032        .map(|c| c.settings.target_fps)
4033        .unwrap_or(0.0);
4034    let own_id = node.id;
4035    let drives_screen = !state.output_nodes.iter().any(|n| {
4036        let fps = n
4037            .capture
4038            .as_ref()
4039            .map(|c| c.settings.target_fps)
4040            .unwrap_or(0.0);
4041        n.owner.unwrap_or(n.id) == screen
4042            && n.capture.is_some()
4043            && (fps > own_fps || (fps == own_fps && n.id < own_id))
4044    });
4045    if render_success && drives_screen {
4046        let time = state.clock.now();
4047        // The composited frame is what the capture consumes, so this render is also the
4048        // presentation moment for wp_presentation feedback.
4049        let mut feedback = OutputPresentationFeedback::new(&output);
4050        for window in state.space.elements_for_output(&output) {
4051            window.send_frame(&output, time, Some(Duration::ZERO), |_, _| {
4052                Some(output.clone())
4053            });
4054            window.take_presentation_feedback(
4055                &mut feedback,
4056                |_, _| Some(output.clone()),
4057                |_, _| wp_presentation_feedback::Kind::empty(),
4058            );
4059        }
4060        // Panels, backgrounds, and other layer-shell surfaces are composited from the
4061        // layer map rather than the space, so they need the callback separately: one
4062        // that never arrives leaves a client which draws on frame callbacks showing
4063        // whatever it painted first, for as long as the session lasts.
4064        for layer in layer_map_for_output(&output).layers() {
4065            layer.send_frame(&output, time, Some(Duration::ZERO), |_, _| {
4066                Some(output.clone())
4067            });
4068            layer.take_presentation_feedback(
4069                &mut feedback,
4070                |_, _| Some(output.clone()),
4071                |_, _| wp_presentation_feedback::Kind::empty(),
4072            );
4073        }
4074        send_cursor_frame(state, &output, time);
4075        let refresh = match node.capture.as_ref() {
4076            Some(c) => Refresh::Fixed(Duration::from_secs_f64(
4077                1.0 / c.settings.target_fps.max(1.0),
4078            )),
4079            None => Refresh::Unknown,
4080        };
4081        node.frame_seq += 1;
4082        feedback.presented(
4083            time,
4084            refresh,
4085            node.frame_seq,
4086            wp_presentation_feedback::Kind::Vsync,
4087        );
4088
4089        // Host mode renders nothing locally, so there is no composited buffer to
4090        // serve capture clients from; their frames stay parked.
4091        if !host_mode {
4092            service_copy_frames(state, node, width, height, &damage_rects);
4093        }
4094
4095        if !hold_frame && let Some(cap) = node.capture.as_mut() {
4096            // A dead encode thread (panic, unexpected exit) cannot drain the pool;
4097            // every publish would park the slot and each tick would silently skip
4098            // while is_capturing still reports true. Rebuild the readback path in
4099            // place, reusing a cleanly handed-back encoder session if any.
4100            if cap.encode_join.as_ref().is_some_and(|j| j.is_finished()) {
4101                let prior = cap.encode_join.take().and_then(|j| j.join().ok().flatten());
4102                if let Some(pool) = cap.encode_pool.take() {
4103                    pool.shutdown();
4104                }
4105                let s = &cap.settings;
4106                let try_gpu = s.codec.is_video() && !(s.use_cpu || s.encode_node_index == -1);
4107                eprintln!("[Wayland] encode thread died; rebuilding the readback path.");
4108                // Host frames reach the pool as BGRA; only our own GLES readback produces RGBA.
4109                bootstrap_readback_pool(
4110                    cap,
4111                    node.id,
4112                    state.use_gpu && !host_mode,
4113                    try_gpu,
4114                    prior,
4115                    None,
4116                );
4117                cap.request_idr();
4118            }
4119            if cap.encode_pool.is_some() {
4120                if take_screenshot && let Some((_, ref buf)) = pool_slot {
4121                    let n = buf.len().min(node.frame_buffer.len());
4122                    node.frame_buffer[..n].copy_from_slice(&buf[..n]);
4123                }
4124                if let Some((id, buf)) = pool_slot.take() {
4125                    let frame = WlFrame {
4126                        id,
4127                        buf,
4128                        frame_id: cap.frame_counter,
4129                        damage: std::mem::take(&mut damage_rects),
4130                        is_animated: node.overlay_state.is_animated(),
4131                        captured_ns: new_stamp.unwrap_or(composite_ns),
4132                    };
4133                    if let Some(pool) = cap.encode_pool.as_ref() {
4134                        pool.publish(frame);
4135                    }
4136                    cap.frame_counter = cap.frame_counter.wrapping_add(1);
4137                }
4138            } else if let Some(ref mut encoder) = cap.video_encoder {
4139                // Deliver the parked frame (if any) first, WITHOUT blocking: this runs
4140                // on the calloop thread, and a blocking send would freeze
4141                // input/command/Wayland dispatch for as long as the Python consumer
4142                // stalls. While a frame stays parked, no new frame is encoded — an
4143                // encoded frame joins the H.264 reference chain and can never be
4144                // dropped — and the tick's damage is latched so the pause never loses
4145                // a change.
4146                let slot_free = match cap.pending_hw_delivery.take() {
4147                    None => true,
4148                    Some(pending) => match cap.deliver_tx.as_ref() {
4149                        None => true,
4150                        Some(tx) => match tx.try_send(pending) {
4151                            Ok(()) => true,
4152                            Err(std::sync::mpsc::TrySendError::Full(p)) => {
4153                                cap.pending_hw_delivery = Some(p);
4154                                false
4155                            }
4156                            Err(std::sync::mpsc::TrySendError::Disconnected(_)) => true,
4157                        },
4158                    },
4159                };
4160                if !slot_free {
4161                    if !damage_rects.is_empty() {
4162                        cap.pending_hw_damage = true;
4163                    }
4164                } else {
4165                    let is_animated = node.overlay_state.is_animated();
4166                    let damage =
4167                        match crate::pipeline::Damage::of_rects(&damage_rects, width, height) {
4168                            crate::pipeline::Damage::None
4169                                if std::mem::take(&mut cap.pending_hw_damage) =>
4170                            {
4171                                crate::pipeline::Damage::Unknown
4172                            }
4173                            damage => {
4174                                cap.pending_hw_damage = false;
4175                                damage
4176                            }
4177                        };
4178                    let decision = crate::pipeline::decide_hw_fullframe(
4179                        &mut cap.vaapi_state,
4180                        &cap.settings,
4181                        cap.frame_counter,
4182                        damage,
4183                        is_animated,
4184                        requested_idr,
4185                        crate::pipeline::EncoderQuality::of(encoder),
4186                    );
4187                    let mut send_frame = decision.send;
4188                    let force_idr = decision.force_idr;
4189                    let target_qp = decision.target_qp;
4190
4191                    let mut frame_out = false;
4192                    if send_frame
4193                        && let Some(sync) = render_sync.take()
4194                        && !wait_render_fence(&sync, node.id)
4195                    {
4196                        send_frame = false;
4197                    }
4198                    if send_frame {
4199                        // Host-capture frames encode from the buffer the host blitted
4200                        // into; otherwise from this display's own composited buffer.
4201                        let enc_dmabuf: Option<Dmabuf> = host_enc_dmabuf
4202                            .clone()
4203                            .or_else(|| node.offscreen_buffer.as_ref().map(|(_, d)| d.clone()));
4204                        let encode_start_ns = wayland::host::now_ns();
4205                        decision.prepare(encoder);
4206                        let result = match enc_dmabuf {
4207                            Some(ref dmabuf) => encoder.encode_dmabuf(
4208                                dmabuf,
4209                                cap.frame_counter as u64,
4210                                target_qp,
4211                                force_idr,
4212                            ),
4213                            None => Err(
4214                                "zero-copy encode requires an offscreen buffer (GPU context)"
4215                                    .to_string(),
4216                            ),
4217                        };
4218
4219                        if let Ok(data) = result {
4220                            cap.hw_error_streak = 0;
4221                            cap.hw_rebuilt = false;
4222                            if !data.is_empty() {
4223                                frame_out = true;
4224                                if wayland::host::trace()
4225                                    && let Some(stamp) = new_stamp
4226                                {
4227                                    let age = (wayland::host::now_ns() - stamp) as f64 / 1e6;
4228                                    eprintln!(
4229                                        "[HostTrace] output {} frame {} encoded +{age:.2}ms {}B",
4230                                        node.id,
4231                                        cap.frame_counter,
4232                                        data.len()
4233                                    );
4234                                }
4235                                cap.encode_stats.frames.fetch_add(1, Ordering::Relaxed);
4236                                cap.encode_stats.stripes.fetch_add(1, Ordering::Relaxed);
4237                                if let Some(ref tx) = cap.deliver_tx {
4238                                    let stripes = vec![EncodedStripe {
4239                                        data: Arc::new(data),
4240                                        codec: cap.settings.codec,
4241                                        stripe_y_start: 0,
4242                                        stripe_height: height,
4243                                        frame_id: cap.frame_counter as i32,
4244                                        timing: FrameTiming {
4245                                            capture_ns: new_stamp.unwrap_or(composite_ns),
4246                                            encode_start_ns,
4247                                            encode_end_ns: wayland::host::now_ns(),
4248                                        },
4249                                        reference: encoder.last_reference(),
4250                                    }];
4251                                    if let Some(ref socket) = cap.recording_sink {
4252                                        socket.write_frame(&stripes, width, height);
4253                                    }
4254                                    crate::recorder::wayland_tap(node.id, &stripes);
4255                                    // Non-blocking: a full slot parks the frame (delivered
4256                                    // ahead of any new encode above).
4257                                    match tx.try_send(stripes) {
4258                                        Ok(()) => {}
4259                                        Err(std::sync::mpsc::TrySendError::Full(s)) => {
4260                                            cap.pending_hw_delivery = Some(s);
4261                                        }
4262                                        Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {}
4263                                    }
4264                                }
4265                            }
4266                        } else if let Err(e) = result {
4267                            eprintln!("[Wayland] HW encode error: {e}");
4268                            cap.hw_error_streak = cap.hw_error_streak.saturating_add(1);
4269                            if cap.hw_error_streak == HW_ERROR_RECOVERY_THRESHOLD {
4270                                let _report = report::enter(&cap.report);
4271                                // The zero-copy session persistently fails after having
4272                                // worked (driver hiccup, CUDA pressure from a co-tenant):
4273                                // rebuild the session once, else demote to the readback
4274                                // path. Streaming black frames forever is not an option.
4275                                // A session whose encodes keep failing still constructs, so
4276                                // the rebuild only counts as recovery until the next streak;
4277                                // otherwise the stream would rebuild in a loop and never demote.
4278                                let rebuilt = if cap.hw_rebuilt {
4279                                    None
4280                                } else {
4281                                    // The broken session is released before its replacement is
4282                                    // opened: the failure it recovers from is usually device
4283                                    // memory pressure, and holding both at once is what would
4284                                    // make the rebuild fail too.
4285                                    drop(cap.video_encoder.take());
4286                                    rebuild_zerocopy_encoder(cap, state)
4287                                };
4288                                match rebuilt {
4289                                    Some(enc) => {
4290                                        cap.video_encoder = Some(enc);
4291                                        cap.pending_force_idr = true;
4292                                        cap.hw_rebuilt = true;
4293                                        eprintln!(
4294                                            "[Wayland] zero-copy HW encoder rebuilt after repeated encode errors."
4295                                        );
4296                                    }
4297                                    None => {
4298                                        eprintln!(
4299                                            "[Wayland] zero-copy HW encoder unrecoverable; demoting to readback encode."
4300                                        );
4301                                        report::capture("readback", false);
4302                                        report::capture_reason(
4303                                            "the zero-copy encoder failed repeatedly and was given up",
4304                                        );
4305                                        cap.video_encoder = None;
4306                                        cap.hw_rebuilt = false;
4307                                        // Mirror the startup intent: readback still
4308                                        // tries the GPU unless the operator opted out.
4309                                        let s = &cap.settings;
4310                                        let try_gpu = s.codec.is_video()
4311                                            && !(s.use_cpu || s.encode_node_index == -1);
4312                                        // Host frames reach the pool as BGRA; only our own
4313                                        // GLES readback produces RGBA.
4314                                        bootstrap_readback_pool(
4315                                            cap,
4316                                            node.id,
4317                                            state.use_gpu && !host_mode,
4318                                            try_gpu,
4319                                            None,
4320                                            None,
4321                                        );
4322                                        // The host has to switch to buffers the readback path
4323                                        // can read back on the CPU.
4324                                        if host_mode && let Some(h) = state.host.as_ref() {
4325                                            h.set_buffer_type(node.id, false);
4326                                        }
4327                                    }
4328                                }
4329                                cap.hw_error_streak = 0;
4330                            }
4331                        }
4332                    }
4333                    // An unserved request stays armed: on an infinite GOP an IDR lost to an
4334                    // encode error would never self-heal.
4335                    cap.pending_force_idr = requested_idr && !frame_out;
4336                    cap.frame_counter = cap.frame_counter.wrapping_add(1);
4337                }
4338            }
4339        }
4340        if take_screenshot && let Some((_, resp)) = state.pending_screenshot.take() {
4341            if !node.frame_buffer.is_empty() {
4342                let w = width as u32;
4343                let h = height as u32;
4344                // A host software frame was written BGRA into the frame buffer, so it
4345                // needs the swap even when the local renderer is GLES.
4346                let png = if state.use_gpu && !host_cpu_frame {
4347                    crate::computer_use::encode_png_rgba(&node.frame_buffer, w, h)
4348                } else {
4349                    let mut rgba = node.frame_buffer.clone();
4350                    for px in rgba.as_chunks_mut::<4>().0 {
4351                        px.swap(0, 2);
4352                    }
4353                    crate::computer_use::encode_png_rgba(&rgba, w, h)
4354                };
4355                match png {
4356                    Ok(data) => {
4357                        let _ = resp.send(Ok(data));
4358                    }
4359                    Err(e) => {
4360                        let _ = resp.send(Err(format!("PNG encode error: {e}")));
4361                        eprintln!("[ComputerUse] PNG encode error: {}", e);
4362                    }
4363                }
4364            } else {
4365                let _ = resp.send(Err("Screenshot render produced no pixels".to_string()));
4366            }
4367        }
4368    }
4369    if let Some((id, buf)) = pool_slot.take()
4370        && let Some(cap) = node.capture.as_ref()
4371        && let Some(ref pool) = cap.encode_pool
4372    {
4373        pool.cancel(id, buf);
4374    }
4375    false
4376}
4377
4378/// True when the rectangles `(x, y, w, h)` overlap: strict interior intersection, so
4379/// touching edges do not count and empty (non-positive-dimension) rectangles never
4380/// overlap anything. Arithmetic is widened so extreme coordinates cannot wrap.
4381fn rects_overlap(a: (i32, i32, i32, i32), b: (i32, i32, i32, i32)) -> bool {
4382    let (ax, ay, aw, ah) = (a.0 as i64, a.1 as i64, a.2 as i64, a.3 as i64);
4383    let (bx, by, bw, bh) = (b.0 as i64, b.1 as i64, b.2 as i64, b.3 as i64);
4384    aw > 0
4385        && ah > 0
4386        && bw > 0
4387        && bh > 0
4388        && ax < bx + bw
4389        && bx < ax + aw
4390        && ay < by + bh
4391        && by < ay + ah
4392}
4393
4394/// An overlapping output as `(id, flavor, rect)`, where flavor names which rectangle
4395/// pair matched and rect is `(x, y, width, height)`.
4396type OutputOverlap = (u32, &'static str, (i32, i32, i32, i32));
4397
4398/// The first live screen (excluding `skip_id`; views stand behind their owner's
4399/// rectangle) whose rectangle overlaps a candidate placement. Both rectangle flavors are checked — logical
4400/// (Space layout, scale-divided) and physical (mode pixels at the same origin) — because
4401/// input injection and cursor compositing key off the physical rects while window layout
4402/// keys off the logical ones, and neither may overlap.
4403fn find_output_overlap(
4404    nodes: &[wayland::frontend::OutputNode],
4405    skip_id: Option<u32>,
4406    logical: (i32, i32, i32, i32),
4407    physical: (i32, i32, i32, i32),
4408) -> Option<OutputOverlap> {
4409    for n in nodes {
4410        if Some(n.id) == skip_id {
4411            continue;
4412        }
4413        if n.owner.is_some() {
4414            continue;
4415        }
4416        if let Some(geo) = n.logical_geometry() {
4417            let other = (geo.loc.x, geo.loc.y, geo.size.w, geo.size.h);
4418            if rects_overlap(logical, other) {
4419                return Some((n.id, "logical", other));
4420            }
4421        }
4422        if let Some(mode) = n.output.current_mode() {
4423            let other = (n.pos.0, n.pos.1, mode.size.w, mode.size.h);
4424            if rects_overlap(physical, other) {
4425                return Some((n.id, "physical", other));
4426            }
4427        }
4428    }
4429    None
4430}
4431
4432/// Create an additional output mapped into the layout at `(x, y)`. Fails (false) on a
4433/// duplicate id, non-positive geometry/scale, a rectangle overlapping a live output, or a
4434/// GPU render-target allocation failure. Only Create/Reposition placements are validated:
4435/// a capture reconfigure (StartCapture on an existing output) resizes UNVALIDATED, so
4436/// keeping a multi-step relayout overlap-free at every step is the caller's ordering
4437/// responsibility.
4438fn create_output_on(
4439    state: &mut AppState,
4440    id: u32,
4441    width: i32,
4442    height: i32,
4443    x: i32,
4444    y: i32,
4445    scale: f64,
4446) -> bool {
4447    if state.node_idx_for_id(id).is_some() || width <= 0 || height <= 0 || scale <= 0.0 {
4448        return false;
4449    }
4450    // Host-capture mode: displays map onto host outputs by rank, so an output beyond
4451    // the host's count would exist but never receive a frame. Refuse it instead.
4452    if let Some(host) = state.host.as_ref() {
4453        let capacity = host.output_count();
4454        if state.output_nodes.len() >= capacity {
4455            eprintln!(
4456                "[Wayland] CreateOutput {id}: rejected, the host compositor has {capacity} output(s) and all are backing displays."
4457            );
4458            return false;
4459        }
4460    }
4461    let logical_size = (
4462        (width as f64 / scale).round() as i32,
4463        (height as f64 / scale).round() as i32,
4464    );
4465    if let Some((oid, flavor, other)) = find_output_overlap(
4466        &state.output_nodes,
4467        None,
4468        (x, y, logical_size.0, logical_size.1),
4469        (x, y, width, height),
4470    ) {
4471        eprintln!(
4472            "[Wayland] CreateOutput {id}: rejected, {flavor} rect {}x{}+{x}+{y} overlaps output {oid} at {}x{}+{}+{}.",
4473            if flavor == "logical" {
4474                logical_size.0
4475            } else {
4476                width
4477            },
4478            if flavor == "logical" {
4479                logical_size.1
4480            } else {
4481                height
4482            },
4483            other.2,
4484            other.3,
4485            other.0,
4486            other.1,
4487        );
4488        return false;
4489    }
4490    let output = Output::new(
4491        format!("HEADLESS-{}", id + 1),
4492        PhysicalProperties {
4493            size: (width, height).into(),
4494            subpixel: Subpixel::Unknown,
4495            make: "Pixelflux".into(),
4496            model: "Virtual".into(),
4497            serial_number: format!("{:03}", id + 1),
4498        },
4499    );
4500    let mode = OutputMode {
4501        size: (width, height).into(),
4502        refresh: 60_000,
4503    };
4504    output.change_current_state(
4505        Some(mode),
4506        Some(Transform::Normal),
4507        Some(OutputScale::Fractional(scale)),
4508        Some((x, y).into()),
4509    );
4510    output.set_preferred(mode);
4511    let mut offscreen = None;
4512    if state.use_gpu {
4513        let Some(gbm) = state.gbm_device.as_mut() else {
4514            return false;
4515        };
4516        match alloc_render_target(gbm, width as u32, height as u32, GbmFormat::Argb8888) {
4517            Ok(bo) => {
4518                let dmabuf = create_dmabuf_from_bo(&bo);
4519                offscreen = Some((bo, dmabuf));
4520            }
4521            Err(e) => {
4522                eprintln!(
4523                    "[Wayland] CreateOutput {id}: GBM allocation {width}x{height} failed ({e:?})."
4524                );
4525                return false;
4526            }
4527        }
4528    }
4529    state.space.map_output(&output, (x, y));
4530    let global = output.create_global::<AppState>(&state.dh);
4531    let damage_tracker = OutputDamageTracker::from_output(&output);
4532    if let Some(host) = state.host.as_ref() {
4533        host.set_layout(id, x, y);
4534    }
4535    println!("[Wayland] Output {id} created: {width}x{height} @ ({x}, {y}) scale {scale:.2}.");
4536    state.output_nodes.push(wayland::frontend::OutputNode {
4537        id,
4538        output,
4539        global: Some(global),
4540        owner: None,
4541        view_size: (width, height),
4542        view_scale: 0.0,
4543        pos: (x, y),
4544        damage_tracker,
4545        content_tracker: None,
4546        frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
4547        offscreen_buffer: offscreen,
4548        undrawn_ticks: 0,
4549        overlay_state: OverlayState::default(),
4550        capture: None,
4551        frame_seq: 0,
4552        target_seeded: false,
4553        content_hold_until: None,
4554    });
4555    // A nested session opens one host toplevel per screen and the extras wait
4556    // parked until a display exists for them: hand the newest waiting window to
4557    // the new output. Windows stacked on an output rather than parked — anything
4558    // placed before this compositor started parking them — remain candidates.
4559    let mut counts: Vec<(u32, usize)> = Vec::new();
4560    for w in state.space.elements() {
4561        let oid = wayland::frontend::window_output_id(w);
4562        match counts.iter_mut().find(|(o, _)| *o == oid) {
4563            Some((_, c)) => *c += 1,
4564            None => counts.push((oid, 1)),
4565        }
4566    }
4567    let newest = |pred: &dyn Fn(&smithay::desktop::Window) -> bool| {
4568        state
4569            .space
4570            .elements()
4571            .filter(|w| pred(w))
4572            .max_by_key(|w| wayland::frontend::window_meta(w).map(|m| m.id).unwrap_or(0))
4573            .cloned()
4574    };
4575    let parked = |w: &smithay::desktop::Window| {
4576        wayland::frontend::window_meta(w)
4577            .map(|m| m.parked.load(std::sync::atomic::Ordering::Relaxed))
4578            .unwrap_or(false)
4579    };
4580    // A recreated output takes back the window it held before -- parked windows
4581    // carry the id of the output they came from, and the tag outlives it -- so
4582    // windows never swap displays however a bulk relayout orders its creates.
4583    let adopt = newest(&|w| parked(w) && wayland::frontend::window_output_id(w) == id)
4584        .or_else(|| newest(&parked))
4585        .or_else(|| {
4586            newest(&|w| {
4587                let oid = wayland::frontend::window_output_id(w);
4588                counts.iter().any(|(o, c)| *o == oid && *c >= 2)
4589            })
4590        });
4591    if let Some(window) = adopt {
4592        state.place_window_on_output(&window, id);
4593        crate::log::debug!(
4594            "[Wayland] Output {id}: adopted waiting window {}.",
4595            wayland::frontend::window_meta(&window)
4596                .map(|m| m.id)
4597                .unwrap_or(0)
4598        );
4599    }
4600    true
4601}
4602
4603/// Move an existing output (the primary included) to layout offset `(x, y)`. The output's
4604/// advertised position, its Space mapping, and the windows placed on it (mapped at the
4605/// output's origin — window positions are output-relative under forced fullscreen) all
4606/// follow, so absolute input injection and cursor compositing — both keyed off `node.pos` —
4607/// resolve against the new layout immediately. A destination overlapping another live
4608/// output is refused (false). As with `CreateOutput`, only the placement itself is
4609/// validated: a capture reconfigure (StartCapture on an existing output) resizes
4610/// UNVALIDATED, so keeping a multi-step relayout overlap-free at every step is the
4611/// caller's ordering responsibility.
4612fn reposition_output_on(state: &mut AppState, id: u32, x: i32, y: i32) -> bool {
4613    let Some(idx) = state.node_idx_for_id(id) else {
4614        return false;
4615    };
4616    let output = state.output_nodes[idx].output.clone();
4617    if state.output_nodes[idx].pos == (x, y) {
4618        return true;
4619    }
4620    // A view moves within the screen it is cut from: no output to remap and no
4621    // windows to carry, since the screen and everything on it stay where they are.
4622    if let Some(owner) = state.output_nodes[idx].owner {
4623        let base = state
4624            .node_idx_for_id(owner)
4625            .map(|o| state.output_nodes[o].pos)
4626            .unwrap_or((0, 0));
4627        let mode = output
4628            .current_mode()
4629            .map(|m| (m.size.w, m.size.h))
4630            .unwrap_or((0, 0));
4631        let size = state.output_nodes[idx]
4632            .capture
4633            .as_ref()
4634            .map(|c| (c.settings.width, c.settings.height))
4635            .unwrap_or(state.output_nodes[idx].view_size);
4636        if x < base.0 || y < base.1 || x - base.0 + size.0 > mode.0 || y - base.1 + size.1 > mode.1
4637        {
4638            eprintln!(
4639                "[Wayland] RepositionOutput {id}: rejected, {}x{}+{x}+{y} leaves output \
4640                 {owner}'s {}x{}.",
4641                size.0, size.1, mode.0, mode.1
4642            );
4643            return false;
4644        }
4645        state.output_nodes[idx].pos = (x, y);
4646        if let Some(cap) = state.output_nodes[idx].capture.as_mut() {
4647            cap.needs_full_render = true;
4648        }
4649        println!("[Wayland] View {id} moved to ({x}, {y}).");
4650        return true;
4651    }
4652    let logical_size = state.output_nodes[idx]
4653        .logical_geometry()
4654        .map(|g| (g.size.w, g.size.h))
4655        .unwrap_or((0, 0));
4656    let physical_size = output
4657        .current_mode()
4658        .map(|m| (m.size.w, m.size.h))
4659        .unwrap_or((0, 0));
4660    if let Some((oid, flavor, other)) = find_output_overlap(
4661        &state.output_nodes,
4662        Some(id),
4663        (x, y, logical_size.0, logical_size.1),
4664        (x, y, physical_size.0, physical_size.1),
4665    ) {
4666        eprintln!(
4667            "[Wayland] RepositionOutput {id}: rejected, {flavor} rect {}x{}+{x}+{y} overlaps output {oid} at {}x{}+{}+{}.",
4668            if flavor == "logical" {
4669                logical_size.0
4670            } else {
4671                physical_size.0
4672            },
4673            if flavor == "logical" {
4674                logical_size.1
4675            } else {
4676                physical_size.1
4677            },
4678            other.2,
4679            other.3,
4680            other.0,
4681            other.1,
4682        );
4683        return false;
4684    }
4685    let (old_x, old_y) = state.output_nodes[idx].pos;
4686    state.output_nodes[idx].pos = (x, y);
4687    // A view is placed in layout coordinates over the screen it is cut from,
4688    // so the screen's move carries every view along: one left behind would
4689    // fall outside its screen and capture nothing.
4690    for view in state
4691        .output_nodes
4692        .iter_mut()
4693        .filter(|n| n.owner == Some(id))
4694    {
4695        view.pos = (view.pos.0 + x - old_x, view.pos.1 + y - old_y);
4696        if let Some(cap) = view.capture.as_mut() {
4697            cap.needs_full_render = true;
4698        }
4699    }
4700    if let Some(host) = state.host.as_ref() {
4701        host.set_layout(id, x, y);
4702    }
4703    output.change_current_state(None, None, None, Some((x, y).into()));
4704    state.space.map_output(&output, (x, y));
4705    let windows: Vec<smithay::desktop::Window> = state
4706        .space
4707        .elements()
4708        .filter(|w| wayland::frontend::window_output_id(w) == id)
4709        .cloned()
4710        .collect();
4711    for window in &windows {
4712        state.space.map_element(window.clone(), (x, y), false);
4713    }
4714    state.refocus_pointer();
4715    if let Some(cap) = state.output_nodes[idx].capture.as_mut() {
4716        cap.needs_full_render = true;
4717    }
4718    println!("[Wayland] Output {id} repositioned to ({x}, {y}).");
4719    true
4720}
4721
4722/// Destroy a secondary output: end its capture, relocate its windows onto the primary
4723/// output, unmap it from the space, and retract its global. The primary (id 0) is refused.
4724fn destroy_output_on(state: &mut AppState, id: u32) -> bool {
4725    if id == 0 {
4726        return false;
4727    }
4728    let Some(_) = state.node_idx_for_id(id) else {
4729        return false;
4730    };
4731    stop_capture_on_display(state, id);
4732    if let Some(host) = state.host.as_ref() {
4733        host.idle_output(id);
4734    }
4735    wayland_owners().lock().unwrap().remove(&id);
4736    // Relocate while the node is still registered so output leave/enter both resolve.
4737    let windows: Vec<smithay::desktop::Window> = state
4738        .space
4739        .elements()
4740        .filter(|w| wayland::frontend::window_output_id(w) == id)
4741        .cloned()
4742        .collect();
4743    for window in &windows {
4744        // The primary keeps whichever screen it already shows: a nested session's
4745        // second screen parks again rather than covering the first, tagged with
4746        // the output it came from so recreating that output takes it back.
4747        if state.would_cover_screen(window, 0) {
4748            state.park_window(window, id);
4749        } else {
4750            state.place_window_on_output(window, 0);
4751        }
4752    }
4753    for w in &state.pending_windows {
4754        if let Some(meta) = wayland::frontend::window_meta(w)
4755            && meta.output.load(Ordering::Relaxed) == id
4756        {
4757            meta.output.store(0, Ordering::Relaxed);
4758        }
4759    }
4760    // A view holds no screen of its own, so only the node that published the
4761    // output unmaps it; destroying that node takes its views with it, captures
4762    // and encoders included.
4763    for view in state.view_ids_of(id) {
4764        stop_capture_on_display(state, view);
4765        wayland_owners().lock().unwrap().remove(&view);
4766        if let Some(vidx) = state.node_idx_for_id(view) {
4767            state.output_nodes.remove(vidx);
4768        }
4769    }
4770    let idx = state.node_idx_for_id(id).unwrap();
4771    let node = state.output_nodes.remove(idx);
4772    if let Some(global) = node.global {
4773        state.space.unmap_output(&node.output);
4774        state.dh.remove_global::<AppState>(global);
4775    }
4776    println!(
4777        "[Wayland] Output {id} destroyed; {} window(s) relocated to primary.",
4778        windows.len()
4779    );
4780    true
4781}
4782/// Resize a screen's output, keeping its position, so the displays cut out of it can be
4783/// laid out over a different desk.
4784///
4785/// Refused for a view (it has no output of its own) and for a size that would leave one of
4786/// the screen's views outside it -- the caller grows the screen before moving views onto
4787/// the new area and shrinks it after taking them off, so no view is ever orphaned.
4788/// Send every non-parked window on `display_id`'s output a fullscreen configure
4789/// for the new mode, with the scale each surface should render at. Without this
4790/// a live session keeps drawing at the old size under the resized screen.
4791fn configure_windows_for_mode(
4792    state: &AppState,
4793    display_id: u32,
4794    output: &Output,
4795    width: i32,
4796    height: i32,
4797    scale: f64,
4798) {
4799    let scale = scale.max(0.1);
4800    let logical_width = (width as f64 / scale).round() as i32;
4801    let logical_height = (height as f64 / scale).round() as i32;
4802    for window in state.space.elements() {
4803        if wayland::frontend::window_output_id(window) != display_id {
4804            continue;
4805        }
4806        // A parked screen is tagged for this display but composited on none: it
4807        // holds PARKED_LOGICAL_SIZE until `create_output` gives it one. Resizing it
4808        // here would double the session's coordinate space onto a screen nobody
4809        // watches, which is where a window that centers itself then lands.
4810        if wayland::frontend::window_meta(window)
4811            .is_some_and(|meta| meta.parked.load(Ordering::Relaxed))
4812        {
4813            continue;
4814        }
4815        if let Some(surface) = window.wl_surface() {
4816            output.enter(&surface);
4817            with_states(&surface, |states| {
4818                smithay::wayland::compositor::send_surface_state(
4819                    &surface,
4820                    states,
4821                    scale.ceil() as i32,
4822                    Transform::Normal,
4823                );
4824                smithay::wayland::fractional_scale::with_fractional_scale(states, |fs| {
4825                    fs.set_preferred_scale(scale);
4826                });
4827            });
4828        }
4829        if let Some(toplevel) = window.toplevel() {
4830            toplevel.with_pending_state(|state| {
4831                use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State;
4832                state.states.set(State::Fullscreen);
4833                state.states.set(State::Activated);
4834                state.size = Some((logical_width, logical_height).into());
4835            });
4836            toplevel.send_configure();
4837        }
4838    }
4839}
4840
4841fn resize_output_on(state: &mut AppState, id: u32, width: i32, height: i32, scale: f64) -> bool {
4842    if width <= 0 || height <= 0 || scale <= 0.0 {
4843        return false;
4844    }
4845    let Some(idx) = state.node_idx_for_id(id) else {
4846        return false;
4847    };
4848    if state.output_nodes[idx].owner.is_some() {
4849        eprintln!("[Wayland] ResizeOutput {id}: rejected, {id} is a view of another output.");
4850        return false;
4851    }
4852    let base = state.output_nodes[idx].pos;
4853    // Only a view that is showing something can be orphaned by the resize; one
4854    // without a capture is placed by whoever starts it, against the new size.
4855    for view in state
4856        .output_nodes
4857        .iter()
4858        .filter(|n| n.owner == Some(id) && n.capture.is_some())
4859    {
4860        let (vw, vh) = view
4861            .capture
4862            .as_ref()
4863            .map(|c| (c.settings.width, c.settings.height))
4864            .unwrap_or(view.view_size);
4865        if view.pos.0 - base.0 + vw > width || view.pos.1 - base.1 + vh > height {
4866            eprintln!(
4867                "[Wayland] ResizeOutput {id}: rejected, view {} would fall outside {width}x{height}.",
4868                view.id
4869            );
4870            return false;
4871        }
4872    }
4873    let output = state.output_nodes[idx].output.clone();
4874    let refresh = output.current_mode().map(|m| m.refresh).unwrap_or(60_000);
4875    if output.current_mode().map(|m| (m.size.w, m.size.h)) == Some((width, height))
4876        && (output.current_scale().fractional_scale() - scale).abs() <= 0.001
4877    {
4878        return true;
4879    }
4880    // The GPU target is allocated before anything is committed: a driver that refuses the
4881    // new dimensions leaves the previous mode and buffers live rather than a half-resized
4882    // output.
4883    let mut new_offscreen = None;
4884    if state.use_gpu {
4885        let Some(gbm) = state.gbm_device.as_mut() else {
4886            return false;
4887        };
4888        match alloc_render_target(gbm, width as u32, height as u32, GbmFormat::Argb8888) {
4889            Ok(bo) => {
4890                let dmabuf = create_dmabuf_from_bo(&bo);
4891                new_offscreen = Some((bo, dmabuf));
4892            }
4893            Err(e) => {
4894                eprintln!(
4895                    "[Wayland] ResizeOutput {id}: GBM allocation {width}x{height} failed ({e:?})."
4896                );
4897                return false;
4898            }
4899        }
4900    }
4901    let mode = OutputMode {
4902        size: (width, height).into(),
4903        refresh,
4904    };
4905    output.change_current_state(
4906        Some(mode),
4907        Some(Transform::Normal),
4908        Some(OutputScale::Fractional(scale)),
4909        Some(Point::from(base)),
4910    );
4911    output.set_preferred(mode);
4912    state.space.map_output(&output, base);
4913    // Capture clients allocate to the announced size; a stale one fails validation on
4914    // every frame they submit from here on.
4915    for cs in state
4916        .copy_sessions
4917        .iter()
4918        .filter(|cs| cs.output.upgrade().as_ref() == Some(&output))
4919    {
4920        if let Some(c) = wayland::frontend::output_capture_constraints(
4921            &output,
4922            state.gles_renderer.as_ref(),
4923            &state.render_node_path,
4924        ) {
4925            cs.session.update_constraints(c);
4926        }
4927    }
4928    let node = &mut state.output_nodes[idx];
4929    node.frame_buffer = vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4];
4930    node.view_size = (width, height);
4931    node.target_seeded = false;
4932    if let Some(off) = new_offscreen.take() {
4933        node.offscreen_buffer = Some(off);
4934    }
4935    for n in state
4936        .output_nodes
4937        .iter_mut()
4938        .filter(|n| n.id == id || n.owner == Some(id))
4939    {
4940        if let Some(cap) = n.capture.as_mut() {
4941            cap.needs_full_render = true;
4942        }
4943    }
4944    configure_windows_for_mode(state, id, &output, width, height, scale);
4945    println!("[Wayland] Output {id} resized to {width}x{height} scale {scale:.2}.");
4946    true
4947}
4948
4949/// The view every screen carries from the start, covering it: the display a
4950/// consumer gets without asking for a layout at all.
4951const DEFAULT_VIEW_ID: u32 = 1;
4952
4953/// Add a view: a display over a rectangle of `owner`'s output.
4954///
4955/// A view renders and encodes on its own -- its own damage tracker, frame buffer, and
4956/// capture -- but publishes no `wl_output`, so the session sees one screen however many
4957/// displays the client is shown. That is what a window drag needs: the pointer grab it
4958/// runs under stays inside one surface across the whole desk, where separate screens
4959/// stop it at the edge of the one it began on.
4960fn create_view_on(
4961    state: &mut AppState,
4962    id: u32,
4963    owner: u32,
4964    x: i32,
4965    y: i32,
4966    width: i32,
4967    height: i32,
4968) -> bool {
4969    if state.node_idx_for_id(id).is_some() || width <= 0 || height <= 0 || x < 0 || y < 0 {
4970        return false;
4971    }
4972    let Some(oidx) = state.node_idx_for_id(owner) else {
4973        return false;
4974    };
4975    if state.output_nodes[oidx].owner.is_some() {
4976        eprintln!("[Wayland] CreateView {id}: rejected, output {owner} is itself a view.");
4977        return false;
4978    }
4979    let output = state.output_nodes[oidx].output.clone();
4980    let scale = output.current_scale().fractional_scale();
4981    let Some(mode) = output.current_mode() else {
4982        return false;
4983    };
4984    if x + width > mode.size.w || y + height > mode.size.h {
4985        eprintln!(
4986            "[Wayland] CreateView {id}: rejected, {width}x{height}+{x}+{y} leaves output \
4987             {owner}'s {}x{}.",
4988            mode.size.w, mode.size.h
4989        );
4990        return false;
4991    }
4992    let mut offscreen = None;
4993    if state.use_gpu {
4994        let Some(gbm) = state.gbm_device.as_mut() else {
4995            return false;
4996        };
4997        match alloc_render_target(gbm, width as u32, height as u32, GbmFormat::Argb8888) {
4998            Ok(bo) => {
4999                let dmabuf = create_dmabuf_from_bo(&bo);
5000                offscreen = Some((bo, dmabuf));
5001            }
5002            Err(e) => {
5003                eprintln!(
5004                    "[Wayland] CreateView {id}: GBM allocation {width}x{height} failed ({e:?})."
5005                );
5006                return false;
5007            }
5008        }
5009    }
5010    let pos = state.output_nodes[oidx].pos;
5011    let origin = (pos.0 + x, pos.1 + y);
5012    // Static rather than tied to the output: the tracker follows the view's rectangle,
5013    // not the whole screen the view is cut from.
5014    let damage_tracker = OutputDamageTracker::new((width, height), scale, Transform::Normal);
5015    println!("[Wayland] View {id} created over output {owner}: {width}x{height} @ ({x}, {y}).");
5016    state.output_nodes.push(wayland::frontend::OutputNode {
5017        id,
5018        output,
5019        global: None,
5020        owner: Some(owner),
5021        view_size: (width, height),
5022        view_scale: 0.0,
5023        pos: origin,
5024        damage_tracker,
5025        content_tracker: None,
5026        frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
5027        offscreen_buffer: offscreen,
5028        undrawn_ticks: 0,
5029        overlay_state: OverlayState::default(),
5030        capture: None,
5031        frame_seq: 0,
5032        target_seeded: false,
5033        content_hold_until: None,
5034    });
5035    true
5036}
5037
5038/// Startup inputs for the compositor thread: its two calloop channels, the sender it hands
5039/// to computer-use, the initial geometry, and the GPU / cursor policy.
5040struct WaylandThreadConfig {
5041    command_rx: smithay::reexports::calloop::channel::Channel<ThreadCommand>,
5042    wake_rx: smithay::reexports::calloop::channel::Channel<()>,
5043    command_tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
5044    initial_width: i32,
5045    initial_height: i32,
5046    explicit_dri_node: String,
5047    auto_gpu_selected: bool,
5048    cursor_size: i32,
5049    pointer_share: Arc<crate::wayland::frontend::PointerShare>,
5050}
5051
5052/// Bring the hardware renderer up on a DRM render node: the GBM device, the EGL display and
5053/// context it backs, the GLES renderer, and the allocator the render targets come from.
5054///
5055/// The compositor and [`probe_wayland_gpu`] share this so the probe's answer is the
5056/// compositor's own. Errors name the step that failed, which is all a caller can act on.
5057fn gpu_render_init(
5058    device_path: &std::path::Path,
5059) -> Result<(RawGbmDevice<File>, GlesRenderer), String> {
5060    let file = File::options()
5061        .read(true)
5062        .write(true)
5063        .open(device_path)
5064        .map_err(|e| format!("Failed to open render device: {}", e))?;
5065    let file_for_alloc = file
5066        .try_clone()
5067        .map_err(|e| format!("Failed to clone file for GBM Allocator: {}", e))?;
5068    let gbm_allocator =
5069        RawGbmDevice::new(file_for_alloc).map_err(|_| "Failed to create Raw GBM Device")?;
5070    let gbm = GbmDevice::new(file).map_err(|_| "Failed to create GBM device")?;
5071    let egl = unsafe { EGLDisplay::new(gbm) }.map_err(|_| "Failed to create EGL display")?;
5072    let context = EGLContext::new(&egl).map_err(|_| "Failed to create EGL context")?;
5073    let renderer =
5074        unsafe { GlesRenderer::new(context) }.map_err(|_| "Failed to init GlesRenderer")?;
5075    Ok((gbm_allocator, renderer))
5076}
5077
5078/// `GL_RENDERER` of a live renderer — the driver that actually answered, which is the only
5079/// thing that separates a GPU from Mesa's software fallback on the same node. Empty when the
5080/// context cannot be made current or the string is unavailable.
5081fn gl_renderer_name(renderer: &mut GlesRenderer) -> String {
5082    renderer
5083        .with_context(|gl| unsafe {
5084            let ptr = gl.GetString(smithay::backend::renderer::gles::ffi::RENDERER);
5085            if ptr.is_null() {
5086                String::new()
5087            } else {
5088                std::ffi::CStr::from_ptr(ptr as *const std::ffi::c_char)
5089                    .to_string_lossy()
5090                    .into_owned()
5091            }
5092        })
5093        .unwrap_or_default()
5094}
5095
5096/// The name the GL driver gives the GPU behind render node `node_index`, asked once per node
5097/// and remembered: what a VA-API session, which names no device, is reported to run on.
5098/// Empty where no GL context comes up on the node.
5099fn node_gpu_name(node_index: i32) -> String {
5100    static NAMES: OnceLock<Mutex<std::collections::HashMap<i32, String>>> = OnceLock::new();
5101    let mut names = NAMES.get_or_init(Default::default).lock().unwrap();
5102    names
5103        .entry(node_index)
5104        .or_insert_with(|| {
5105            let path = format!("/dev/dri/renderD{}", 128 + node_index);
5106            gpu_render_init(std::path::Path::new(&path))
5107                .map(|(_gbm, mut renderer)| gl_renderer_name(&mut renderer))
5108                .unwrap_or_default()
5109        })
5110        .clone()
5111}
5112
5113/// A GPU is exposed to this container or machine.
5114///
5115/// Only device nodes count. `/sys/class/drm` is the host's and lists cards a container may
5116/// have no access to, while `/dev` is the container's own; the NVIDIA character devices
5117/// answer for a driver stack that was given no DRM node at all.
5118fn gpu_exposed() -> bool {
5119    std::path::Path::new("/dev/nvidiactl").exists()
5120        || std::fs::read_dir("/dev/dri")
5121            .into_iter()
5122            .flatten()
5123            .flatten()
5124            .any(|e| {
5125                let name = e.file_name();
5126                let name = name.to_string_lossy();
5127                name.starts_with("renderD") || name.starts_with("card")
5128            })
5129}
5130
5131/// The main execution loop of the Wayland backend.
5132///
5133/// This function is the central nervous system of the backend. It runs on its own thread and owns
5134/// the entire lifecycle of the headless Wayland compositor:
5135///
5136/// 1. **Initialization**: builds the `calloop` event loop and the Wayland display, raises
5137///    libwayland's per-client buffer limit when the newer setter is available (resolved at runtime
5138///    so the module still loads against older libwayland), and brings up the rendering pipeline —
5139///    GBM/EGL hardware acceleration on the resolved DRM render node, falling back to software
5140///    rendering (Pixman) when no node is usable.
5141/// 2. **State management**: constructs and holds the `AppState` — the Wayland globals (compositor,
5142///    seat, SHM, shell, dmabuf, selections, and the rest) plus the output registry: the primary
5143///    virtual `HEADLESS-1` output at layout (0, 0), extended at runtime by CreateOutput with
5144///    additional outputs at their layout offsets, each `OutputNode` owning its damage tracker,
5145///    render targets, and (at most one) capture pipeline.
5146/// 3. **Event dispatch**:
5147///    - **Command channel**: control messages from the Python thread — per-display start/stop,
5148///      output lifecycle (create/destroy/list/move-window), input injection routed across the
5149///      output layout, keymap and clipboard operations, live rate / tunable changes, and the
5150///      computer-use queries.
5151///    - **Wayland socket**: accepts client connections and drives the compositor protocol.
5152/// 4. **StartCapture reconfigure** (per display): reprograms that output's mode / scale / refresh,
5153///    resizes its framebuffer and offscreen GBM buffer, and fullscreens the toplevels placed on
5154///    it. The encode device is resolved here: an operator's explicit `encode_node_index`
5155///    (-1 software, >= 0 a device) always wins, and only the unset `-2` sentinel is filled: from
5156///    the auto-picked render node, else the first one present. H.264 output masks the dimensions
5157///    even, because 4:2:0 needs even width and height.
5158/// 5. **Encode-path choice + render loop**: only a same-GPU GLES session encodes zero-copy on this
5159///    calloop thread, because the dmabuf and its EGL context are calloop-affine; every readback
5160///    flavor (striped software H.264/JPEG, Pixman, or a cross-GPU hardware encoder) builds its
5161///    encoders on that display's dedicated encode thread instead. A shared timer (paced at the
5162///    fastest active capture) renders each capturing output — its windows, popups, and layers made
5163///    output-local, the cursor only on the pointer's output — applies the shared paint-over /
5164///    recovery-IDR policy per display, and delivers each display's encoded stripes through its own
5165///    frame callback. The zero-copy encode waits the GL render fence first, so a hardware encoder
5166///    reading the dmabuf through CUDA/VA never maps a half-rasterized (torn) frame.
5167/// 6. **Thread lifecycle**: the Python frame callback runs on a dedicated delivery thread so its
5168///    GIL never stalls calloop input / control dispatch, and in readback mode the encoders run on
5169///    the `wl-encode` thread. On a restart or stop the encode thread is torn down before the
5170///    delivery thread — it feeds the delivery sender and must be gone first — and the retained
5171///    callbacks are dropped and gated by a process-shutdown flag so nothing fires into a finalizing
5172///    interpreter.
5173fn run_wayland_thread(cfg: WaylandThreadConfig) {
5174    let WaylandThreadConfig {
5175        command_rx,
5176        wake_rx,
5177        command_tx,
5178        initial_width,
5179        initial_height,
5180        explicit_dri_node,
5181        auto_gpu_selected,
5182        cursor_size,
5183        pointer_share,
5184    } = cfg;
5185    let width: i32 = if initial_width > 0 {
5186        initial_width
5187    } else {
5188        1024
5189    };
5190    let height: i32 = if initial_height > 0 {
5191        initial_height
5192    } else {
5193        768
5194    };
5195
5196    let mut event_loop = match EventLoop::<AppState>::try_new() {
5197        Ok(l) => l,
5198        Err(e) => {
5199            eprintln!("[Wayland] compositor thread aborting: event loop init failed: {e}");
5200            return;
5201        }
5202    };
5203    let display: Display<AppState> = match Display::new() {
5204        Ok(d) => d,
5205        Err(e) => {
5206            eprintln!("[Wayland] compositor thread aborting: display init failed: {e}");
5207            return;
5208        }
5209    };
5210    let dh: DisplayHandle = display.handle();
5211    unsafe {
5212        if let Ok(lib) = libloading::Library::new("libwayland-server.so.0") {
5213            if let Ok(set_max) = lib.get::<unsafe extern "C" fn(*mut std::ffi::c_void, usize)>(
5214                b"wl_display_set_default_max_buffer_size\0",
5215            ) {
5216                set_max(
5217                    dh.backend_handle().display_ptr() as *mut std::ffi::c_void,
5218                    10 * 1024 * 1024,
5219                );
5220            }
5221            std::mem::forget(lib);
5222        }
5223    }
5224
5225    let dri_node = explicit_dri_node;
5226
5227    let mut use_gpu = !dri_node.is_empty();
5228    let render_node_path = dri_node.clone();
5229
5230    let mut gles_renderer = None;
5231    let mut pixman_renderer = None;
5232    let mut offscreen_buffer: Option<(BufferObject<()>, Dmabuf)> = None;
5233    let mut dmabuf_global = None;
5234    let mut gbm_device_raw = None;
5235    let mut dmabuf_state = DmabufState::new();
5236
5237    let mut gpu_success = false;
5238    if use_gpu {
5239        println!("[Wayland] Renderer: GL on {dri_node}.");
5240        let init_res: Result<(), String> = (|| {
5241            let device_path = std::path::Path::new(&dri_node);
5242            let (gbm_allocator, mut renderer) = gpu_render_init(device_path)?;
5243
5244            if let Err(e) = renderer.bind_wl_display(&dh) {
5245                crate::log::debug!(
5246                    "[Wayland] EGL did not bind to the Wayland display (optional): {:?}",
5247                    e
5248                );
5249            }
5250
5251            let formats = Bind::<Dmabuf>::supported_formats(&renderer)
5252                .ok_or("Failed to query formats")?
5253                .into_iter()
5254                .collect::<Vec<_>>();
5255
5256            let node = DrmNode::from_path(device_path).map_err(|_| "Failed to create DrmNode")?;
5257            let dmabuf_default_feedback =
5258                DmabufFeedbackBuilder::new(node.dev_id(), formats.clone()).build();
5259
5260            dmabuf_global = Some(if let Ok(default_feedback) = dmabuf_default_feedback {
5261                dmabuf_state.create_global_with_default_feedback::<AppState>(&dh, &default_feedback)
5262            } else {
5263                dmabuf_state.create_global::<AppState>(&dh, formats)
5264            });
5265
5266            let bo = alloc_render_target(
5267                &gbm_allocator,
5268                width as u32,
5269                height as u32,
5270                GbmFormat::Argb8888,
5271            )
5272            .map_err(|_| "Failed to allocate GBM buffer")?;
5273
5274            let dmabuf = create_dmabuf_from_bo(&bo);
5275            offscreen_buffer = Some((bo, dmabuf));
5276            gbm_device_raw = Some(gbm_allocator);
5277            gles_renderer = Some(renderer);
5278            Ok(())
5279        })();
5280
5281        match init_res {
5282            Ok(_) => {
5283                gpu_success = true;
5284                report::set_renderer(report::Renderer {
5285                    kind: "gl",
5286                    node: dri_node.clone(),
5287                    gpu: gles_renderer
5288                        .as_mut()
5289                        .map(gl_renderer_name)
5290                        .unwrap_or_default(),
5291                    reason: String::new(),
5292                });
5293            }
5294            Err(e) => {
5295                eprintln!(
5296                    "[Wayland] GPU renderer failed to initialize ({e}); rendering in software (Pixman)."
5297                );
5298                report::set_renderer(report::Renderer {
5299                    kind: "pixman",
5300                    node: String::new(),
5301                    gpu: String::new(),
5302                    reason: format!("the GPU renderer did not initialize on {dri_node}: {e}"),
5303                });
5304                use_gpu = false;
5305            }
5306        }
5307    }
5308
5309    if !gpu_success {
5310        if dri_node.is_empty() {
5311            println!("[Wayland] Renderer: software (Pixman), no render node.");
5312            report::set_renderer(report::Renderer {
5313                kind: "pixman",
5314                node: String::new(),
5315                gpu: String::new(),
5316                reason: "no render node".to_string(),
5317            });
5318        }
5319        pixman_renderer = Some(PixmanRenderer::new().expect("Failed to init PixmanRenderer"));
5320        use_gpu = false;
5321    }
5322
5323    let compositor_state = CompositorState::new_v6::<AppState>(&dh);
5324    let image_capture_source_state = ImageCaptureSourceState::new();
5325    let output_capture_source_state = OutputCaptureSourceState::new::<AppState>(&dh);
5326    let image_copy_capture_state = ImageCopyCaptureState::new::<AppState>(&dh);
5327    let fractional_scale_state = FractionalScaleManagerState::new::<AppState>(&dh);
5328    let shm_state = ShmState::new::<AppState>(&dh, vec![]);
5329    let output_state = OutputManagerState::new_with_xdg_output::<AppState>(&dh);
5330    let mut seat_state = SeatState::new();
5331    let shell_state = XdgShellState::new::<AppState>(&dh);
5332    let space = Space::default();
5333    let layer_shell_state = WlrLayerShellState::new::<AppState>(&dh);
5334    let data_device_state = DataDeviceState::new::<AppState>(&dh);
5335    let data_control_state = DataControlState::new::<AppState, _>(&dh, None, |_| true);
5336    let ext_data_control_state = ExtDataControlState::new::<AppState, _>(&dh, None, |_| true);
5337    let cursor_shape_state = CursorShapeManagerState::new::<AppState>(&dh);
5338    let _vk_global = dh.create_global::<AppState, ZwpVirtualKeyboardManagerV1, _>(1, ());
5339    let pointer_warp_state = PointerWarpManager::new::<AppState>(&dh);
5340    let relative_pointer_state = RelativePointerManagerState::new::<AppState>(&dh);
5341    let pointer_constraints_state = PointerConstraintsState::new::<AppState>(&dh);
5342
5343    let foreign_toplevel_list = ForeignToplevelListState::new::<AppState>(&dh);
5344    let xdg_decoration_state = XdgDecorationState::new::<AppState>(&dh);
5345    let single_pixel_buffer = SinglePixelBufferState::new::<AppState>(&dh);
5346    let viewporter_state = ViewporterState::new::<AppState>(&dh);
5347    let presentation_state = PresentationState::new::<AppState>(&dh, 1);
5348    let xdg_activation_state = XdgActivationState::new::<AppState>(&dh);
5349    let primary_selection_state = PrimarySelectionState::new::<AppState>(&dh);
5350    let popups = PopupManager::default();
5351
5352    let mut seat = seat_state.new_wl_seat(&dh, "seat0");
5353    seat.add_keyboard(XkbConfig::default(), 200, 25)
5354        .expect("Failed to init keyboard");
5355    seat.add_pointer();
5356
5357    let (host_frame_tx, host_frame_rx) = smithay::reexports::calloop::channel::channel::<usize>();
5358
5359    let mut state = AppState {
5360        compositor_state,
5361        fractional_scale_state,
5362        viewporter_state,
5363        presentation_state,
5364        shm_state,
5365        single_pixel_buffer,
5366        dmabuf_state,
5367        dmabuf_global,
5368        ext_data_control_state,
5369        cursor_shape_state,
5370        image_capture_source_state,
5371        output_capture_source_state,
5372        image_copy_capture_state,
5373        copy_sessions: Vec::new(),
5374        output_state,
5375        seat_state,
5376        shell_state,
5377        layer_shell_state,
5378        space,
5379        data_device_state,
5380        data_control_state,
5381        dh: dh.clone(),
5382        seat,
5383        pointer_warp_state,
5384        relative_pointer_state,
5385        pointer_constraints_state,
5386        output_nodes: Vec::new(),
5387        pending_windows: Vec::new(),
5388        foreign_toplevel_list,
5389        xdg_decoration_state,
5390        xdg_activation_state,
5391        primary_selection_state,
5392        popups,
5393        gles_renderer,
5394        pixman_renderer,
5395        gbm_device: gbm_device_raw,
5396        settings: RustCaptureSettings {
5397            width,
5398            height,
5399            ..RustCaptureSettings::default()
5400        },
5401        cursor_callback_set: false,
5402        cursor_tx: wayland::cursor::spawn_cursor_worker(
5403            cursor_size,
5404            RustCaptureSettings::default().cursor_size_cap,
5405        ),
5406        clipboard_callback: None,
5407        pending_clipboard_read: None,
5408        clipboard_generation: Arc::new(std::sync::atomic::AtomicU64::new(0)),
5409        current_selection_mimes: Vec::new(),
5410        last_log_time: Instant::now(),
5411        start_time: Instant::now(),
5412        clock: Clock::new(),
5413        use_gpu,
5414        cursor_helper: Cursor::load(cursor_size),
5415        keymap_policy: wayland::keymap::KeymapPolicy::seat(),
5416        host: None,
5417        host_layout_pending: std::collections::HashMap::new(),
5418        host_frame_tx,
5419        host_mode_refusals: std::collections::HashMap::new(),
5420        current_cursor_icon: None,
5421        cursor_surface_pending: false,
5422        cursor_buffer: None,
5423        render_cursor_on_framebuffer: false,
5424        render_node_path,
5425        auto_gpu_selected,
5426        pending_screenshot: None,
5427        command_rx: None,
5428        last_input_at: None,
5429        pointer_motion_at: None,
5430        pointer_share,
5431        motion_applied: 0,
5432        input_interval: None,
5433        frame_idle_long: false,
5434        last_idle_service_at: None,
5435        deliver_reaper: Vec::new(),
5436        encode_reaper: Vec::new(),
5437    };
5438    // Seed the keymap policy with the seat's initial keymap so overlay binds splice onto
5439    // the exact text clients received.
5440    {
5441        let initial_keymap = if let Some(kb) = state.seat.get_keyboard() {
5442            kb.with_xkb_state(&mut state, |context| match context.xkb().lock() {
5443                Ok(guard) => {
5444                    let keymap = unsafe { guard.keymap() };
5445                    keymap.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
5446                }
5447                Err(_) => String::new(),
5448            })
5449        } else {
5450            String::new()
5451        };
5452        state.keymap_policy.rebuild_base(initial_keymap);
5453    }
5454
5455    let output = Output::new(
5456        "HEADLESS-1".into(),
5457        PhysicalProperties {
5458            size: (width, height).into(),
5459            subpixel: Subpixel::Unknown,
5460            make: "Pixelflux".into(),
5461            model: "Virtual".into(),
5462            serial_number: "001".into(),
5463        },
5464    );
5465    output.change_current_state(
5466        Some(OutputMode {
5467            size: (width, height).into(),
5468            refresh: 60_000,
5469        }),
5470        Some(Transform::Normal),
5471        Some(OutputScale::Fractional(1.0)),
5472        Some((0, 0).into()),
5473    );
5474    output.set_preferred(OutputMode {
5475        size: (width, height).into(),
5476        refresh: 60_000,
5477    });
5478    state.space.map_output(&output, (0, 0));
5479    let global = output.create_global::<AppState>(&dh);
5480    let damage_tracker = OutputDamageTracker::from_output(&output);
5481    state.output_nodes.push(wayland::frontend::OutputNode {
5482        id: 0,
5483        output,
5484        global: Some(global),
5485        owner: None,
5486        view_size: (width, height),
5487        view_scale: 0.0,
5488        pos: (0, 0),
5489        damage_tracker,
5490        content_tracker: None,
5491        frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
5492        offscreen_buffer,
5493        undrawn_ticks: 0,
5494        overlay_state: OverlayState::default(),
5495        capture: None,
5496        frame_seq: 0,
5497        target_seeded: false,
5498        content_hold_until: None,
5499    });
5500    // The screen always carries one view covering it, so a consumer that asks for
5501    // nothing but a single display finds one there: the screen itself is never
5502    // captured, only the views cut from it are.
5503    create_view_on(&mut state, DEFAULT_VIEW_ID, 0, 0, 0, width, height);
5504
5505    /// Apply every queued control command in FIFO order, and say whether any was input. Sends
5506    /// wake the loop through the
5507    /// separate wake channel, and the render tick ALSO drains before starting its work, so
5508    /// queued input is applied ahead of a long render/encode instead of waiting it out.
5509    /// Fold one pointer move into the smoothed spacing between moves, the cadence the client
5510    /// sends its motion at; a gap of more than a few frame periods is a pause and is clamped so
5511    /// the next moves read as paced again within a couple of them.
5512    fn note_pointer_motion(state: &mut AppState) {
5513        let now = Instant::now();
5514        if let Some(prev) = state.pointer_motion_at {
5515            let cap = frame_period(state.settings.target_fps) * 4;
5516            let dt = now.saturating_duration_since(prev).min(cap);
5517            state.input_interval = Some(match state.input_interval {
5518                None => dt,
5519                Some(avg) => avg.mul_f64(0.875) + dt.mul_f64(0.125),
5520            });
5521        }
5522        state.pointer_motion_at = Some(now);
5523    }
5524
5525    fn drain_thread_commands(state: &mut AppState) -> bool {
5526        let Some(rx) = state.command_rx.take() else {
5527            return false;
5528        };
5529        let mut had_input = false;
5530        while let Ok(cmd) = rx.try_recv() {
5531            had_input |= matches!(
5532                cmd,
5533                ThreadCommand::KeyboardKey { .. }
5534                    | ThreadCommand::KeyboardKeys { .. }
5535                    | ThreadCommand::PointerMotion { .. }
5536                    | ThreadCommand::PointerRelativeMotion { .. }
5537                    | ThreadCommand::PointerButton { .. }
5538                    | ThreadCommand::PointerAxis { .. }
5539                    | ThreadCommand::PointerScroll { .. }
5540                    | ThreadCommand::PointerScrollEnd
5541            );
5542            if matches!(
5543                cmd,
5544                ThreadCommand::PointerMotion { .. } | ThreadCommand::PointerRelativeMotion { .. }
5545            ) {
5546                note_pointer_motion(state);
5547            }
5548            handle_thread_command(state, cmd);
5549        }
5550        if had_input {
5551            state.last_input_at = Some(Instant::now());
5552        }
5553        state.command_rx = Some(rx);
5554        had_input
5555    }
5556
5557    /// One idle-tick service pass for committed clients while nothing captures:
5558    /// frame callbacks unblock vsynced clients, requested presentation feedback
5559    /// is discarded (nothing presents), and the renderer's import cache is
5560    /// pruned since no render will do it. Shared by the frame timer's idle
5561    /// branch and the input wake handler, which must not let a fresh keypress
5562    /// wait out an already-armed long idle deadline.
5563    fn send_idle_frame_callbacks(state: &mut AppState) {
5564        let time = state.clock.now();
5565        for node in state.output_nodes.iter().filter(|n| n.owner.is_none()) {
5566            let mut feedback = OutputPresentationFeedback::new(&node.output);
5567            for window in state.space.elements_for_output(&node.output) {
5568                window.send_frame(&node.output, time, Some(Duration::ZERO), |_, _| {
5569                    Some(node.output.clone())
5570                });
5571                window.take_presentation_feedback(
5572                    &mut feedback,
5573                    |_, _| Some(node.output.clone()),
5574                    |_, _| wp_presentation_feedback::Kind::empty(),
5575                );
5576            }
5577            for layer in layer_map_for_output(&node.output).layers() {
5578                layer.send_frame(&node.output, time, Some(Duration::ZERO), |_, _| {
5579                    Some(node.output.clone())
5580                });
5581                layer.take_presentation_feedback(
5582                    &mut feedback,
5583                    |_, _| Some(node.output.clone()),
5584                    |_, _| wp_presentation_feedback::Kind::empty(),
5585                );
5586            }
5587            feedback.discarded();
5588        }
5589        if let Some(output) = state.primary_output() {
5590            send_cursor_frame(state, output, time);
5591        }
5592        if let Some(renderer) = state.gles_renderer.as_mut() {
5593            let _ = renderer.cleanup_texture_cache();
5594        }
5595    }
5596
5597    fn handle_thread_command(state: &mut AppState, cmd: ThreadCommand) {
5598        match cmd {
5599            ThreadCommand::StartCapture {
5600                display_id,
5601                callback,
5602                settings,
5603            } => {
5604                start_capture_on_display(state, display_id, callback.map(Arc::new), settings);
5605            }
5606            ThreadCommand::StopCapture { display_id } => {
5607                // Cursor and clipboard callbacks deliberately SURVIVE StopCapture:
5608                // captures cycle on client disconnects and setting restarts, and a
5609                // copy or cursor change during that gap must still reach Python.
5610                // PY_SHUTDOWN gates every use against a finalizing interpreter.
5611                stop_capture_on_display(state, display_id);
5612            }
5613            ThreadCommand::CreateOutput {
5614                id,
5615                width,
5616                height,
5617                x,
5618                y,
5619                scale,
5620                reply,
5621            } => {
5622                let _ = reply.send(create_output_on(state, id, width, height, x, y, scale));
5623            }
5624            ThreadCommand::CreateView {
5625                id,
5626                owner,
5627                x,
5628                y,
5629                width,
5630                height,
5631                reply,
5632            } => {
5633                let _ = reply.send(create_view_on(state, id, owner, x, y, width, height));
5634            }
5635            ThreadCommand::ResizeOutput {
5636                id,
5637                width,
5638                height,
5639                scale,
5640                reply,
5641            } => {
5642                let _ = reply.send(resize_output_on(state, id, width, height, scale));
5643            }
5644            ThreadCommand::DestroyOutput { id, reply } => {
5645                let _ = reply.send(destroy_output_on(state, id));
5646            }
5647            ThreadCommand::OutputCapacity { reply } => {
5648                let _ = reply.send(state.host.as_ref().map_or(-1, |h| h.output_count() as i64));
5649            }
5650            ThreadCommand::RepositionOutput { id, x, y, reply } => {
5651                let _ = reply.send(reposition_output_on(state, id, x, y));
5652            }
5653            ThreadCommand::ListOutputs { reply } => {
5654                let list = state
5655                    .output_nodes
5656                    .iter()
5657                    .map(|n| {
5658                        // A view's size is its own rectangle, not the screen it is
5659                        // cut from, which is the size a caller lays displays out by.
5660                        let (w, h) = if n.owner.is_some() {
5661                            n.capture
5662                                .as_ref()
5663                                .map(|c| (c.settings.width, c.settings.height))
5664                                .unwrap_or(n.view_size)
5665                        } else {
5666                            n.output
5667                                .current_mode()
5668                                .map(|m| (m.size.w, m.size.h))
5669                                .unwrap_or((0, 0))
5670                        };
5671                        (
5672                            n.id,
5673                            n.pos.0,
5674                            n.pos.1,
5675                            w,
5676                            h,
5677                            n.output.current_scale().fractional_scale(),
5678                            n.capture.is_some(),
5679                        )
5680                    })
5681                    .collect();
5682                let _ = reply.send(list);
5683            }
5684            ThreadCommand::MoveWindowToOutput {
5685                window_id,
5686                output_id,
5687                reply,
5688            } => {
5689                let window = state
5690                    .space
5691                    .elements()
5692                    .find(|w| {
5693                        wayland::frontend::window_meta(w)
5694                            .map(|m| m.id == window_id)
5695                            .unwrap_or(false)
5696                    })
5697                    .cloned();
5698                let ok = match window {
5699                    Some(w) => state.place_window_on_output(&w, output_id),
5700                    None => false,
5701                };
5702                let _ = reply.send(ok);
5703            }
5704            ThreadCommand::ListWindows { reply } => {
5705                use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
5706                let mut list = Vec::new();
5707                for window in state.space.elements() {
5708                    let Some(meta) = wayland::frontend::window_meta(window) else {
5709                        continue;
5710                    };
5711                    let (title, app_id) = window
5712                        .toplevel()
5713                        .map(|tl| {
5714                            with_states(tl.wl_surface(), |states| {
5715                                states
5716                                    .data_map
5717                                    .get::<XdgToplevelSurfaceData>()
5718                                    .map(|d| {
5719                                        let a = d.lock().unwrap();
5720                                        (
5721                                            a.title.clone().unwrap_or_default(),
5722                                            a.app_id.clone().unwrap_or_default(),
5723                                        )
5724                                    })
5725                                    .unwrap_or_default()
5726                            })
5727                        })
5728                        .unwrap_or_default();
5729                    list.push((
5730                        meta.id,
5731                        title,
5732                        app_id,
5733                        meta.output.load(Ordering::Relaxed),
5734                        meta.parked.load(Ordering::Relaxed),
5735                    ));
5736                }
5737                let _ = reply.send(list);
5738            }
5739            ThreadCommand::SetClipboardCallback(cb) => {
5740                state.clipboard_callback = Some(cb);
5741                // Re-stage a read of the CURRENT selection so a copy made before this
5742                // callback was (re)armed is delivered rather than lost; the post-dispatch
5743                // drain performs the read. An empty or compositor-owned selection stages
5744                // nothing, since there is no copy to deliver.
5745                state.pending_clipboard_read = (!state.current_selection_mimes.is_empty())
5746                    .then(|| state.current_selection_mimes.clone());
5747            }
5748            ThreadCommand::SetClipboard { entries } => {
5749                // Every text alias is offered once, for the first text entry.
5750                let mut mimes: Vec<String> = Vec::new();
5751                for (mime, _) in &entries {
5752                    if !mime.starts_with("text/plain") {
5753                        mimes.push(mime.clone());
5754                    } else if !mimes.iter().any(|m| m == "TEXT") {
5755                        mimes.extend(
5756                            [
5757                                "text/plain;charset=utf-8",
5758                                "UTF8_STRING",
5759                                "text/plain",
5760                                "STRING",
5761                                "TEXT",
5762                            ]
5763                            .iter()
5764                            .map(|s| s.to_string()),
5765                        );
5766                    }
5767                }
5768                let payload = std::sync::Arc::new(entries);
5769                smithay::wayland::selection::data_device::set_data_device_selection(
5770                    &state.dh,
5771                    &state.seat,
5772                    mimes.clone(),
5773                    payload.clone(),
5774                );
5775                // Middle-click parity with the X11 clipboard bridge: the same
5776                // offer backs the primary selection too.
5777                smithay::wayland::selection::primary_selection::set_primary_selection(
5778                    &state.dh,
5779                    &state.seat,
5780                    mimes,
5781                    payload,
5782                );
5783                // The selection is compositor-owned now; a later SetClipboardCallback
5784                // must not try to re-read a client source that no longer holds it.
5785                state.current_selection_mimes.clear();
5786            }
5787            ThreadCommand::SetCursorCallback(cb) => {
5788                state.cursor_callback_set = cb.is_some();
5789                let _ = state.cursor_tx.send(CursorJob::SetCallback(cb));
5790                if state.cursor_callback_set {
5791                    // With no client cursor yet the default theme sprite stands in, the
5792                    // same one the render path draws for None, so a consumer registering
5793                    // before the first client cursor event still sees a pointer.
5794                    let icon = state
5795                        .current_cursor_icon
5796                        .clone()
5797                        .unwrap_or(CursorImageStatus::Named(Default::default()));
5798                    state.send_cursor_image(&icon);
5799                }
5800            }
5801            ThreadCommand::KeyboardKeys { events } => {
5802                for (scancode, key_state_val) in events {
5803                    if let Some(host) = state.host.as_ref() {
5804                        host.key(scancode, key_state_val > 0);
5805                        continue;
5806                    }
5807                    let key_state = if key_state_val > 0 {
5808                        KeyState::Pressed
5809                    } else {
5810                        KeyState::Released
5811                    };
5812                    let serial = next_serial();
5813                    let time = wayland_time();
5814                    if let Some(keyboard) = state.seat.get_keyboard() {
5815                        keyboard.input(
5816                            state,
5817                            Keycode::new(scancode),
5818                            key_state,
5819                            serial,
5820                            time,
5821                            |_, _, _| FilterResult::<()>::Forward,
5822                        );
5823                    }
5824                }
5825            }
5826            ThreadCommand::KeyboardKey {
5827                scancode,
5828                state: key_state_val,
5829            } => {
5830                if let Some(host) = state.host.as_ref() {
5831                    host.key(scancode, key_state_val > 0);
5832                    return;
5833                }
5834                let key_state = if key_state_val > 0 {
5835                    KeyState::Pressed
5836                } else {
5837                    KeyState::Released
5838                };
5839                let serial = next_serial();
5840                let time = wayland_time();
5841                if let Some(keyboard) = state.seat.get_keyboard() {
5842                    keyboard.input(
5843                        state,
5844                        Keycode::new(scancode),
5845                        key_state,
5846                        serial,
5847                        time,
5848                        |_, _, _| FilterResult::<()>::Forward,
5849                    );
5850                }
5851            }
5852            ThreadCommand::SetKeymapString(text) => {
5853                // rebuild_base rejects a string that will not compile without touching
5854                // the policy, so the seat keymap survives a bad one either way.
5855                if state.keymap_policy.rebuild_base(text) {
5856                    state.apply_keymap_policy();
5857                } else {
5858                    eprintln!(
5859                        "[Wayland] set_keymap_string: keymap failed to compile; keeping current keymap."
5860                    );
5861                }
5862            }
5863            ThreadCommand::SetXkbLayout {
5864                rules,
5865                model,
5866                layout,
5867                variant,
5868                options,
5869                reply,
5870            } => {
5871                match crate::wayland::keymap::compile_rmlvo(
5872                    &rules, &model, &layout, &variant, &options,
5873                ) {
5874                    Some(text) => {
5875                        state.keymap_policy.rebuild_base(text);
5876                        state.apply_keymap_policy();
5877                        let _ = reply.send(true);
5878                    }
5879                    None => {
5880                        eprintln!(
5881                            "[Wayland] set_xkb_layout: RMLVO ({rules:?}, {model:?}, {layout:?}, {variant:?}, {options:?}) failed to compile."
5882                        );
5883                        let _ = reply.send(false);
5884                    }
5885                }
5886            }
5887            ThreadCommand::BindKeysyms { keysyms, reply } => {
5888                let _ = reply.send(state.bind_keysyms(&keysyms));
5889            }
5890            ThreadCommand::SetKeymapOverlay { binds } => {
5891                if state.keymap_policy.has_base() {
5892                    state.keymap_policy.set_manual_overlay(&binds);
5893                    state.apply_keymap_policy();
5894                } else {
5895                    eprintln!("[Wayland] set_keymap_overlay: no base keymap to splice onto.");
5896                }
5897            }
5898            ThreadCommand::GetKeyboardState { reply } => {
5899                let (pressed, mods) = state
5900                    .seat
5901                    .get_keyboard()
5902                    .map(|kb| {
5903                        let pressed: Vec<u32> = kb.pressed_keys().iter().map(|c| c.raw()).collect();
5904                        let m = kb.modifier_state();
5905                        let mask = (m.ctrl as u32)
5906                            | (m.shift as u32) << 1
5907                            | (m.alt as u32) << 2
5908                            | (m.logo as u32) << 3
5909                            | (m.caps_lock as u32) << 4
5910                            | (m.num_lock as u32) << 5
5911                            | (m.iso_level3_shift as u32) << 6
5912                            | (m.iso_level5_shift as u32) << 7;
5913                        (pressed, mask)
5914                    })
5915                    .unwrap_or_default();
5916                let _ = reply.send((pressed, mods));
5917            }
5918            ThreadCommand::Barrier { reply } => {
5919                // The shutdown path fences on this after its StopCaptures:
5920                // joining the reaped threads before acknowledging means
5921                // nothing that can attach to Python survives past the
5922                // fence, so the interpreter never finalizes under a live
5923                // callback. Bounded — discard flags are up and senders
5924                // dropped, so each chain exits after at most its
5925                // in-flight callback.
5926                for join in state.encode_reaper.drain(..) {
5927                    let _ = join.join();
5928                }
5929                for join in state.deliver_reaper.drain(..) {
5930                    let _ = join.join();
5931                }
5932                let _ = reply.send(());
5933            }
5934            ThreadCommand::GetXkbKeymap { reply } => {
5935                let mut keymap_str = String::new();
5936                if let Some(keyboard) = state.seat.get_keyboard() {
5937                    keymap_str =
5938                        keyboard.with_xkb_state(state, |context| match context.xkb().lock() {
5939                            Ok(guard) => {
5940                                let keymap = unsafe { guard.keymap() };
5941                                keymap.get_as_string(
5942                                    smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1,
5943                                )
5944                            }
5945                            Err(_) => String::new(),
5946                        });
5947                }
5948                let _ = reply.send(keymap_str);
5949            }
5950            ThreadCommand::PointerMotion { x, y, motion } => {
5951                state.motion_applied = state.motion_applied.max(motion);
5952                if let Some(host) = state.host.as_ref() {
5953                    host.pointer_motion_abs(x, y);
5954                    return;
5955                }
5956                let serial = next_serial();
5957                let time = wayland_time();
5958                // (x, y) are physical union-layout coordinates: each output occupies
5959                // the physical rectangle at its layout offset, and the point maps
5960                // through the CONTAINING output's scale (clamped into the nearest
5961                // output when outside all of them).
5962                let p = state.layout_physical_to_logical(x, y);
5963
5964                if let Some(pointer) = state.seat.get_pointer() {
5965                    let under = state.pointer_target_under(p);
5966
5967                    state.release_grab_across_screens(&pointer, &under, serial, time);
5968                    let entered = pointer.current_focus() != under.as_ref().map(|(t, _)| t.clone());
5969                    pointer.motion(
5970                        state,
5971                        under.clone(),
5972                        &MotionEvent {
5973                            location: p,
5974                            serial,
5975                            time,
5976                        },
5977                    );
5978                    // A nested wlroots session takes its cursor position from motion
5979                    // events alone, so the position the enter carried must be repeated
5980                    // or its cursor stays behind until the next move -- and a button
5981                    // landing first presses at that stale spot.
5982                    if entered && under.is_some() {
5983                        pointer.motion(
5984                            state,
5985                            under.clone(),
5986                            &MotionEvent {
5987                                location: p,
5988                                serial: next_serial(),
5989                                time,
5990                            },
5991                        );
5992                    }
5993                    pointer.frame(state);
5994                    state.activate_constraint_under(&pointer, &under, p);
5995                }
5996            }
5997            ThreadCommand::PointerRelativeMotion { dx, dy, motion } => {
5998                state.motion_applied = state.motion_applied.max(motion);
5999                if let Some(host) = state.host.as_ref() {
6000                    host.pointer_motion_rel(dx, dy);
6001                    return;
6002                }
6003                // A nested KWin drops the delta the seat's relative_motion carries;
6004                // its fake-input device takes it instead, ahead of the seat's
6005                // absolute move below. KWin sets its pointer from that move rather
6006                // than adding to it, so the two agree when aligned, and a KWin that
6007                // moved its pointer on its own (a screen change) is realigned by the
6008                // next delta. The seat's relative_motion is then withheld, since a
6009                // KWin that did bind the seat's relative pointer would count it twice.
6010                let via_fake_input = crate::wayland::ficlient::pointer_motion_rel(dx, dy);
6011                let time = wayland_time();
6012                let serial = next_serial();
6013
6014                if let Some(pointer) = state.seat.get_pointer() {
6015                    let current_pos = pointer.current_location();
6016                    let event = RelativeMotionEvent {
6017                        time,
6018                        delta: (dx, dy).into(),
6019                        delta_unaccel: (dx, dy).into(),
6020                    };
6021                    let resting = state
6022                        .space
6023                        .element_under(current_pos)
6024                        .map(|(window, loc)| (FocusTarget::Window(window.clone()), loc.to_f64()));
6025                    // A lock's holder is given the delta and nothing else: moved as well, it
6026                    // reads the move a second time off the position it is handed, and a game
6027                    // turns twice as far as the hand did.
6028                    if state.pointer_locked(&pointer, &resting, current_pos) {
6029                        if !via_fake_input {
6030                            pointer.relative_motion(state, resting, &event);
6031                        }
6032                        pointer.frame(state);
6033                        return;
6034                    }
6035                    let new_pos =
6036                        state.clamp_logical((current_pos.x + dx, current_pos.y + dy).into());
6037
6038                    let under = state
6039                        .space
6040                        .element_under(new_pos)
6041                        .map(|(window, loc)| (FocusTarget::Window(window.clone()), loc.to_f64()));
6042
6043                    state.release_grab_across_screens(&pointer, &under, serial, time);
6044                    let entered = pointer.current_focus() != under.as_ref().map(|(t, _)| t.clone());
6045                    pointer.motion(
6046                        state,
6047                        under.clone(),
6048                        &MotionEvent {
6049                            location: new_pos,
6050                            serial,
6051                            time,
6052                        },
6053                    );
6054                    // Same repeat as the absolute arm: an entered nested session
6055                    // learns the position only from a motion event.
6056                    if entered && under.is_some() {
6057                        pointer.motion(
6058                            state,
6059                            under.clone(),
6060                            &MotionEvent {
6061                                location: new_pos,
6062                                serial: next_serial(),
6063                                time,
6064                            },
6065                        );
6066                    }
6067
6068                    if !via_fake_input {
6069                        pointer.relative_motion(state, under.clone(), &event);
6070                    }
6071
6072                    pointer.frame(state);
6073                    state.activate_constraint_under(&pointer, &under, new_pos);
6074                }
6075            }
6076            ThreadCommand::PointerButton {
6077                btn,
6078                state: btn_state_val,
6079            } => {
6080                if let Some(host) = state.host.as_ref() {
6081                    host.pointer_button(btn, btn_state_val > 0);
6082                    return;
6083                }
6084                let serial = next_serial();
6085                let time = wayland_time();
6086                let button_state = if btn_state_val > 0 {
6087                    smithay::backend::input::ButtonState::Pressed
6088                } else {
6089                    smithay::backend::input::ButtonState::Released
6090                };
6091
6092                state.refocus_pointer();
6093                if let Some(pointer) = state.seat.get_pointer() {
6094                    if button_state == smithay::backend::input::ButtonState::Pressed {
6095                        let pos = pointer.current_location();
6096                        let target_window = state.space.element_under(pos).map(|(w, _)| w.clone());
6097
6098                        if let Some(window) = target_window {
6099                            state.space.raise_element(&window, true);
6100                            if let Some(keyboard) = state.seat.get_keyboard() {
6101                                keyboard.set_focus(
6102                                    state,
6103                                    Some(FocusTarget::Window(window)),
6104                                    serial,
6105                                );
6106                            }
6107                        }
6108                    }
6109                    let button = btn;
6110                    pointer.button(
6111                        state,
6112                        &ButtonEvent {
6113                            button,
6114                            state: button_state,
6115                            serial,
6116                            time,
6117                        },
6118                    );
6119                    pointer.frame(state);
6120                }
6121            }
6122            ThreadCommand::PointerAxis { x, y } => {
6123                if let Some(host) = state.host.as_ref() {
6124                    host.pointer_axis(x, y);
6125                    return;
6126                }
6127                let time = wayland_time();
6128                state.refocus_pointer();
6129                if let Some(pointer) = state.seat.get_pointer() {
6130                    let mut frame = AxisFrame::new(time).source(AxisSource::Wheel);
6131
6132                    if x != 0.0 {
6133                        frame = frame
6134                            .value(Axis::Horizontal, x)
6135                            .v120(Axis::Horizontal, (x * SCROLL_V120_PER_UNIT) as i32);
6136                    }
6137
6138                    if y != 0.0 {
6139                        frame = frame
6140                            .value(Axis::Vertical, y)
6141                            .v120(Axis::Vertical, (y * SCROLL_V120_PER_UNIT) as i32);
6142                    }
6143
6144                    if x != 0.0 || y != 0.0 {
6145                        pointer.axis(state, frame);
6146                        pointer.frame(state);
6147                    }
6148                }
6149            }
6150            ThreadCommand::PointerScroll { dx, dy } => {
6151                if let Some(host) = state.host.as_ref() {
6152                    host.pointer_scroll(dx, dy);
6153                    return;
6154                }
6155                let time = wayland_time();
6156                state.refocus_pointer();
6157                if let Some(pointer) = state.seat.get_pointer()
6158                    && (dx != 0.0 || dy != 0.0)
6159                {
6160                    let (_, _, scale) =
6161                        state.layout_logical_to_physical_scaled(pointer.current_location());
6162                    let mut frame = AxisFrame::new(time).source(AxisSource::Finger);
6163                    if dx != 0.0 {
6164                        frame = frame.value(Axis::Horizontal, dx / scale);
6165                    }
6166                    if dy != 0.0 {
6167                        frame = frame.value(Axis::Vertical, dy / scale);
6168                    }
6169                    pointer.axis(state, frame);
6170                    pointer.frame(state);
6171                }
6172            }
6173            ThreadCommand::PointerScrollEnd => {
6174                if let Some(host) = state.host.as_ref() {
6175                    host.pointer_scroll_end();
6176                    return;
6177                }
6178                let time = wayland_time();
6179                if let Some(pointer) = state.seat.get_pointer() {
6180                    let frame = AxisFrame::new(time)
6181                        .source(AxisSource::Finger)
6182                        .stop(Axis::Horizontal)
6183                        .stop(Axis::Vertical);
6184                    pointer.axis(state, frame);
6185                    pointer.frame(state);
6186                }
6187            }
6188            ThreadCommand::UpdateCursorConfig {
6189                render_on_framebuffer,
6190            } => {
6191                state.render_cursor_on_framebuffer = render_on_framebuffer;
6192                if let Some(host) = state.host.as_ref() {
6193                    host.set_cursor_painting(render_on_framebuffer);
6194                }
6195            }
6196            ThreadCommand::SetCursorSize { size, reply } => {
6197                if size <= 0 {
6198                    let _ = reply.send(false);
6199                } else {
6200                    state.cursor_helper = Cursor::load(size);
6201                    let _ = state.cursor_tx.send(CursorJob::SetSize(size));
6202                    // The burned-in cursor changed size; force a repaint everywhere so
6203                    // a static screen doesn't keep showing the old sprite.
6204                    for node in state.output_nodes.iter_mut() {
6205                        if let Some(cap) = node.capture.as_mut() {
6206                            cap.needs_full_render = true;
6207                        }
6208                    }
6209                    let _ = reply.send(true);
6210                }
6211            }
6212            ThreadCommand::RequestIdr { display_id } => {
6213                if let Some(idx) = state.node_idx_for_id(display_id)
6214                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
6215                {
6216                    cap.request_idr();
6217                }
6218            }
6219            ThreadCommand::InvalidateReference {
6220                display_id,
6221                frame_id,
6222            } => {
6223                if let Some(idx) = state.node_idx_for_id(display_id)
6224                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
6225                {
6226                    cap.invalidate_reference(frame_id);
6227                }
6228            }
6229            ThreadCommand::AcknowledgeReference {
6230                display_id,
6231                frame_id,
6232                held,
6233            } => {
6234                if let Some(idx) = state.node_idx_for_id(display_id)
6235                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
6236                {
6237                    cap.acknowledge_reference(frame_id, held);
6238                }
6239            }
6240            ThreadCommand::UpdateRate {
6241                display_id,
6242                bitrate_kbps,
6243                vbv_multiplier,
6244                fps,
6245            } => {
6246                if let Some(idx) = state.node_idx_for_id(display_id)
6247                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
6248                {
6249                    if let Some(b) = bitrate_kbps {
6250                        cap.settings.video_bitrate_kbps = b;
6251                    }
6252                    if let Some(v) = vbv_multiplier {
6253                        cap.settings.video_vbv_multiplier = v;
6254                    }
6255                    if let Some(f) = fps
6256                        && f > 0.0
6257                    {
6258                        cap.settings.target_fps = f;
6259                        if let Some(host) = state.host.as_ref() {
6260                            host.set_fps(display_id, f);
6261                        }
6262                    }
6263                    if let Some(enc) = cap.video_encoder.as_mut()
6264                        && let Err(e) = enc.reconfigure_rate(&cap.settings)
6265                    {
6266                        // The failed re-open left no codec context: the next
6267                        // tick's encode fails, and a full streak makes that
6268                        // failure run the recovery ladder at once.
6269                        eprintln!("[Wayland] rate reconfigure failed: {e}");
6270                        cap.hw_error_streak = HW_ERROR_RECOVERY_THRESHOLD - 1;
6271                    }
6272                    let c = &cap.encode_controls;
6273                    c.bitrate_kbps
6274                        .store(cap.settings.video_bitrate_kbps, Ordering::Relaxed);
6275                    c.vbv_mult_milli.store(
6276                        (cap.settings.video_vbv_multiplier * 1000.0).round() as i32,
6277                        Ordering::Relaxed,
6278                    );
6279                    c.fps_bits.store(
6280                        cap.settings.target_fps.max(1.0).to_bits(),
6281                        Ordering::Relaxed,
6282                    );
6283                    c.rate_dirty.store(true, Ordering::Release);
6284                    if display_id == 0 {
6285                        state.settings.video_bitrate_kbps = cap.settings.video_bitrate_kbps;
6286                        state.settings.video_vbv_multiplier = cap.settings.video_vbv_multiplier;
6287                        state.settings.target_fps = cap.settings.target_fps;
6288                    }
6289                }
6290            }
6291            ThreadCommand::UpdateTunables {
6292                display_id,
6293                tunables: t,
6294            } => {
6295                state.render_cursor_on_framebuffer = t.capture_cursor;
6296                if let Some(host) = state.host.as_ref() {
6297                    host.set_cursor_painting(t.capture_cursor);
6298                }
6299                let _ = state
6300                    .cursor_tx
6301                    .send(CursorJob::SetSizeCap(t.cursor_size_cap));
6302                if display_id == 0 {
6303                    t.apply_to(&mut state.settings);
6304                }
6305                if let Some(idx) = state.node_idx_for_id(display_id)
6306                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
6307                {
6308                    t.apply_to(&mut cap.settings);
6309                    *cap.encode_controls.tunables.lock().unwrap() = Some(t);
6310                    cap.encode_controls
6311                        .tunables_dirty
6312                        .store(true, Ordering::Release);
6313                }
6314            }
6315            ThreadCommand::CuScreenshot { display_id, resp } => {
6316                if state.node_idx_for_id(display_id).is_some() {
6317                    state.pending_screenshot = Some((display_id, resp));
6318                } else {
6319                    let _ = resp.send(Err(format!("Unknown display: {display_id}")));
6320                }
6321            }
6322            ThreadCommand::CuCursorPosition { resp } => {
6323                let pos = state
6324                    .seat
6325                    .get_pointer()
6326                    .map(|p| p.current_location())
6327                    .unwrap_or_else(|| (0.0f64, 0.0f64).into());
6328                let _ = resp.send(state.layout_logical_to_physical(pos));
6329            }
6330            ThreadCommand::CuGetInfo { display_id, resp } => {
6331                // A host-capture start whose mode the host has not answered yet:
6332                // the read parks until it has (the reply then carries the size
6333                // actually captured), which is what makes it a barrier.
6334                match state.host_layout_pending.get_mut(&display_id) {
6335                    Some(p) => p.geometry_waiters.push(resp),
6336                    None => {
6337                        let _ = resp.send(realized_geometry(state, display_id));
6338                    }
6339                }
6340            }
6341        }
6342    }
6343
6344    state.command_rx = Some(command_rx);
6345    event_loop
6346        .handle()
6347        .insert_source(wake_rx, |_, _, state| {
6348            let had_input = drain_thread_commands(state);
6349            // Input landing while the frame timer sits on its long idle
6350            // deadline must not wait it out: service the frame callbacks now
6351            // (at most at frame pace) so the app's echo repaint starts with
6352            // the keypress; the timer's own next fire re-arms the short pace.
6353            if state.frame_idle_long
6354                && state
6355                    .last_input_at
6356                    .is_some_and(|t| t.elapsed() < Duration::from_millis(50))
6357                && state
6358                    .last_idle_service_at
6359                    .is_none_or(|t| t.elapsed() >= Duration::from_millis(16))
6360            {
6361                state.last_idle_service_at = Some(Instant::now());
6362                send_idle_frame_callbacks(state);
6363            }
6364            // Input may pull a capture's due frame forward to its own phase (see
6365            // FramePace). Not under host capture: a pull there would re-encode the frame
6366            // the host already delivered, so the host's own frames wake the render instead.
6367            if had_input
6368                && state.host.is_none()
6369                && state.output_nodes.iter().any(|n| n.capture.is_some())
6370            {
6371                state.space.refresh();
6372                render_pass(state, TickTrigger::Input);
6373            }
6374        })
6375        .unwrap();
6376
6377    event_loop
6378        .handle()
6379        .insert_source(host_frame_rx, |event, _, state| {
6380            // A host frame landed for one output: its display renders now, at most at frame
6381            // pace (FramePace), rather than on a timer tick up to a period away. A wake the
6382            // timer already served finds nothing queued and does nothing.
6383            let smithay::reexports::calloop::channel::Event::Msg(index) = event else {
6384                return;
6385            };
6386            let Some(id) = state
6387                .host
6388                .as_ref()
6389                .filter(|h| h.has_queued_frame(index))
6390                .and_then(|h| h.display_for_output(index))
6391            else {
6392                return;
6393            };
6394            state.space.refresh();
6395            render_display(state, id, TickTrigger::HostFrame);
6396        })
6397        .unwrap();
6398
6399    let source = match ListeningSocketSource::new_auto() {
6400        Ok(s) => s,
6401        Err(e) => {
6402            eprintln!(
6403                "[Wayland] compositor thread aborting: could not bind a wayland-N socket (XDG_RUNTIME_DIR unset/full?): {e}"
6404            );
6405            return;
6406        }
6407    };
6408    let socket_name = source.socket_name().to_string_lossy().into_owned();
6409    println!("[Wayland] Socket listening on: {:?}", socket_name);
6410    // Writing the process environment races any concurrent getenv, which is why
6411    // it is unsafe: nothing here can stop a thread already running from reading
6412    // it. This one write happens during compositor bring-up, before any capture
6413    // or encoder thread exists, and the value is what children of this process
6414    // and the backend probe below need to find the compositor.
6415    unsafe { std::env::set_var("WAYLAND_DISPLAY", &socket_name) };
6416    publish_socket_name(&socket_name);
6417
6418    event_loop
6419        .handle()
6420        .insert_source(source, |client_stream, _, state| {
6421            if let Err(err) = state
6422                .dh
6423                .insert_client(client_stream, Arc::new(ClientState::default()))
6424            {
6425                eprintln!("Error adding wayland client: {:?}", err);
6426            }
6427        })
6428        .expect("Failed to init wayland socket source");
6429
6430    let timer = Timer::immediate();
6431    event_loop
6432        .handle()
6433        .insert_source(timer, move |_, _, state| {
6434            // Apply queued commands (input above all) BEFORE the render/encode work: a
6435            // command that raced the timer wakeup would otherwise wait out the whole tick.
6436            reap_dead_host(state);
6437            drain_thread_commands(state);
6438            reconcile_host_layouts(state);
6439            // Deliver and deferred encode threads of stopped captures are
6440            // joined only once they report finished (a plain atomic load), so
6441            // the tick never waits on one; whatever remains at shutdown, the
6442            // Barrier drain joins.
6443            let mut i = 0;
6444            while i < state.deliver_reaper.len() {
6445                if state.deliver_reaper[i].is_finished() {
6446                    let _ = state.deliver_reaper.swap_remove(i).join();
6447                } else {
6448                    i += 1;
6449                }
6450            }
6451            let mut i = 0;
6452            while i < state.encode_reaper.len() {
6453                if state.encode_reaper[i].is_finished() {
6454                    let _ = state.encode_reaper.swap_remove(i).join();
6455                } else {
6456                    i += 1;
6457                }
6458            }
6459            let loop_start_time = Instant::now();
6460            state.space.refresh();
6461
6462            let now = Instant::now();
6463            let elapsed = now.duration_since(state.last_log_time).as_secs_f64();
6464            if elapsed >= 1.0 {
6465                // Memory is read here rather than per tick: it is reported, not acted on,
6466                // and /proc plus a /dev/shm walk have no place in the render path.
6467                let mut mem: Option<(usize, u64)> = None;
6468                for node in &state.output_nodes {
6469                    let Some(cap) = node.capture.as_ref() else { continue };
6470                    let frames = cap.encode_stats.frames.swap(0, Ordering::Relaxed);
6471                    let stripes = cap.encode_stats.stripes.swap(0, Ordering::Relaxed);
6472                    if crate::log::debug_enabled() {
6473                        let actual_fps = frames as f64 / elapsed;
6474                        let stripes_per_sec = stripes as f64 / elapsed;
6475                        let mode_str = cap.encode_stats.desc.lock().unwrap().clone();
6476                        let n_stripes = cap.encode_stats.n_stripes.load(Ordering::Relaxed);
6477                        let (current_rss, shm_usage) = *mem
6478                            .get_or_insert_with(|| (get_process_rss_bytes(), get_shm_usage_bytes()));
6479
6480                        println!("Display: {} Res: {}x{} Mode: {} Stripes: {} EncFPS: {:.2} EncStripes/s: {:.2} Mem: {}MB SHM: {}MB",
6481                            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);
6482                    }
6483                }
6484                state.last_log_time = now;
6485            }
6486
6487            let any_capturing = state.output_nodes.iter().any(|n| n.capture.is_some());
6488            let any_copy_frame = state.copy_sessions.iter().any(|cs| cs.pending.is_some());
6489            if !any_capturing && state.pending_screenshot.is_none() && !any_copy_frame {
6490                // No render/encode work, but committed clients still need their
6491                // frame callbacks: a vsynced client (FIFO Vulkan present, games, a
6492                // nested compositor's own clients) otherwise blocks in its swap
6493                // until a viewer attaches — apps appear frozen whenever nobody is
6494                // watching.
6495                //
6496                // Slowly, though: at a viewing rate the clients draw at a viewing rate
6497                // too, and every frame they hand over is one that nothing renders,
6498                // encodes, or looks at. Unblocking them is the whole purpose here, and
6499                // that costs a callback every so often rather than sixty a second.
6500                state.last_idle_service_at = Some(Instant::now());
6501                send_idle_frame_callbacks(state);
6502                // A parked capture session is an attached consumer that may request a
6503                // frame at any moment: tick at frame pace so that request waits at most
6504                // one frame, not a quarter second. Input gets the same window: apps
6505                // pacing on frame callbacks see their echo promptly after a keypress,
6506                // or the idle rate alone turns typing into a 250ms stutter. The wake
6507                // handler covers the first keypress landing while the long deadline
6508                // is already armed.
6509                let post_input = state
6510                    .last_input_at
6511                    .is_some_and(|t| t.elapsed() < Duration::from_secs(1));
6512                let idle = if !state.copy_sessions.is_empty() || post_input {
6513                    Duration::from_millis(16)
6514                } else {
6515                    IDLE_FRAME_INTERVAL
6516                };
6517                state.frame_idle_long = idle == IDLE_FRAME_INTERVAL;
6518                return TimeoutAction::ToDuration(idle);
6519            }
6520            state.frame_idle_long = false;
6521
6522            if render_pass(state, TickTrigger::Timer) {
6523                return TimeoutAction::ToDuration(Duration::from_millis(1));
6524            }
6525            // The next fire is the earliest capture's due time, so a frame input pulled
6526            // forward carries the cadence with it; with no capture (a screenshot or a
6527            // capture client alone) the primary's rate paces from this tick's start.
6528            let now = Instant::now();
6529            let next_due = state
6530                .output_nodes
6531                .iter()
6532                .filter_map(|n| n.capture.as_ref())
6533                .map(|cap| cap.pace.next_due(capture_period(cap), now))
6534                .min()
6535                .unwrap_or(loop_start_time + frame_period(state.settings.target_fps));
6536            let wait = next_due.saturating_duration_since(now);
6537            TimeoutAction::ToDuration(if wait.as_millis() < 1 { Duration::from_millis(1) } else { wait })
6538        })
6539        .expect("Failed to init capture timer");
6540
6541    event_loop
6542        .handle()
6543        .insert_source(
6544            Generic::new(display, Interest::READ, Mode::Level),
6545            |_, display, _state| {
6546                // A single misbehaving client must not take the compositor (and every other
6547                // session) down with it.
6548                if let Err(e) = unsafe { display.get_mut().dispatch_clients(_state) } {
6549                    eprintln!("[Wayland] client dispatch error: {e:?}");
6550                }
6551                Ok(PostAction::Continue)
6552            },
6553        )
6554        .unwrap();
6555
6556    crate::computer_use::register_wayland_backend(command_tx.clone());
6557    crate::computer_use::spawn_cu_from_env();
6558    crate::wayland::ficlient::arm(crate::computer_use::app_wayland_socket_path());
6559
6560    let _ = event_loop.run(None, &mut state, |state| {
6561        state.process_pending_clipboard_read();
6562        state.flush_pending_cursor();
6563        if state.host.is_none()
6564            && let Some(pointer) = state.seat.get_pointer()
6565        {
6566            let (x, y, scale) = state.layout_logical_to_physical_scaled(pointer.current_location());
6567            state
6568                .pointer_share
6569                .publish(crate::wayland::frontend::PointerAt {
6570                    x,
6571                    y,
6572                    scale,
6573                    motion: state.motion_applied,
6574                });
6575        }
6576        let _ = state.dh.flush_clients();
6577    });
6578}
6579
6580/// Zero-copy encoded-frame handoff to Python. Owns the encoded `Vec<u8>` and
6581/// exposes it read-only via the buffer protocol, so `bytes(frame)` /
6582/// `memoryview(frame)` alias the Rust buffer instead of copying. Carries the
6583/// four stripe-metadata ints as Python attributes, and the frame's capture and
6584/// encode times as CLOCK_MONOTONIC nanoseconds (`time.monotonic_ns()`), zero
6585/// where a path does not stamp them.
6586#[pyclass]
6587struct StripeFrame {
6588    data: Arc<Vec<u8>>,
6589    #[pyo3(get, set)]
6590    data_type: i32,
6591    #[pyo3(get, set)]
6592    stripe_y_start: i32,
6593    #[pyo3(get, set)]
6594    stripe_height: i32,
6595    #[pyo3(get, set)]
6596    frame_id: i32,
6597    #[pyo3(get)]
6598    capture_ns: i64,
6599    #[pyo3(get)]
6600    encode_start_ns: i64,
6601    #[pyo3(get)]
6602    encode_end_ns: i64,
6603    /// The frame this one predicts from: its id, -1 for a frame that decodes on its own, -2
6604    /// where the encoder does not track its references.
6605    #[pyo3(get)]
6606    reference_frame_id: i32,
6607}
6608
6609impl StripeFrame {
6610    /// Hot-path constructor: shares the encoder's buffer by `Arc` (no copy) and carries stripe
6611    /// metadata as attributes, so the consumer can read it without parsing a header
6612    /// (required for omit_stripe_headers).
6613    fn new_owned_meta(
6614        data: Arc<Vec<u8>>,
6615        data_type: i32,
6616        stripe_y_start: i32,
6617        stripe_height: i32,
6618        frame_id: i32,
6619        timing: FrameTiming,
6620        reference: Reference,
6621    ) -> Self {
6622        Self {
6623            data,
6624            data_type,
6625            stripe_y_start,
6626            stripe_height,
6627            frame_id,
6628            capture_ns: timing.capture_ns,
6629            encode_start_ns: timing.encode_start_ns,
6630            encode_end_ns: timing.encode_end_ns,
6631            reference_frame_id: reference.frame_id(),
6632        }
6633    }
6634}
6635
6636#[pymethods]
6637impl StripeFrame {
6638    /// Symmetry / testability constructor: copies the bytes-like into the owned `Vec`.
6639    /// The hot path uses `new_owned_meta` (a move) instead.
6640    #[new]
6641    #[pyo3(signature = (data, data_type = 0, stripe_y_start = 0, stripe_height = 0, frame_id = 0))]
6642    fn new(
6643        data: Vec<u8>,
6644        data_type: i32,
6645        stripe_y_start: i32,
6646        stripe_height: i32,
6647        frame_id: i32,
6648    ) -> Self {
6649        Self::new_owned_meta(
6650            Arc::new(data),
6651            data_type,
6652            stripe_y_start,
6653            stripe_height,
6654            frame_id,
6655            FrameTiming::default(),
6656            Reference::Untracked,
6657        )
6658    }
6659
6660    fn __len__(&self) -> usize {
6661        self.data.len()
6662    }
6663
6664    /// Expose the owned bytes read-only through the Python buffer protocol.
6665    ///
6666    /// `PyBuffer_FillInfo` INCREFs `slf` into `view->obj`, pinning the `Vec` until every view is
6667    /// released, so memoryviews can outlive the Python `frame` handle.
6668    unsafe fn __getbuffer__(
6669        slf: PyRef<'_, Self>,
6670        view: *mut pyo3::ffi::Py_buffer,
6671        flags: std::os::raw::c_int,
6672    ) -> PyResult<()> {
6673        let r = unsafe {
6674            pyo3::ffi::PyBuffer_FillInfo(
6675                view,
6676                slf.as_ptr(),
6677                slf.data.as_ptr() as *mut std::os::raw::c_void,
6678                slf.data.len() as pyo3::ffi::Py_ssize_t,
6679                1,
6680                flags,
6681            )
6682        };
6683        if r != 0 {
6684            return Err(PyErr::fetch(slf.py()));
6685        }
6686        Ok(())
6687    }
6688
6689    unsafe fn __releasebuffer__(&self, _view: *mut pyo3::ffi::Py_buffer) {}
6690}
6691
6692/// The Python handle to the one long-lived compositor thread. Because calloop, EGL/GBM, and
6693/// the Wayland display are all thread-affine and the compositor has to keep running to serve its
6694/// clients even between captures, the backend cannot be a passive object that starts work on demand:
6695/// constructing it spawns that thread and it stays up for the process lifetime. The struct itself is
6696/// nothing but the command-channel sender used to drive that thread across the boundary.
6697#[pyclass]
6698struct WaylandBackend {
6699    tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
6700    /// The compositor's pointer, as its thread last published it (`pointer_location`).
6701    pointer: Arc<crate::wayland::frontend::PointerShare>,
6702    /// The number the last motion sent was given.
6703    motions: AtomicU64,
6704    /// Wakes the calloop after each command send: the command channel itself is drained in
6705    /// place by the compositor thread (render tick and wake handler), not registered as its
6706    /// own source, so input never waits behind an in-flight render tick's timer wakeup.
6707    wake_tx: smithay::reexports::calloop::channel::Sender<()>,
6708}
6709
6710impl WaylandBackend {
6711    fn send(&self, cmd: ThreadCommand) -> Result<(), String> {
6712        self.tx.send(cmd).map_err(|e| e.to_string())?;
6713        let _ = self.wake_tx.send(());
6714        Ok(())
6715    }
6716}
6717
6718#[pymethods]
6719impl WaylandBackend {
6720    /// Construct the backend and spawn the long-lived compositor thread, handing it the
6721    /// strongest scheduling edge (nice -15) because it drives the calloop, input dispatch, the render
6722    /// loop, and — on the zero-copy path — the encode itself, all on this single thread, so any
6723    /// scheduling starvation here surfaces directly as dropped or late frames.
6724    #[new]
6725    #[pyo3(signature = (width, height, dri_node, auto_gpu_selected = false, cursor_size = -1))]
6726    fn new(
6727        width: i32,
6728        height: i32,
6729        dri_node: String,
6730        auto_gpu_selected: bool,
6731        cursor_size: i32,
6732    ) -> Self {
6733        let (tx, rx) = smithay::reexports::calloop::channel::channel();
6734        let (wake_tx, wake_rx) = smithay::reexports::calloop::channel::channel();
6735        let cu_tx = tx.clone();
6736        let pointer = Arc::new(crate::wayland::frontend::PointerShare::default());
6737        let pointer_share = pointer.clone();
6738        thread::spawn(move || {
6739            crate::boost_thread_priority(-15);
6740            run_wayland_thread(WaylandThreadConfig {
6741                command_rx: rx,
6742                wake_rx,
6743                command_tx: cu_tx,
6744                initial_width: width,
6745                initial_height: height,
6746                explicit_dri_node: dri_node,
6747                auto_gpu_selected,
6748                cursor_size,
6749                pointer_share,
6750            });
6751        });
6752        WaylandBackend {
6753            tx,
6754            wake_tx,
6755            pointer,
6756            motions: AtomicU64::new(0),
6757        }
6758    }
6759
6760    /// Where the compositor's pointer is as of its thread's last pass, as `(x, y, scale,
6761    /// motion)`: physical union-layout coordinates (where `inject_mouse_move` puts it), the
6762    /// scale of the output under it, by which a relative move's pixels travel physically,
6763    /// and the number of the last move applied (what `inject_mouse_move` and
6764    /// `inject_relative_mouse_move` return), so a caller can tell which of its moves the
6765    /// position includes. An app's warp is included too, since the position is read back
6766    /// rather than summed from what was injected. None before the first pass and under host
6767    /// capture, whose pointer is the host's. Never waits on the compositor.
6768    fn pointer_location(&self) -> Option<(f64, f64, f64, u64)> {
6769        self.pointer
6770            .read()
6771            .map(|at| (at.x, at.y, at.scale, at.motion))
6772    }
6773
6774    /// Begin a capture with the given frame callback and settings. The target display is
6775    /// the settings' `display_id` attribute (absent = 0, the primary); each display id runs
6776    /// at most one capture, independent of every other display's.
6777    ///
6778    /// Issuing the start also clears the interpreter-teardown gate: starting from Python proves the
6779    /// interpreter is live again after a manual atexit sweep.
6780    fn start_capture(&self, callback: Py<PyAny>, settings: &Bound<'_, PyAny>) -> PyResult<()> {
6781        let rust_settings = extract_settings(settings)?;
6782        let display_id = read_display_id(settings);
6783
6784        PY_SHUTDOWN.store(false, Ordering::Relaxed);
6785        self.send(ThreadCommand::StartCapture {
6786            display_id,
6787            callback: Some(callback),
6788            settings: rust_settings,
6789        })
6790        .map_err(|e| {
6791            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6792                "Failed to send start command: {}",
6793                e
6794            ))
6795        })?;
6796        Ok(())
6797    }
6798
6799    /// Stop the capture bound to `display_id` (default: the primary display).
6800    #[pyo3(signature = (display_id = 0))]
6801    fn stop_capture(&self, display_id: u32) -> PyResult<()> {
6802        self.send(ThreadCommand::StopCapture { display_id })
6803            .map_err(|e| {
6804                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6805                    "Failed to send stop command: {}",
6806                    e
6807                ))
6808            })?;
6809        Ok(())
6810    }
6811
6812    /// Create an additional output (`WxH` physical pixels at fractional `scale`) mapped into
6813    /// the layout at offset `(x, y)`; `id` is the display key used by every per-display API.
6814    /// False when the id is taken, the geometry/scale is invalid, the rectangle overlaps a
6815    /// live output, or the GPU render target cannot be allocated. Capture reconfigures
6816    /// (`start_capture` on an existing output) resize without this validation, so a
6817    /// multi-step relayout must stay overlap-free at every step by caller ordering.
6818    // The parameter list is the Python signature; grouping it would change the ABI.
6819    #[allow(clippy::too_many_arguments)]
6820    #[pyo3(signature = (id, width, height, x = 0, y = 0, scale = 1.0))]
6821    fn create_output(
6822        &self,
6823        py: Python<'_>,
6824        id: u32,
6825        width: i32,
6826        height: i32,
6827        x: i32,
6828        y: i32,
6829        scale: f64,
6830    ) -> PyResult<bool> {
6831        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
6832        self.send(ThreadCommand::CreateOutput {
6833            id,
6834            width,
6835            height,
6836            x,
6837            y,
6838            scale,
6839            reply: reply_tx,
6840        })
6841        .map_err(|e| {
6842            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6843                "Failed to create output: {}",
6844                e
6845            ))
6846        })?;
6847        Ok(py
6848            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6849            .unwrap_or(false))
6850    }
6851
6852    /// Add a display over a rectangle of an existing output, capturing that rectangle
6853    /// without publishing a screen of its own.
6854    ///
6855    /// The session then sees one screen where the client sees several, which is what a
6856    /// window drag needs: a pointer grab stays inside the one surface as it crosses from
6857    /// one display to the next, instead of stopping at the edge of the screen it started
6858    /// on. `x` and `y` are relative to `owner`'s output. False when the id is taken, the
6859    /// owner is unknown or itself a view, or the rectangle leaves the owner's output.
6860    #[allow(clippy::too_many_arguments)]
6861    #[pyo3(signature = (id, owner, x, y, width, height))]
6862    fn create_view(
6863        &self,
6864        py: Python<'_>,
6865        id: u32,
6866        owner: u32,
6867        x: i32,
6868        y: i32,
6869        width: i32,
6870        height: i32,
6871    ) -> PyResult<bool> {
6872        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
6873        self.send(ThreadCommand::CreateView {
6874            id,
6875            owner,
6876            x,
6877            y,
6878            width,
6879            height,
6880            reply: reply_tx,
6881        })
6882        .map_err(|e| {
6883            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6884                "Failed to create view: {}",
6885                e
6886            ))
6887        })?;
6888        Ok(py
6889            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6890            .unwrap_or(false))
6891    }
6892
6893    /// Resize a screen's output in place, keeping its position: the desk the displays cut
6894    /// out of it are laid over. False for a view, an unknown id, or a size that would leave
6895    /// one of the screen's views outside it.
6896    #[pyo3(signature = (id, width, height, scale = 1.0))]
6897    fn resize_output(
6898        &self,
6899        py: Python<'_>,
6900        id: u32,
6901        width: i32,
6902        height: i32,
6903        scale: f64,
6904    ) -> PyResult<bool> {
6905        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
6906        self.send(ThreadCommand::ResizeOutput {
6907            id,
6908            width,
6909            height,
6910            scale,
6911            reply: reply_tx,
6912        })
6913        .map_err(|e| {
6914            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6915                "Failed to resize output: {}",
6916                e
6917            ))
6918        })?;
6919        Ok(py
6920            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6921            .unwrap_or(false))
6922    }
6923
6924    /// Destroy a secondary output: its capture ends cleanly and its windows relocate to the
6925    /// primary output. False for the primary (id 0) or an unknown id.
6926    fn destroy_output(&self, py: Python<'_>, id: u32) -> PyResult<bool> {
6927        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
6928        self.send(ThreadCommand::DestroyOutput {
6929            id,
6930            reply: reply_tx,
6931        })
6932        .map_err(|e| {
6933            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6934                "Failed to destroy output: {}",
6935                e
6936            ))
6937        })?;
6938        Ok(py
6939            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6940            .unwrap_or(false))
6941    }
6942
6943    /// Move an existing output (the primary, id 0, included) to layout offset `(x, y)`;
6944    /// its windows, absolute input injection, and cursor compositing follow. False for an
6945    /// unknown id or a destination overlapping a live output. Capture reconfigures
6946    /// (`start_capture` on an existing output) resize without this validation, so a
6947    /// multi-step relayout must stay overlap-free at every step by caller ordering.
6948    fn reposition_output(&self, py: Python<'_>, id: u32, x: i32, y: i32) -> PyResult<bool> {
6949        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
6950        self.send(ThreadCommand::RepositionOutput {
6951            id,
6952            x,
6953            y,
6954            reply: reply_tx,
6955        })
6956        .map_err(|e| {
6957            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6958                "Failed to reposition output: {}",
6959                e
6960            ))
6961        })?;
6962        Ok(py
6963            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6964            .unwrap_or(false))
6965    }
6966
6967    /// Every live output as `(id, x, y, width, height, scale, capturing)` — width/height in
6968    /// physical pixels, `(x, y)` the layout offset.
6969    fn list_outputs(&self, py: Python<'_>) -> PyResult<Vec<OutputDesc>> {
6970        let (reply_tx, reply_rx) = std::sync::mpsc::channel();
6971        self.send(ThreadCommand::ListOutputs { reply: reply_tx })
6972            .map_err(|e| {
6973                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6974                    "Failed to list outputs: {}",
6975                    e
6976                ))
6977            })?;
6978        Ok(py
6979            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6980            .unwrap_or_default())
6981    }
6982
6983    /// How many displays this backend can back with real content: -1 when
6984    /// self-compositing (outputs are created on demand, no fixed bound), otherwise the
6985    /// host compositor's output count. Host-capture mode reports -1 until the first
6986    /// capture start establishes the host session; 0 when the backend is unresponsive.
6987    fn output_capacity(&self, py: Python<'_>) -> PyResult<i64> {
6988        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<i64>();
6989        self.send(ThreadCommand::OutputCapacity { reply: reply_tx })
6990            .map_err(|e| {
6991                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6992                    "Failed to query output capacity: {}",
6993                    e
6994                ))
6995            })?;
6996        Ok(py
6997            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6998            .unwrap_or(0))
6999    }
7000
7001    /// Move the window with the given id onto output `output_id`, fullscreened at that
7002    /// output's logical size. False for an unknown window or output id.
7003    fn move_window_to_output(
7004        &self,
7005        py: Python<'_>,
7006        window_id: u32,
7007        output_id: u32,
7008    ) -> PyResult<bool> {
7009        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
7010        self.send(ThreadCommand::MoveWindowToOutput {
7011            window_id,
7012            output_id,
7013            reply: reply_tx,
7014        })
7015        .map_err(|e| {
7016            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7017                "Failed to move window: {}",
7018                e
7019            ))
7020        })?;
7021        Ok(py
7022            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
7023            .unwrap_or(false))
7024    }
7025
7026    /// Every mapped window as `(window_id, title, app_id, output_id, waiting)`. A waiting
7027    /// window is tagged for that output but mapped clear of every one of them, holding its
7028    /// size until an output exists for it — a nested session's spare screens.
7029    fn list_windows(&self, py: Python<'_>) -> PyResult<Vec<WindowDesc>> {
7030        let (reply_tx, reply_rx) = std::sync::mpsc::channel();
7031        self.send(ThreadCommand::ListWindows { reply: reply_tx })
7032            .map_err(|e| {
7033                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7034                    "Failed to list windows: {}",
7035                    e
7036                ))
7037            })?;
7038        Ok(py
7039            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
7040            .unwrap_or_default())
7041    }
7042
7043    /// cb(msg_type: str, png: bytes, hot_x: int, hot_y: int); `None` withdraws it.
7044    fn set_cursor_callback(&self, callback: Option<Py<PyAny>>) -> PyResult<()> {
7045        self.send(ThreadCommand::SetCursorCallback(callback))
7046            .map_err(|e| {
7047                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7048                    "Failed to set cursor callback: {}",
7049                    e
7050                ))
7051            })?;
7052        Ok(())
7053    }
7054
7055    fn clear_cursor_callback(&self) -> PyResult<()> {
7056        self.set_cursor_callback(None)
7057    }
7058
7059    /// Recreate the cursor theme at `size` pixels — no restart: subsequent named-cursor
7060    /// callbacks and the burned-in cursor overlay render at the new size. False for a
7061    /// non-positive size.
7062    fn set_cursor_size(&self, py: Python<'_>, size: i32) -> PyResult<bool> {
7063        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
7064        self.send(ThreadCommand::SetCursorSize {
7065            size,
7066            reply: reply_tx,
7067        })
7068        .map_err(|e| {
7069            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7070                "Failed to set cursor size: {}",
7071                e
7072            ))
7073        })?;
7074        Ok(py
7075            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
7076            .unwrap_or(false))
7077    }
7078
7079    /// cb(entries: list[tuple[str, bytes]]) fires when a client app copies to the clipboard,
7080    /// with the flavors of the copy: the picture, or the markup and the text beneath it, and a
7081    /// password manager's hint beside a secret; a client clearing the clipboard fires it with
7082    /// no entries.
7083    fn set_clipboard_callback(&self, callback: Py<PyAny>) -> PyResult<()> {
7084        self.send(ThreadCommand::SetClipboardCallback(callback))
7085            .map_err(|e| {
7086                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7087                    "Failed to set clipboard callback: {}",
7088                    e
7089                ))
7090            })?;
7091        Ok(())
7092    }
7093
7094    /// Compositor-side clipboard offer: serve one payload per `(mime, data)` entry, so a
7095    /// client pasting rich text takes the markup and one pasting into a plain field takes
7096    /// the text the source itself wrote, instead of both taking whichever flavor was
7097    /// picked for them.
7098    fn set_clipboard(&self, entries: Vec<(String, Vec<u8>)>) -> PyResult<()> {
7099        self.send(ThreadCommand::SetClipboard { entries })
7100            .map_err(|e| {
7101                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7102                    "Failed to set clipboard: {}",
7103                    e
7104                ))
7105            })?;
7106        Ok(())
7107    }
7108
7109    fn inject_key(&self, scancode: u32, state: u32) -> PyResult<()> {
7110        self.send(ThreadCommand::KeyboardKey { scancode, state })
7111            .map_err(|e| {
7112                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7113                    "Failed to inject key: {}",
7114                    e
7115                ))
7116            })?;
7117        Ok(())
7118    }
7119
7120    /// Inject an ordered run of `(keycode, state)` events as one message, so a paste
7121    /// costs one channel send and one wake instead of one per event.
7122    fn inject_keys(&self, events: Vec<(u32, u32)>) -> PyResult<()> {
7123        if events.is_empty() {
7124            return Ok(());
7125        }
7126        self.send(ThreadCommand::KeyboardKeys { events })
7127            .map_err(|e| {
7128                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7129                    "Failed to inject keys: {}",
7130                    e
7131                ))
7132            })?;
7133        Ok(())
7134    }
7135
7136    /// Swap the seat keyboard's xkb keymap (XKB_KEYMAP_FORMAT_TEXT_V1 text). The caller
7137    /// owns keysym-to-keycode policy: define keycodes here, then press them via
7138    /// `inject_key`. Ordered with key events on the one compositor channel.
7139    /// Bind explicit `(keycode, keysym)` pairs onto the current base keymap in ONE swap.
7140    ///
7141    /// The caller owns the assignment — which keysym goes to which keycode, and when to
7142    /// recycle one — because that tracks layouts and user reports. This end only assembles
7143    /// the xkb text and delivers it, reusing the installed base rather than recompiling a
7144    /// re-supplied one. False when no base keymap is installed yet.
7145    fn set_keymap_overlay(&self, binds: Vec<(u32, u32)>) -> PyResult<()> {
7146        self.send(ThreadCommand::SetKeymapOverlay { binds })
7147            .map_err(|e| {
7148                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7149                    "Failed to set overlay: {}",
7150                    e
7151                ))
7152            })?;
7153        Ok(())
7154    }
7155
7156    fn set_keymap_string(&self, text: String) -> PyResult<()> {
7157        self.send(ThreadCommand::SetKeymapString(text))
7158            .map_err(|e| {
7159                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7160                    "Failed to set keymap: {}",
7161                    e
7162                ))
7163            })?;
7164        Ok(())
7165    }
7166
7167    /// Return the active xkb keymap as an XKB_KEYMAP_FORMAT_TEXT_V1 string so a consumer can
7168    /// build a reverse keysym->keycode map from the identical keymap.
7169    ///
7170    /// The GIL is released while awaiting the reply, because the Wayland thread can call back into
7171    /// Python and would otherwise deadlock; the wait is bounded so a stall cannot hang the caller,
7172    /// and an empty string is returned when the keymap cannot be read in time.
7173    fn get_xkb_keymap_string(&self, py: Python<'_>) -> PyResult<String> {
7174        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<String>();
7175        self.send(ThreadCommand::GetXkbKeymap { reply: reply_tx })
7176            .map_err(|e| {
7177                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7178                    "Failed to request keymap: {}",
7179                    e
7180                ))
7181            })?;
7182        let result = py.detach(move || reply_rx.recv_timeout(Duration::from_secs(2)));
7183        match result {
7184            Ok(s) => Ok(s),
7185            Err(_) => Ok(String::new()),
7186        }
7187    }
7188
7189    /// Move the pointer to a point in physical union-layout coordinates. Returns the
7190    /// move's number (`pointer_location`).
7191    fn inject_mouse_move(&self, x: f64, y: f64) -> PyResult<u64> {
7192        let motion = self.motions.fetch_add(1, Ordering::Relaxed) + 1;
7193        self.send(ThreadCommand::PointerMotion { x, y, motion })
7194            .map_err(|e| {
7195                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7196                    "Failed to inject motion: {}",
7197                    e
7198                ))
7199            })?;
7200        Ok(motion)
7201    }
7202
7203    /// Move the pointer by a delta. On the seat this is an absolute move plus a
7204    /// `zwp_relative_pointer_v1` event; a nested KWin session, which forwards no
7205    /// delta from its host seat, receives it through its `org_kde_kwin_fake_input`
7206    /// device on the app compositor socket named by `set_app_wayland_display`. Returns the
7207    /// move's number (`pointer_location`).
7208    fn inject_relative_mouse_move(&self, dx: f64, dy: f64) -> PyResult<u64> {
7209        let motion = self.motions.fetch_add(1, Ordering::Relaxed) + 1;
7210        self.send(ThreadCommand::PointerRelativeMotion { dx, dy, motion })
7211            .map_err(|e| {
7212                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7213                    "Failed to inject relative motion: {}",
7214                    e
7215                ))
7216            })?;
7217        Ok(motion)
7218    }
7219
7220    fn inject_mouse_button(&self, btn: u32, state: u32) -> PyResult<()> {
7221        self.send(ThreadCommand::PointerButton { btn, state })
7222            .map_err(|e| {
7223                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7224                    "Failed to inject button: {}",
7225                    e
7226                ))
7227            })?;
7228        Ok(())
7229    }
7230
7231    fn inject_mouse_scroll(&self, x: f64, y: f64) -> PyResult<()> {
7232        self.send(ThreadCommand::PointerAxis { x, y })
7233            .map_err(|e| {
7234                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7235                    "Failed to inject axis: {}",
7236                    e
7237                ))
7238            })?;
7239        Ok(())
7240    }
7241
7242    /// Scroll by a finger's travel of (dx, dy) physical pixels, the way a touchpad does:
7243    /// fractional, with no wheel steps, until `inject_finger_scroll_end`.
7244    fn inject_finger_scroll(&self, dx: f64, dy: f64) -> PyResult<()> {
7245        self.send(ThreadCommand::PointerScroll { dx, dy })
7246            .map_err(|e| {
7247                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7248                    "Failed to inject finger scroll: {}",
7249                    e
7250                ))
7251            })?;
7252        Ok(())
7253    }
7254
7255    /// End a finger's scroll: the axis stop a toolkit starts its kinetic scroll on.
7256    fn inject_finger_scroll_end(&self) -> PyResult<()> {
7257        self.send(ThreadCommand::PointerScrollEnd).map_err(|e| {
7258            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7259                "Failed to end finger scroll: {}",
7260                e
7261            ))
7262        })?;
7263        Ok(())
7264    }
7265
7266    fn set_cursor_rendering(&self, enabled: bool) -> PyResult<()> {
7267        self.send(ThreadCommand::UpdateCursorConfig {
7268            render_on_framebuffer: enabled,
7269        })
7270        .map_err(|e| {
7271            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7272                "Failed to set cursor config: {}",
7273                e
7274            ))
7275        })?;
7276        Ok(())
7277    }
7278
7279    /// Forces an IDR/keyframe on the next captured frame so a (re)connecting client
7280    /// or a decoder reset can resume immediately. With the default infinite GOP this
7281    /// is the only recovery path, so every consumer that can lose decoder state must
7282    /// call it. No-op cost on the JPEG/software path (keyframes are N/A).
7283    #[pyo3(signature = (display_id = 0))]
7284    fn request_idr_frame(&self, display_id: u32) -> PyResult<()> {
7285        self.send(ThreadCommand::RequestIdr { display_id })
7286            .map_err(|e| {
7287                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7288                    "Failed to request IDR: {}",
7289                    e
7290                ))
7291            })?;
7292        Ok(())
7293    }
7294
7295    /// A client lost frame `frame_id` of the given display's capture: the frames after it stop
7296    /// predicting from it, so the next one decodes there without a keyframe. An encoder that
7297    /// cannot leave a frame out codes a keyframe instead.
7298    #[pyo3(signature = (frame_id, display_id = 0))]
7299    fn invalidate_reference(&self, frame_id: u16, display_id: u32) -> PyResult<()> {
7300        self.send(ThreadCommand::InvalidateReference {
7301            display_id,
7302            frame_id,
7303        })
7304        .map_err(|e| {
7305            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7306                "Failed to invalidate reference: {}",
7307                e
7308            ))
7309        })
7310    }
7311
7312    /// Every client holds frame `frame_id` of one display's capture, or where not `held` was
7313    /// sent it, so a session keeping long-term references pins the newest anchor they all hold
7314    /// and predicts its anchors from a frame they were all sent: each client recovers on its own
7315    /// from a loss of any depth, without a keyframe.
7316    #[pyo3(signature = (frame_id, display_id = 0, held = true))]
7317    fn acknowledge_reference(&self, frame_id: u16, display_id: u32, held: bool) -> PyResult<()> {
7318        self.send(ThreadCommand::AcknowledgeReference {
7319            display_id,
7320            frame_id,
7321            held,
7322        })
7323        .map_err(|e| {
7324            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7325                "Failed to acknowledge reference: {}",
7326                e
7327            ))
7328        })
7329    }
7330
7331    /// Apply a live bitrate (kbps) / VBV (kb) / framerate change to the given display's
7332    /// running capture.
7333    #[pyo3(signature = (bitrate_kbps = None, vbv_multiplier = None, fps = None, display_id = 0))]
7334    fn update_rate(
7335        &self,
7336        bitrate_kbps: Option<i32>,
7337        vbv_multiplier: Option<f64>,
7338        fps: Option<f64>,
7339        display_id: u32,
7340    ) -> PyResult<()> {
7341        self.send(ThreadCommand::UpdateRate {
7342            display_id,
7343            bitrate_kbps,
7344            vbv_multiplier,
7345            fps,
7346        })
7347        .map_err(|e| {
7348            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7349                "Failed to update rate: {}",
7350                e
7351            ))
7352        })?;
7353        Ok(())
7354    }
7355
7356    /// Set the seat's BASE xkb layout from RMLVO names at runtime (empty strings select the
7357    /// xkbcommon defaults). Returns whether the layout compiled and was applied; overlay binds
7358    /// rebuild on top with their keycodes unchanged.
7359    #[pyo3(signature = (layout, variant = String::new(), options = String::new(), model = String::new(), rules = String::new()))]
7360    fn set_xkb_layout(
7361        &self,
7362        py: Python<'_>,
7363        layout: String,
7364        variant: String,
7365        options: String,
7366        model: String,
7367        rules: String,
7368    ) -> PyResult<bool> {
7369        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
7370        self.send(ThreadCommand::SetXkbLayout {
7371            rules,
7372            model,
7373            layout,
7374            variant,
7375            options,
7376            reply: reply_tx,
7377        })
7378        .map_err(|e| {
7379            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7380                "Failed to set layout: {}",
7381                e
7382            ))
7383        })?;
7384        Ok(py
7385            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
7386            .unwrap_or(false))
7387    }
7388
7389    /// Debug/verification readback of the seat keyboard: `(pressed_keycodes, modifier_mask)`
7390    /// with mask bits 1 ctrl, 2 shift, 4 alt, 8 logo, 16 caps, 32 num, 64 altgr, 128 level5.
7391    fn get_keyboard_state(&self, py: Python<'_>) -> PyResult<(Vec<u32>, u32)> {
7392        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<(Vec<u32>, u32)>();
7393        self.send(ThreadCommand::GetKeyboardState { reply: reply_tx })
7394            .map_err(|e| {
7395                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7396                    "Failed to read keyboard state: {}",
7397                    e
7398                ))
7399            })?;
7400        Ok(py
7401            .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
7402            .unwrap_or_default())
7403    }
7404
7405    /// The capture geometry actually live on the given display: `(width, height, scale)` in
7406    /// physical pixels, or `None` when the compositor did not answer in time. Reflects any
7407    /// degrade a `start_capture` performed (H.264 even-masking, GBM allocation failure keeping
7408    /// the previous mode, a host that kept its own mode), and the command channel is FIFO, so
7409    /// calling this after `start_capture` returns what that start realized. A host-capture
7410    /// start answers once the host has ruled on the requested mode (the compositor thread
7411    /// keeps serving input meanwhile), so a slow-but-live start reads its real size instead
7412    /// of being reported as a timeout; the `None` a genuine timeout returns lets the caller
7413    /// treat the geometry as unknown, not as "nothing to reconcile", which a `(0, 0, 0.0)`
7414    /// sentinel could not.
7415    #[pyo3(signature = (display_id = 0))]
7416    fn get_realized_geometry(
7417        &self,
7418        py: Python<'_>,
7419        display_id: u32,
7420    ) -> PyResult<Option<(i32, i32, f64)>> {
7421        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<(i32, i32, f64)>();
7422        self.send(ThreadCommand::CuGetInfo {
7423            display_id,
7424            resp: reply_tx,
7425        })
7426        .map_err(|e| {
7427            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7428                "Failed to read geometry: {}",
7429                e
7430            ))
7431        })?;
7432        Ok(py
7433            .detach(move || reply_rx.recv_timeout(GEOMETRY_BARRIER_TIMEOUT))
7434            .ok())
7435    }
7436
7437    /// Lifecycle of `display_id`'s capture as `(state, last_error)`: `state` is `"running"`
7438    /// (pipeline live), `"failed"` (start left no live pipeline), or `"idle"` (never started
7439    /// / stopped clean); `last_error` is the reason a start failed, or a caveat a live
7440    /// capture came up with (host connect refused -> local compositing, a hardware encoder
7441    /// that fell back to CPU, a refused resize). Read straight from the recorded outcome, so
7442    /// pair it with a prior `get_realized_geometry` when the ordering after a start matters.
7443    #[pyo3(signature = (display_id = 0))]
7444    fn capture_state(&self, display_id: u32) -> (String, Option<String>) {
7445        wayland_capture_state(display_id)
7446    }
7447}
7448
7449/// Bound on the geometry read-back barrier. Behind a host-capture start the read is held
7450/// until the host has ruled on the requested mode, which the host's control thread bounds
7451/// by `LAYOUT_DEADLINE`, so this clears that with margin or a live-but-slow start would read
7452/// as a timeout.
7453const GEOMETRY_BARRIER_TIMEOUT: Duration = Duration::from_secs(6);
7454
7455/// `(state, last_error)` for `display_id`'s Wayland capture, from the shared liveness and
7456/// outcome maps (no command round-trip).
7457fn wayland_capture_state(display_id: u32) -> (String, Option<String>) {
7458    let running = wayland_alive().lock().unwrap().contains(&display_id);
7459    let last_error = wayland_capture_err()
7460        .lock()
7461        .unwrap()
7462        .get(&display_id)
7463        .cloned();
7464    let state = if running {
7465        "running"
7466    } else if last_error.is_some() {
7467        "failed"
7468    } else {
7469        "idle"
7470    };
7471    (state.to_string(), last_error)
7472}
7473
7474impl WaylandBackend {
7475    fn update_tunables(&self, display_id: u32, t: LiveTunables) -> PyResult<()> {
7476        self.send(ThreadCommand::UpdateTunables {
7477            display_id,
7478            tunables: t,
7479        })
7480        .map_err(|e| {
7481            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7482                "Failed to update tunables: {}",
7483                e
7484            ))
7485        })?;
7486        Ok(())
7487    }
7488}
7489
7490/// The optional `display_id` attribute on a settings object (absent/invalid = 0, the
7491/// primary display).
7492fn read_display_id(settings: &Bound<'_, PyAny>) -> u32 {
7493    settings
7494        .getattr("display_id")
7495        .ok()
7496        .and_then(|v| v.extract::<u32>().ok())
7497        .unwrap_or(0)
7498}
7499
7500use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering};
7501use std::sync::{Condvar, Mutex, OnceLock};
7502
7503use crate::encoders::software::{EncodedStripe, FrameTiming};
7504
7505/// Let Python wrap already-encoded bytes back into a `StripeFrame`, for callers that produce or
7506/// replay stripe data outside a live capture (tests, re-sends to a late joiner). It copies the
7507/// buffer-like input (bytes/bytearray/memoryview) in because the frame owns its bytes; the capture
7508/// hot path instead uses `new_owned_meta` to MOVE the encoder's buffer with no copy, so this
7509/// copying constructor stays off that path.
7510#[pyfunction]
7511#[pyo3(signature = (data, data_type = 0, stripe_y_start = 0, stripe_height = 0, frame_id = 0))]
7512fn stripe_frame_from_buffer(
7513    data: Vec<u8>,
7514    data_type: i32,
7515    stripe_y_start: i32,
7516    stripe_height: i32,
7517    frame_id: i32,
7518) -> StripeFrame {
7519    StripeFrame::new_owned_meta(
7520        Arc::new(data),
7521        data_type,
7522        stripe_y_start,
7523        stripe_height,
7524        frame_id,
7525        FrameTiming::default(),
7526        Reference::Untracked,
7527    )
7528}
7529
7530/// Capture configuration read by `start_capture` (each field by attribute name via
7531/// `extract_settings`, so the field names must match exactly). Declared `dict` so callers
7532/// can stash extra attributes not listed here.
7533#[pyclass(dict)]
7534struct CaptureSettings {
7535    /// Wayland display key this capture binds to (0 = primary output); ignored on X11.
7536    #[pyo3(get, set)]
7537    display_id: u32,
7538    #[pyo3(get, set)]
7539    capture_width: i32,
7540    #[pyo3(get, set)]
7541    capture_height: i32,
7542    #[pyo3(get, set)]
7543    scale: f64,
7544    #[pyo3(get, set)]
7545    capture_x: i32,
7546    #[pyo3(get, set)]
7547    capture_y: i32,
7548    #[pyo3(get, set)]
7549    target_fps: f64,
7550    #[pyo3(get, set)]
7551    jpeg_quality: i32,
7552    #[pyo3(get, set)]
7553    paint_over_jpeg_quality: i32,
7554    #[pyo3(get, set)]
7555    use_paint_over_quality: bool,
7556    #[pyo3(get, set)]
7557    paint_over_trigger_frames: i32,
7558    #[pyo3(get, set)]
7559    damage_block_threshold: i32,
7560    #[pyo3(get, set)]
7561    damage_block_duration: i32,
7562    /// The codec: "jpeg" (striped stills), "h264" (striped, or full-frame with
7563    /// `video_fullframe`), or a full-frame video codec "h265", "vp8", "vp9", "av1".
7564    #[pyo3(get, set)]
7565    codec: String,
7566    #[pyo3(get, set)]
7567    video_crf: i32,
7568    #[pyo3(get, set)]
7569    video_paintover_crf: i32,
7570    #[pyo3(get, set)]
7571    video_paintover_burst_frames: i32,
7572    #[pyo3(get, set)]
7573    video_fullcolor: bool,
7574    /// Bits per sample, 8 or 10: 10 where an encoder of the codec carries it (VA-API and the
7575    /// software encoders for H.265, VP9, and AV1, x264 for H.264), in software where the
7576    /// engine does not, and 8 elsewhere.
7577    #[pyo3(get, set)]
7578    video_bit_depth: i32,
7579    /// H.264 only: encode whole frames instead of stripes. Other video codecs are always
7580    /// full-frame, and so is a hardware H.264 session.
7581    #[pyo3(get, set)]
7582    video_fullframe: bool,
7583    #[pyo3(get, set)]
7584    video_streaming_mode: bool,
7585    #[pyo3(get, set)]
7586    capture_cursor: bool,
7587    #[pyo3(get, set)]
7588    watermark_path: Py<PyAny>,
7589    #[pyo3(get, set)]
7590    watermark_location_enum: i32,
7591    #[pyo3(get, set)]
7592    encode_node_index: i32,
7593    #[pyo3(get, set)]
7594    use_cpu: bool,
7595    #[pyo3(get, set)]
7596    debug_logging: bool,
7597    #[pyo3(get, set)]
7598    video_cbr_mode: bool,
7599    #[pyo3(get, set)]
7600    video_bitrate_kbps: i32,
7601    #[pyo3(get, set)]
7602    video_vbv_multiplier: f64,
7603    #[pyo3(get, set)]
7604    keyframe_interval_s: f64,
7605    #[pyo3(get, set)]
7606    video_min_qp: i32,
7607    #[pyo3(get, set)]
7608    video_max_qp: i32,
7609    #[pyo3(get, set)]
7610    auto_adjust_screen_capture_size: bool,
7611    /// Emit the bare encoded payload without the wire header; with H.264 and `video_fullframe`
7612    /// each frame is one Annex-B access unit and its metadata is on the `StripeFrame` attributes.
7613    #[pyo3(get, set)]
7614    omit_stripe_headers: bool,
7615    /// The consumer acknowledges the frames every client holds (`acknowledge_reference`): an
7616    /// NVENC anchor predicts from a frame every client was sent, and where the session keeps two
7617    /// long-term references (H.265, and AV1, which keeps one otherwise) the newest every client
7618    /// holds stays pinned, so each client recovers on its own from a loss of any depth. H.264
7619    /// keeps one.
7620    #[pyo3(get, set)]
7621    acknowledge_references: bool,
7622    /// The most frames an H.264 or H.265 stream keeps for reference, 0 for the level's: a
7623    /// decoder that holds its whole buffer before showing a picture shows each that much later.
7624    #[pyo3(get, set)]
7625    video_reference_frames: u32,
7626    #[pyo3(get, set)]
7627    encode_node_path: Py<PyAny>,
7628    /// Compositor render node (Wayland): an explicit path wins; empty with auto_gpu
7629    /// set lets the library pick one; empty without falls back to the encoder node.
7630    #[pyo3(get, set)]
7631    render_node_path: Py<PyAny>,
7632    /// Auto-GPU request: "" = off, "true" = first GPU, any other token = first GPU
7633    /// whose kernel identity matches (vendor name, driver name, DT prefix, PCI id).
7634    #[pyo3(get, set)]
7635    auto_gpu: Py<PyAny>,
7636    /// Backend choice: True/False force Wayland/X11; None follows WAYLAND_DISPLAY.
7637    #[pyo3(get, set)]
7638    use_wayland: Py<PyAny>,
7639    /// H.264 recording tap: a Unix socket path to bind, or empty for none.
7640    #[pyo3(get, set)]
7641    recording_socket: Py<PyAny>,
7642    /// Wayland display of an EXTERNAL compositor to capture (host-capture mode).
7643    #[pyo3(get, set)]
7644    wayland_host_display: Py<PyAny>,
7645    /// Compositor cursor-theme size in pixels; <=0 keeps the theme default (24).
7646    #[pyo3(get, set)]
7647    cursor_size: i32,
7648    /// Longest cursor edge the X11 out-of-band cursor callback delivers; larger
7649    /// images are downscaled. <=0 disables the cap.
7650    #[pyo3(get, set)]
7651    cursor_size_cap: i32,
7652}
7653
7654#[pymethods]
7655impl CaptureSettings {
7656    #[new]
7657    fn new(py: Python<'_>) -> Self {
7658        Self {
7659            display_id: 0,
7660            capture_width: 1920,
7661            capture_height: 1080,
7662            scale: 1.0,
7663            capture_x: 0,
7664            capture_y: 0,
7665            target_fps: 60.0,
7666            jpeg_quality: 85,
7667            paint_over_jpeg_quality: 95,
7668            use_paint_over_quality: false,
7669            paint_over_trigger_frames: 10,
7670            damage_block_threshold: 15,
7671            damage_block_duration: 30,
7672            codec: "jpeg".to_string(),
7673            video_crf: 25,
7674            video_paintover_crf: 18,
7675            video_paintover_burst_frames: 5,
7676            video_fullcolor: false,
7677            video_bit_depth: 8,
7678            video_fullframe: false,
7679            video_streaming_mode: false,
7680            capture_cursor: false,
7681            watermark_path: py.None(),
7682            watermark_location_enum: 0,
7683            encode_node_index: -2,
7684            use_cpu: false,
7685            debug_logging: false,
7686            video_cbr_mode: false,
7687            video_bitrate_kbps: 4000,
7688            video_vbv_multiplier: 0.0,
7689            keyframe_interval_s: 0.0,
7690            video_min_qp: 0,
7691            video_max_qp: 0,
7692            auto_adjust_screen_capture_size: false,
7693            omit_stripe_headers: false,
7694            acknowledge_references: false,
7695            video_reference_frames: 0,
7696            encode_node_path: py.None(),
7697            render_node_path: py.None(),
7698            auto_gpu: py.None(),
7699            use_wayland: py.None(),
7700            recording_socket: py.None(),
7701            wayland_host_display: py.None(),
7702            cursor_size: -1,
7703            cursor_size_cap: 128,
7704        }
7705    }
7706}
7707
7708/// Process-wide Wayland backend: input and capture share ONE compositor (constructed lazily).
7709static WAYLAND_BACKEND: OnceLock<Mutex<Option<Py<WaylandBackend>>>> = OnceLock::new();
7710/// The compositor's auto-picked socket name (e.g. "wayland-1"), published by the compositor
7711/// thread once its listening socket exists. `ListeningSocketSource::new_auto` binds the first
7712/// FREE wayland-N, which need not match any configured index — consumers must read the real
7713/// name from here instead of assuming one.
7714static WAYLAND_SOCKET_NAME: Mutex<Option<String>> = Mutex::new(None);
7715static WAYLAND_SOCKET_CV: Condvar = Condvar::new();
7716
7717fn publish_socket_name(name: &str) {
7718    *WAYLAND_SOCKET_NAME.lock().unwrap() = Some(name.to_string());
7719    WAYLAND_SOCKET_CV.notify_all();
7720}
7721
7722/// Wait (bounded) for the compositor thread to publish its socket name.
7723fn wait_socket_name(timeout: Duration) -> Option<String> {
7724    let deadline = Instant::now() + timeout;
7725    let mut g = WAYLAND_SOCKET_NAME.lock().unwrap();
7726    loop {
7727        if let Some(name) = g.as_ref() {
7728            return Some(name.clone());
7729        }
7730        let now = Instant::now();
7731        if now >= deadline {
7732            return None;
7733        }
7734        let (gg, _) = WAYLAND_SOCKET_CV.wait_timeout(g, deadline - now).unwrap();
7735        g = gg;
7736    }
7737}
7738/// Cursor callback registered before the backend exists (selkies registers it pre-start);
7739/// applied when the backend is created, which is deferred to capture start so the real
7740/// render node (not a placeholder) reaches the compositor.
7741static PENDING_CURSOR_CALLBACK: Mutex<Option<Py<PyAny>>> = Mutex::new(None);
7742/// Every capture's own cursor callback, the latest registration last: the process-wide slots
7743/// carry the latest one's, so a capture withdrawing its own (a second display leaving) hands
7744/// the cursor back to the capture registered before it.
7745static CURSOR_REGISTRATIONS: Mutex<Vec<(u64, Py<PyAny>)>> = Mutex::new(Vec::new());
7746/// Interpreter-teardown gate, set by the atexit sweep: the detached compositor and delivery
7747/// threads must never attach to a finalizing interpreter (aborts the process pre-3.13).
7748/// Cleared by a fresh capture start (only a live interpreter can start one).
7749pub(crate) static PY_SHUTDOWN: AtomicBool = AtomicBool::new(false);
7750/// Per-display capture ownership: display id -> the ScreenCapture id that owns that
7751/// display's capture. Only the owner may stop it, so an input-only or stale instance can't
7752/// tear down a live capture.
7753static WAYLAND_OWNERS: OnceLock<Mutex<std::collections::HashMap<u32, u64>>> = OnceLock::new();
7754
7755fn wayland_owners() -> &'static Mutex<std::collections::HashMap<u32, u64>> {
7756    WAYLAND_OWNERS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
7757}
7758
7759/// Display ids whose capture pipeline is actually running (StartCapture inserts,
7760/// StopCapture/DestroyOutput remove), so the Python-facing is_capturing() reports pipeline
7761/// liveness, not merely which ScreenCapture owns the backend.
7762static WAYLAND_ALIVE_DISPLAYS: OnceLock<Mutex<std::collections::HashSet<u32>>> = OnceLock::new();
7763
7764fn wayland_alive() -> &'static Mutex<std::collections::HashSet<u32>> {
7765    WAYLAND_ALIVE_DISPLAYS.get_or_init(|| Mutex::new(std::collections::HashSet::new()))
7766}
7767/// Per-display outcome of the most recent capture start, recorded by the calloop thread
7768/// so the Python-facing `capture_state` can report it. A start that came up clean removes
7769/// its entry; a hard failure (no output, ...) or a caveat that still capturing (host
7770/// connect failed -> local compositing, hardware encoder fell back to CPU) leaves the
7771/// reason here. Distinct from `wayland_alive`, which says only whether the pipeline runs.
7772static WAYLAND_CAPTURE_ERR: OnceLock<Mutex<std::collections::HashMap<u32, String>>> =
7773    OnceLock::new();
7774
7775fn wayland_capture_err() -> &'static Mutex<std::collections::HashMap<u32, String>> {
7776    WAYLAND_CAPTURE_ERR.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
7777}
7778
7779/// Record (or clear, with `None`) the last-start outcome for `display_id`.
7780static WAYLAND_ACTIVE_CODEC: OnceLock<Mutex<std::collections::HashMap<u32, u32>>> = OnceLock::new();
7781
7782/// The codec each Wayland display's capture streams, as `Codec::id`, read by
7783/// `ScreenCapture::active_codec` without a round trip to the compositor thread.
7784fn wayland_active_codec() -> &'static Mutex<std::collections::HashMap<u32, u32>> {
7785    WAYLAND_ACTIVE_CODEC.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
7786}
7787
7788fn set_wayland_active_codec(display_id: u32, codec: Option<Codec>) {
7789    let mut map = wayland_active_codec().lock().unwrap();
7790    match codec {
7791        Some(c) => {
7792            map.insert(display_id, c.id());
7793        }
7794        None => {
7795            map.remove(&display_id);
7796        }
7797    }
7798}
7799
7800fn set_wayland_capture_err(display_id: u32, err: Option<String>) {
7801    let mut map = wayland_capture_err().lock().unwrap();
7802    match err {
7803        Some(e) => {
7804            map.insert(display_id, e);
7805        }
7806        None => {
7807            map.remove(&display_id);
7808        }
7809    }
7810}
7811/// Hands each `ScreenCapture` a unique, monotonic id — the token `WAYLAND_OWNERS` compares to
7812/// decide which instance is allowed to stop a display's capture. Starts at 1 so 0 can
7813/// mean "no owner".
7814static NEXT_CAPTURE_ID: AtomicU64 = AtomicU64::new(1);
7815/// Registry of every live X11 capture's `Controls`, so the atexit sweep can flag them all to
7816/// stop before the interpreter finalizes even after the owning `ScreenCapture` Python handles have
7817/// been dropped — without a central registry those captures would have no reachable stop switch.
7818static LIVE_X11: OnceLock<Mutex<Vec<Arc<crate::x11::Controls>>>> = OnceLock::new();
7819
7820fn live_x11() -> &'static Mutex<Vec<Arc<crate::x11::Controls>>> {
7821    LIVE_X11.get_or_init(|| Mutex::new(Vec::new()))
7822}
7823
7824/// Convert premultiplied-alpha RGBA pixels to the straight alpha PNG expects. Every cursor
7825/// source feeding the callback stores premultiplied color (XFixes and Xcursor by format
7826/// definition, wl_shm/dmabuf by Wayland convention); encoding those values as straight
7827/// alpha double-darkens antialiased edges. Rounds as `(c * 255 + a/2) / a`, clamped —
7828/// selkies' python seed path mirrors this exact integer math so both sources hash a cursor
7829/// to the same content handle.
7830pub(crate) fn unpremultiply_rgba(image: &mut image::RgbaImage) {
7831    for p in image.pixels_mut() {
7832        let a = p.0[3] as u32;
7833        if a == 0 {
7834            p.0 = [0, 0, 0, 0];
7835        } else if a < 255 {
7836            for c in &mut p.0[..3] {
7837                *c = ((*c as u32 * 255 + a / 2) / a).min(255) as u8;
7838            }
7839        }
7840    }
7841}
7842
7843/// Best-effort nice boost for the calling capture/encode/delivery thread. These threads
7844/// compete with the very workload being captured, so a scheduling edge keeps frame pacing
7845/// steady under load. Requires CAP_SYS_NICE (or root); otherwise EPERM and silently a no-op.
7846/// How long a stop waits for a capture or delivery thread to end before leaving it behind.
7847const STOP_JOIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
7848
7849/// Join `handle` if it ends within `STOP_JOIN_TIMEOUT`, else leave the thread behind with one
7850/// line saying so: a thread held inside a driver call that never returns must not hold the
7851/// stop, and with it every later capture of the session, with nothing said.
7852pub(crate) fn join_within(handle: thread::JoinHandle<()>, what: &str) {
7853    let deadline = std::time::Instant::now() + STOP_JOIN_TIMEOUT;
7854    while !handle.is_finished() {
7855        if std::time::Instant::now() >= deadline {
7856            eprintln!(
7857                "[X11] the {what} thread did not end within {} s; it is left behind",
7858                STOP_JOIN_TIMEOUT.as_secs()
7859            );
7860            return;
7861        }
7862        thread::sleep(std::time::Duration::from_millis(10));
7863    }
7864    let _ = handle.join();
7865}
7866
7867#[cfg(test)]
7868mod join_tests {
7869    use super::*;
7870
7871    /// A thread that ends is joined; one that does not is left behind once the bound passes.
7872    #[test]
7873    fn join_within_is_bounded() {
7874        let quick = thread::spawn(|| {});
7875        let started = std::time::Instant::now();
7876        join_within(quick, "quick");
7877        assert!(started.elapsed() < STOP_JOIN_TIMEOUT);
7878        let (tx, rx) = std::sync::mpsc::channel::<()>();
7879        let stuck = thread::spawn(move || {
7880            let _ = rx.recv();
7881        });
7882        let started = std::time::Instant::now();
7883        join_within(stuck, "stuck");
7884        assert!(started.elapsed() >= STOP_JOIN_TIMEOUT);
7885        drop(tx);
7886    }
7887}
7888
7889/// Give the calling thread the nice level `nice`, below the default. `setpriority` takes it
7890/// only with CAP_SYS_NICE or an RLIMIT_NICE that reaches it, which a session's process rarely
7891/// has; where it refuses, the request goes to rtkit over the system bus, which grants a
7892/// level within the daemon's own bounds to any process of the seat. A host without rtkit,
7893/// or without a system bus at all as a container is, leaves the thread at the default, said
7894/// once in the log.
7895pub(crate) fn boost_thread_priority(nice: libc::c_int) {
7896    static RTKIT: OnceLock<zbus::Result<zbus::blocking::Connection>> = OnceLock::new();
7897    static REFUSED: std::sync::Once = std::sync::Once::new();
7898    let tid = unsafe { libc::syscall(libc::SYS_gettid) };
7899    if unsafe { libc::setpriority(libc::PRIO_PROCESS, tid as libc::id_t, nice) } == 0 {
7900        return;
7901    }
7902    let granted = RTKIT
7903        .get_or_init(zbus::blocking::Connection::system)
7904        .as_ref()
7905        .map_err(|e| e.to_string())
7906        .and_then(|conn| {
7907            zbus::blocking::Proxy::new(
7908                conn,
7909                "org.freedesktop.RealtimeKit1",
7910                "/org/freedesktop/RealtimeKit1",
7911                "org.freedesktop.RealtimeKit1",
7912            )
7913            .and_then(|rtkit| rtkit.call_method("MakeThreadHighPriority", &(tid as u64, nice)))
7914            .map(|_| ())
7915            .map_err(|e| e.to_string())
7916        });
7917    if let Err(why) = granted {
7918        REFUSED.call_once(|| {
7919            eprintln!("[pixelflux] Thread priority {nice} refused and rtkit does not grant it; capture threads run at normal priority.");
7920            crate::log::debug!("[pixelflux] rtkit: {why}");
7921        });
7922    }
7923}
7924
7925/// Forward a live rate change to the shared Wayland backend (no-op if none is running).
7926fn wayland_update_rate(
7927    py: Python<'_>,
7928    display_id: u32,
7929    bitrate_kbps: Option<i32>,
7930    vbv_multiplier: Option<f64>,
7931    fps: Option<f64>,
7932) {
7933    if let Some(slot) = WAYLAND_BACKEND.get()
7934        && let Some(be) = slot.lock().unwrap().as_ref()
7935    {
7936        let _ = be
7937            .bind(py)
7938            .borrow()
7939            .update_rate(bitrate_kbps, vbv_multiplier, fps, display_id);
7940    }
7941}
7942
7943/// Forward live per-frame tunables to the shared Wayland backend (no-op if none is running).
7944fn wayland_update_tunables(py: Python<'_>, display_id: u32, t: LiveTunables) {
7945    if let Some(slot) = WAYLAND_BACKEND.get()
7946        && let Some(be) = slot.lock().unwrap().as_ref()
7947    {
7948        let _ = be.bind(py).borrow().update_tunables(display_id, t);
7949    }
7950}
7951
7952/// Get-or-create the singleton Wayland backend (idempotent: the first dimensions and render
7953/// node win, and a later capture just resizes).
7954///
7955/// Called from capture start (which knows the operator's real node) and from the import-time
7956/// bootstrap when the deployment opts into pixelflux-as-compositor. The render node is chosen once,
7957/// at creation, by precedence: an explicit `render_node_path`, then an `auto_gpu` pick, then the
7958/// encoder node (so a caller that sets only one node still renders on that GPU); empty selects the
7959/// software renderer.
7960fn ensure_wayland_backend(
7961    py: Python<'_>,
7962    width: i32,
7963    height: i32,
7964    explicit_node: String,
7965    auto_gpu: String,
7966    fallback_node: String,
7967    cursor_size: i32,
7968) -> PyResult<Py<WaylandBackend>> {
7969    let slot = WAYLAND_BACKEND.get_or_init(|| Mutex::new(None));
7970    let mut g = slot.lock().unwrap();
7971    if g.is_none() {
7972        let mut node = (!explicit_node.is_empty()).then_some(explicit_node);
7973        let mut auto_gpu_selected = false;
7974        if node.is_none()
7975            && let Some(request) = parse_auto_gpu(&auto_gpu)
7976        {
7977            match auto_select_render_node(request.as_deref()) {
7978                Some(picked) => {
7979                    println!("[Wayland] AUTO_GPU selected {picked}.");
7980                    node = Some(picked);
7981                    auto_gpu_selected = true;
7982                }
7983                None => {
7984                    if let Some(token) = request {
7985                        eprintln!("[pixelflux] AUTO_GPU={token}: no matching GPU found.");
7986                    }
7987                }
7988            }
7989        }
7990        let node = node.unwrap_or(fallback_node);
7991        let be = Py::new(
7992            py,
7993            WaylandBackend::new(width, height, node, auto_gpu_selected, cursor_size),
7994        )?;
7995        if let Some(cb) = PENDING_CURSOR_CALLBACK.lock().unwrap().take() {
7996            let _ = be.bind(py).borrow().set_cursor_callback(Some(cb));
7997        }
7998        *g = Some(be);
7999    }
8000    Ok(g.as_ref().unwrap().clone_ref(py))
8001}
8002
8003/// The live Wayland backend, if any — never creates one. The pre-capture entry points
8004/// (input injection, cursor/config setters) use this so they can't lock in a backend
8005/// with a placeholder render node.
8006fn wayland_backend_running(py: Python<'_>) -> Option<Py<WaylandBackend>> {
8007    let slot = WAYLAND_BACKEND.get()?;
8008    let g = slot.lock().unwrap();
8009    g.as_ref().map(|b| b.clone_ref(py))
8010}
8011
8012/// Backend choice: an explicit `use_wayland` bool in the settings wins (selkies
8013/// forwards --wayland / SELKIES_WAYLAND there); when left unset (None), capture
8014/// goes through Wayland exactly when the session exposes a WAYLAND_DISPLAY.
8015fn want_wayland(settings: &Bound<'_, PyAny>) -> bool {
8016    if let Some(explicit) = settings
8017        .getattr("use_wayland")
8018        .ok()
8019        .and_then(|v| v.extract::<bool>().ok())
8020    {
8021        return explicit;
8022    }
8023    std::env::var("WAYLAND_DISPLAY")
8024        .map(|v| !v.is_empty())
8025        .unwrap_or(false)
8026}
8027
8028/// Mutable per-capture state behind `ScreenCapture`'s mutex: the active backend, the live
8029/// X11 controls and thread handle, and the capture / encode thread ids used to detect a re-entrant
8030/// stop.
8031struct ScState {
8032    /// 0 = idle, 1 = X11, 2 = Wayland.
8033    backend: u8,
8034    /// This capture holds one reference on the shared X11 cursor monitor. Set in
8035    /// the same locked section as `backend = 1` and TAKEN in the same locked
8036    /// section a stop reads the backend, so acquire/release pair exactly per
8037    /// capture — inferring the reference from `backend` alone would let a stop
8038    /// that interleaves with a start release a reference not yet taken (leaking
8039    /// the monitor once the acquire lands). The GIL happens to serialize that
8040    /// window today; the pairing must not depend on it.
8041    cursor_ref: bool,
8042    controls: Option<Arc<crate::x11::Controls>>,
8043    /// The controls of an X11 capture whose threads already run while `start_capture` has not
8044    /// published it: its first frames can reach a consumer before the start returns, and what
8045    /// that consumer asks for in reply (a lost frame, a key frame, a rate) has to reach the
8046    /// capture. A stop or a newer start in that window ends that capture, which is then never
8047    /// published.
8048    starting: Option<Arc<crate::x11::Controls>>,
8049    handle: Option<thread::JoinHandle<()>>,
8050    cap_thread_id: Option<thread::ThreadId>,
8051    /// The internal encode thread's id, so a re-entrant stop arriving on it is
8052    /// detected and doesn't try to join a chain that includes itself.
8053    encode_thread_id: Option<thread::ThreadId>,
8054    /// Handshake receiver kept when the bounded start-time wait for the encode thread id
8055    /// lapsed (slow X11 setup precedes the encode-thread spawn): the id is late-resolved
8056    /// from here on demand, so the re-entrant-stop guard still recognizes the encode
8057    /// thread — with `encode_thread_id` stuck at `None`, a stop from inside the delivery
8058    /// callback would join the capture thread, which joins the encode thread (the
8059    /// caller), a deadlock cycle. The id send strictly precedes any callback running on
8060    /// that thread, so a `try_recv` at stop time cannot miss it.
8061    encode_tid_rx: Option<std::sync::mpsc::Receiver<thread::ThreadId>>,
8062    /// Delivery thread: owns the GIL-bound Python callback so encode(N+1) never
8063    /// serializes behind deliver(N). Joined on stop; a re-entrant stop from
8064    /// inside the callback (which runs on this thread) must detach instead of
8065    /// self-joining.
8066    deliver_handle: Option<thread::JoinHandle<()>>,
8067    deliver_thread_id: Option<thread::ThreadId>,
8068    /// The Wayland display id this instance's capture is bound to (backend == 2).
8069    wl_display: u32,
8070    /// The X11 capture thread's exit error (backend == 1): the thread records why it died
8071    /// mid-run here, so `capture_state` can report it instead of the error scrolling past in
8072    /// a single stderr line. `None` while healthy.
8073    err: Option<Arc<Mutex<Option<String>>>>,
8074}
8075
8076impl ScState {
8077    /// The X11 controls a request reaches: the published capture's, else those of one still
8078    /// starting.
8079    fn x11_controls(&self) -> Option<Arc<crate::x11::Controls>> {
8080        match self.backend {
8081            1 => self.controls.clone(),
8082            0 => self.starting.clone(),
8083            _ => None,
8084        }
8085    }
8086}
8087
8088/// Unified capture handle exposed to Python. Drives the X11 capture directly or delegates to the
8089/// shared Wayland backend, chosen at `start_capture` time. Exposes start_capture / stop_capture /
8090/// request_idr_frame / update_* / is_capturing, plus the Wayland input-injection methods.
8091#[pyclass]
8092struct ScreenCapture {
8093    id: u64,
8094    inner: Mutex<ScState>,
8095}
8096
8097impl ScreenCapture {
8098    /// Hand what a client said of a frame to the capture: the X11 controls, drained ahead of the
8099    /// next encode, or the shared Wayland backend.
8100    fn hand_report(&self, py: Python<'_>, report: encoders::reference::ReferenceReport) {
8101        use encoders::reference::ReferenceReport;
8102        let (backend, controls, did) = {
8103            let st = self.inner.lock().unwrap();
8104            (st.backend, st.x11_controls(), st.wl_display)
8105        };
8106        match backend {
8107            0 | 1 => {
8108                if let Some(c) = controls {
8109                    c.reference_reports.lock().unwrap().push(report);
8110                }
8111            }
8112            2 => {
8113                if let Some(slot) = WAYLAND_BACKEND.get()
8114                    && let Some(be) = slot.lock().unwrap().as_ref()
8115                {
8116                    let be = be.bind(py).borrow();
8117                    let _ = match report {
8118                        ReferenceReport::Lost(frame_id) => be.invalidate_reference(frame_id, did),
8119                        ReferenceReport::Held(frame_id) => {
8120                            be.acknowledge_reference(frame_id, did, true)
8121                        }
8122                        ReferenceReport::Sent(frame_id) => {
8123                            be.acknowledge_reference(frame_id, did, false)
8124                        }
8125                    };
8126                }
8127            }
8128            _ => {}
8129        }
8130    }
8131
8132    /// Stop this capture: signal the capture thread, drop the live controls, and join.
8133    ///
8134    /// The path forks on the backend. A **Wayland** capture only tells the shared compositor to
8135    /// stop when this instance still owns it — ownership is claimed-and-cleared atomically so a
8136    /// stale stop cannot tear down a capture another instance just started. An **X11** capture joins
8137    /// its capture thread (which also joins the encode thread) and then its deliver thread,
8138    /// releasing the GIL first because the deliver thread runs the Python callback and holding the
8139    /// GIL across the joins would deadlock. A re-entrant stop arriving on the capture, encode, or
8140    /// deliver thread cannot join itself, so it detaches and lets the threads exit on the stop flag.
8141    /// An X11 capture still starting is only signaled: its start joins it and publishes nothing.
8142    fn stop_internal(&self, py: Python<'_>) -> PyResult<()> {
8143        let (handle, deliver_handle, same_thread, backend, controls, wl_display, cursor_ref) = {
8144            let mut st = self.inner.lock().unwrap();
8145            if let Some(c) = &st.controls {
8146                c.stop.store(true, Ordering::Relaxed);
8147            }
8148            if let Some(c) = st.starting.take() {
8149                c.stop.store(true, Ordering::Relaxed);
8150            }
8151            let cur = Some(thread::current().id());
8152            if st.encode_thread_id.is_none()
8153                && let Some(rx) = st.encode_tid_rx.as_ref()
8154                && let Ok(id) = rx.try_recv()
8155            {
8156                st.encode_thread_id = Some(id);
8157            }
8158            let same = st.cap_thread_id == cur
8159                || st.encode_thread_id == cur
8160                || st.deliver_thread_id == cur;
8161            let controls = st.controls.take();
8162            let handle = st.handle.take();
8163            let deliver_handle = st.deliver_handle.take();
8164            let backend = st.backend;
8165            let cursor_ref = std::mem::take(&mut st.cursor_ref);
8166            let wl_display = st.wl_display;
8167            st.backend = 0;
8168            st.cap_thread_id = None;
8169            st.encode_thread_id = None;
8170            st.encode_tid_rx = None;
8171            st.deliver_thread_id = None;
8172            st.wl_display = 0;
8173            st.err = None;
8174            (
8175                handle,
8176                deliver_handle,
8177                same,
8178                backend,
8179                controls,
8180                wl_display,
8181                cursor_ref,
8182            )
8183        };
8184        if let Some(c) = &controls {
8185            live_x11().lock().unwrap().retain(|x| !Arc::ptr_eq(x, c));
8186        }
8187        if cursor_ref {
8188            crate::x11::cursor::release(py);
8189        }
8190        if backend == 2 {
8191            let did = wl_display;
8192            let owned = {
8193                let mut owners = wayland_owners().lock().unwrap();
8194                if owners.get(&did) == Some(&self.id) {
8195                    owners.remove(&did);
8196                    true
8197                } else {
8198                    false
8199                }
8200            };
8201            if owned
8202                && let Some(slot) = WAYLAND_BACKEND.get()
8203                && let Some(be) = slot.lock().unwrap().as_ref()
8204            {
8205                let _ = be.bind(py).borrow().stop_capture(did);
8206            }
8207        } else {
8208            if same_thread {
8209                // Detach: the threads exit on the stop flag once the callback returns.
8210                drop(handle);
8211                drop(deliver_handle);
8212            } else {
8213                py.detach(|| {
8214                    if let Some(h) = handle {
8215                        join_within(h, "capture");
8216                    }
8217                    // The capture thread's end drops the delivery sender, so the deliver
8218                    // thread drains its one queued frame and exits behind it.
8219                    if let Some(h) = deliver_handle {
8220                        join_within(h, "delivery");
8221                    }
8222                });
8223            }
8224        }
8225        Ok(())
8226    }
8227}
8228
8229impl ScreenCapture {
8230    /// The report of the capture this handle runs, whichever backend it is on.
8231    fn report(&self) -> Option<Arc<report::StreamReport>> {
8232        let st = self.inner.lock().unwrap();
8233        match st.backend {
8234            1 => st.controls.as_ref().map(|c| c.report.clone()),
8235            2 => report::wayland_reports()
8236                .lock()
8237                .unwrap()
8238                .get(&st.wl_display)
8239                .cloned(),
8240            _ => None,
8241        }
8242    }
8243}
8244
8245#[pymethods]
8246impl ScreenCapture {
8247    #[new]
8248    fn new() -> Self {
8249        Self {
8250            id: NEXT_CAPTURE_ID.fetch_add(1, Ordering::Relaxed),
8251            inner: Mutex::new(ScState {
8252                backend: 0,
8253                cursor_ref: false,
8254                controls: None,
8255                starting: None,
8256                handle: None,
8257                cap_thread_id: None,
8258                encode_thread_id: None,
8259                encode_tid_rx: None,
8260                deliver_handle: None,
8261                deliver_thread_id: None,
8262                wl_display: 0,
8263                err: None,
8264            }),
8265        }
8266    }
8267
8268    /// Begin capture: `callback(frame)` is invoked per encoded stripe with a `StripeFrame`.
8269    ///
8270    /// The backend is chosen from the settings (`want_wayland`). A **Wayland** start delegates to
8271    /// the shared backend, distinguishing the compositor RENDER node from the ENCODER node and
8272    /// resolving an AUTO_GPU request; restarting this instance's own live Wayland capture skips the
8273    /// stop so the calloop can reconfigure the running session in place — keeping a compatible NVENC
8274    /// session alive — instead of destroying it and forcing a full rebuild; it publishes the
8275    /// display id with the GIL held since the command, so no frame reaches Python before a
8276    /// control request can find the capture. An **X11** start resolves AUTO_GPU to an encoder
8277    /// device when none was chosen explicitly, then spawns the capture thread (which internally
8278    /// spawns the encode+deliver thread). Its first frames can reach `callback` before it returns,
8279    /// so the control methods reach the capture from the moment its threads spawn (`starting`).
8280    /// The per-frame delivery closure makes one GIL acquisition per frame with all stripes
8281    /// batched, and a failed start surfaces as a `PyErr` rather than a silent,
8282    /// forever-"capturing" state.
8283    fn start_capture(
8284        &self,
8285        py: Python<'_>,
8286        callback: Py<PyAny>,
8287        settings: &Bound<'_, PyAny>,
8288    ) -> PyResult<()> {
8289        let display_id = read_display_id(settings);
8290        let live_wayland_restart = want_wayland(settings)
8291            && {
8292                let st = self.inner.lock().unwrap();
8293                st.backend == 2 && st.wl_display == display_id
8294            }
8295            && wayland_owners().lock().unwrap().get(&display_id) == Some(&self.id)
8296            && wayland_alive().lock().unwrap().contains(&display_id);
8297        if !live_wayland_restart {
8298            self.stop_internal(py)?;
8299        }
8300        let rs = extract_settings(settings)?;
8301
8302        if want_wayland(settings) {
8303            let read_node = |attr: &str| -> Option<String> {
8304                settings.getattr(attr).ok().and_then(|o| {
8305                    o.extract::<String>()
8306                        .or_else(|_| {
8307                            o.extract::<Vec<u8>>()
8308                                .map(|b| String::from_utf8_lossy(&b).into_owned())
8309                        })
8310                        .ok()
8311                })
8312            };
8313            let cursor_size = settings
8314                .getattr("cursor_size")
8315                .ok()
8316                .and_then(|v| v.extract::<i32>().ok())
8317                .unwrap_or(-1);
8318            let be = ensure_wayland_backend(
8319                py,
8320                rs.width,
8321                rs.height,
8322                read_node("render_node_path").unwrap_or_default(),
8323                read_node("auto_gpu").unwrap_or_default(),
8324                read_node("encode_node_path").unwrap_or_default(),
8325                cursor_size,
8326            )?;
8327            be.bind(py).borrow().start_capture(callback, settings)?;
8328            wayland_owners().lock().unwrap().insert(display_id, self.id);
8329            {
8330                let mut st = self.inner.lock().unwrap();
8331                st.backend = 2;
8332                st.wl_display = display_id;
8333            }
8334            return Ok(());
8335        }
8336
8337        // A fresh start proves the interpreter is alive: clear the teardown flag the atexit
8338        // sweep may have set (the Wayland start path already does the same), or the delivery
8339        // thread below would drop every frame.
8340        PY_SHUTDOWN.store(false, Ordering::Relaxed);
8341        let mut rs = rs;
8342        if rs.encode_node_index < -1 {
8343            let auto_gpu = settings
8344                .getattr("auto_gpu")
8345                .ok()
8346                .and_then(|o| {
8347                    o.extract::<String>()
8348                        .or_else(|_| {
8349                            o.extract::<Vec<u8>>()
8350                                .map(|b| String::from_utf8_lossy(&b).into_owned())
8351                        })
8352                        .ok()
8353                })
8354                .unwrap_or_default();
8355            if let Some(picked) = auto_render_node(&auto_gpu)
8356                && let Some(idx) = render_node_index(&picked)
8357            {
8358                println!("[X11] AUTO_GPU selected {picked}.");
8359                rs.encode_node_index = idx;
8360            } else if let Some(idx) = first_render_node_index() {
8361                rs.encode_node_index = idx;
8362            }
8363        }
8364
8365        println!(
8366            "[X11] Configuring Output: {}x{} @ {:.2} FPS (Encode Node: {})",
8367            rs.width, rs.height, rs.target_fps, rs.encode_node_index
8368        );
8369
8370        let controls = Arc::new(crate::x11::Controls::new(&rs));
8371        crate::x11::cursor::set_size_cap(rs.cursor_size_cap);
8372        live_x11().lock().unwrap().push(controls.clone());
8373        if let Some(other) = self
8374            .inner
8375            .lock()
8376            .unwrap()
8377            .starting
8378            .replace(controls.clone())
8379        {
8380            other.stop.store(true, Ordering::Relaxed);
8381        }
8382        let c2 = controls.clone();
8383        let c3 = controls.clone();
8384        let cb = callback;
8385        let err_slot: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
8386        let err_slot2 = err_slot.clone();
8387
8388        // The Python callback is GIL-bound: run it on its own thread (as the
8389        // Wayland backend and pcmflux do) so encode(N+1) never serializes behind
8390        // deliver(N). Bounded at one in-flight frame: a slower consumer
8391        // backpressures the encoder by exactly one frame instead of growing a
8392        // queue, and no frame is ever dropped.
8393        let (deliver_tx, deliver_rx) = std::sync::mpsc::sync_channel::<Vec<EncodedStripe>>(1);
8394        let deliver_handle = thread::spawn(move || {
8395            crate::boost_thread_priority(-10);
8396            while let Ok(frame) = deliver_rx.recv() {
8397                if PY_SHUTDOWN.load(Ordering::Relaxed) {
8398                    continue;
8399                }
8400                Python::attach(|py| {
8401                    for s in frame {
8402                        match Py::new(
8403                            py,
8404                            StripeFrame::new_owned_meta(
8405                                s.data,
8406                                s.codec.data_type(),
8407                                s.stripe_y_start,
8408                                s.stripe_height,
8409                                s.frame_id,
8410                                s.timing,
8411                                s.reference,
8412                            ),
8413                        ) {
8414                            Ok(f) => {
8415                                if let Err(e) = cb.call1(py, (f,)) {
8416                                    e.print(py);
8417                                }
8418                            }
8419                            Err(e) => eprintln!("[X11] frame alloc error: {e:?}"),
8420                        }
8421                    }
8422                });
8423            }
8424        });
8425        let deliver_thread_id = deliver_handle.thread().id();
8426
8427        let report = controls.report.clone();
8428        let on_frame = move |frame: Vec<EncodedStripe>| {
8429            report.tally(&frame);
8430            // Blocks only when the single slot is still occupied (consumer more
8431            // than one frame behind); a dropped receiver (stop) discards.
8432            let _ = deliver_tx.send(frame);
8433        };
8434
8435        let (tid_tx, tid_rx) = std::sync::mpsc::channel();
8436        let (etid_tx, etid_rx) = std::sync::mpsc::channel();
8437        let handle = thread::spawn(move || {
8438            crate::boost_thread_priority(-15);
8439            let _ = tid_tx.send(thread::current().id());
8440            let res = crate::x11::run_capture(rs, c2, etid_tx, on_frame);
8441            c3.stop.store(true, Ordering::Release);
8442            if let Err(e) = res {
8443                let msg = e.to_string();
8444                eprintln!("[X11] capture error: {msg}");
8445                if let Ok(mut g) = err_slot2.lock() {
8446                    *g = Some(msg);
8447                }
8448            }
8449            // run_capture has joined its encode thread by now (NVENC/CUDA session dropped),
8450            // so the atexit sweep may finalize the interpreter without racing that drop.
8451            c3.finished.store(true, Ordering::Release);
8452        });
8453        let (tid, etid_res, etid_rx) = py.detach(move || {
8454            let tid = tid_rx.recv().ok();
8455            let etid_res = etid_rx.recv_timeout(std::time::Duration::from_secs(2));
8456            (tid, etid_res, etid_rx)
8457        });
8458        let mut late_etid_rx = None;
8459        let etid = match etid_res {
8460            Ok(id) => Some(id),
8461            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
8462                let _ = handle.join();
8463                live_x11()
8464                    .lock()
8465                    .unwrap()
8466                    .retain(|x| !Arc::ptr_eq(x, &controls));
8467                let mut st = self.inner.lock().unwrap();
8468                if !st
8469                    .starting
8470                    .as_ref()
8471                    .is_some_and(|c| Arc::ptr_eq(c, &controls))
8472                {
8473                    return Ok(());
8474                }
8475                st.starting = None;
8476                drop(st);
8477                let msg = err_slot
8478                    .lock()
8479                    .ok()
8480                    .and_then(|g| g.clone())
8481                    .unwrap_or_else(|| "X11 capture thread exited during start".to_string());
8482                return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(msg));
8483            }
8484            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
8485                // Slow pre-spawn X11 setup: the id will still arrive on this
8486                // channel. Keep the receiver so stop_internal can late-resolve
8487                // it — a None id would blind the re-entrant-stop guard.
8488                late_etid_rx = Some(etid_rx);
8489                None
8490            }
8491        };
8492        let mut st = self.inner.lock().unwrap();
8493        if !st
8494            .starting
8495            .as_ref()
8496            .is_some_and(|c| Arc::ptr_eq(c, &controls))
8497        {
8498            drop(st);
8499            live_x11()
8500                .lock()
8501                .unwrap()
8502                .retain(|x| !Arc::ptr_eq(x, &controls));
8503            py.detach(|| {
8504                join_within(handle, "capture");
8505                join_within(deliver_handle, "delivery");
8506            });
8507            return Ok(());
8508        }
8509        st.starting = None;
8510        // Take the monitor reference BEFORE publishing the capture: a stop that
8511        // observes this capture must always find the reference it is to release.
8512        crate::x11::cursor::acquire();
8513        st.backend = 1;
8514        st.cursor_ref = true;
8515        st.controls = Some(controls);
8516        st.handle = Some(handle);
8517        st.cap_thread_id = tid;
8518        st.encode_thread_id = etid;
8519        st.encode_tid_rx = late_etid_rx;
8520        st.deliver_handle = Some(deliver_handle);
8521        st.deliver_thread_id = Some(deliver_thread_id);
8522        st.err = Some(err_slot);
8523        drop(st);
8524        Ok(())
8525    }
8526
8527    /// The codec this capture streams, by name, once its pipeline has been built: the
8528    /// requested one, or the one the selection ladder demoted it to when no encoder could
8529    /// serve the request. None before the first frame or after a stop.
8530    fn active_codec(&self, py: Python<'_>) -> PyResult<Option<String>> {
8531        let (backend, controls, wl_display) = {
8532            let st = self.inner.lock().unwrap();
8533            (st.backend, st.controls.clone(), st.wl_display)
8534        };
8535        let id = match backend {
8536            1 => controls.map_or(u32::MAX, |c| c.codec.load(Ordering::Relaxed)),
8537            2 => py
8538                .detach(|| {
8539                    wayland_active_codec()
8540                        .lock()
8541                        .unwrap()
8542                        .get(&wl_display)
8543                        .copied()
8544                })
8545                .unwrap_or(u32::MAX),
8546            _ => u32::MAX,
8547        };
8548        Ok(Codec::from_id(id).map(|c| c.name().to_string()))
8549    }
8550
8551    /// What this capture streams and how it got there, or None before a start and after a
8552    /// stop: `backend` (`x11`, `wayland`), `capture` (the path: `NvFBC`, `DRI3`, `XShm`,
8553    /// `dmabuf`, `readback`), `zero_copy` and the `capture_reason` a faster path was declined
8554    /// for, `zero_copy_available` (whether the display server offered the encoder a zero-copy
8555    /// path at all), `encoder` (`NVENC`, `VAAPI`, or the software library), `hardware` and the
8556    /// `encoder_reason` it is not, `codec`, `fullcolor`, `striped`, and for a hardware session
8557    /// its `gpu`, kernel `driver`, and `encode_node`. A Wayland capture adds how its compositor
8558    /// renders: `renderer` (`gl`, `pixman`), `render_node`, `render_gpu`, `renderer_reason`.
8559    /// The first read of a VA-API session brings a GL context up once to name its GPU, so a
8560    /// caller with an event loop reads this off it.
8561    fn stream_info(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
8562        let Some(report) = self.report() else {
8563            return Ok(None);
8564        };
8565        let (info, renderer) = py.detach(|| {
8566            let mut info = report.info();
8567            let renderer = (info.backend == "wayland").then(report::renderer);
8568            if info.hardware && info.gpu.is_empty() {
8569                info.gpu = match &renderer {
8570                    Some(r) if render_node_index(&r.node) == Some(info.encode_node) => {
8571                        r.gpu.clone()
8572                    }
8573                    _ => node_gpu_name(info.encode_node),
8574                };
8575            }
8576            (info, renderer)
8577        });
8578        let d = pyo3::types::PyDict::new(py);
8579        d.set_item("backend", info.backend)?;
8580        d.set_item("capture", info.capture)?;
8581        d.set_item("zero_copy", info.zero_copy)?;
8582        d.set_item("zero_copy_available", info.zero_copy_available)?;
8583        d.set_item("capture_reason", &info.capture_reason)?;
8584        d.set_item("encoder", &info.encoder)?;
8585        d.set_item("hardware", info.hardware)?;
8586        d.set_item("encoder_reason", &info.encoder_reason)?;
8587        d.set_item("codec", info.codec)?;
8588        d.set_item("fullcolor", info.fullcolor)?;
8589        d.set_item("bit_depth", info.bit_depth)?;
8590        d.set_item("full_range", info.full_range)?;
8591        d.set_item("striped", info.stripes > 1)?;
8592        d.set_item("gpu", &info.gpu)?;
8593        d.set_item("driver", &info.driver)?;
8594        let encode_node = if info.hardware {
8595            format!("/dev/dri/renderD{}", 128 + info.encode_node)
8596        } else {
8597            String::new()
8598        };
8599        d.set_item("encode_node", encode_node)?;
8600        if let Some(r) = renderer {
8601            d.set_item("renderer", r.kind)?;
8602            d.set_item("render_node", &r.node)?;
8603            d.set_item("render_gpu", &r.gpu)?;
8604            d.set_item("renderer_reason", &r.reason)?;
8605        }
8606        Ok(Some(d.into_any().unbind()))
8607    }
8608
8609    /// Cumulative counters of this capture since it started, or None without one: `frames`
8610    /// and `bytes` delivered, and the nanoseconds those frames spent encoding (`encode_ns`)
8611    /// and from capture to the end of the encode (`pipeline_ns`). A caller differences two
8612    /// reads; a restart begins again from zero. Beside them, the extremes since the previous
8613    /// read, which each read starts again: `pipeline_min_ns` and `pipeline_max_ns`, zero where
8614    /// no frame was timed, and `frame_max_bytes`, the largest frame.
8615    fn stream_stats(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
8616        let Some(report) = self.report() else {
8617            return Ok(None);
8618        };
8619        let totals = report.totals();
8620        let peaks = report.take_peaks();
8621        let d = pyo3::types::PyDict::new(py);
8622        d.set_item("frames", totals.frames)?;
8623        d.set_item("bytes", totals.bytes)?;
8624        d.set_item("encode_ns", totals.encode_ns)?;
8625        d.set_item("pipeline_ns", totals.pipeline_ns)?;
8626        d.set_item("pipeline_min_ns", peaks.pipeline_min_ns)?;
8627        d.set_item("pipeline_max_ns", peaks.pipeline_max_ns)?;
8628        d.set_item("frame_max_bytes", peaks.frame_max_bytes)?;
8629        Ok(Some(d.into_any().unbind()))
8630    }
8631
8632    fn stop_capture(&self, py: Python<'_>) -> PyResult<()> {
8633        self.stop_internal(py)
8634    }
8635
8636    fn request_idr_frame(&self, py: Python<'_>) -> PyResult<()> {
8637        let (backend, controls, did) = {
8638            let st = self.inner.lock().unwrap();
8639            (st.backend, st.x11_controls(), st.wl_display)
8640        };
8641        match backend {
8642            0 | 1 => {
8643                if let Some(c) = controls {
8644                    c.force_idr.store(true, Ordering::Relaxed);
8645                }
8646            }
8647            2 => {
8648                if let Some(slot) = WAYLAND_BACKEND.get()
8649                    && let Some(be) = slot.lock().unwrap().as_ref()
8650                {
8651                    let _ = be.bind(py).borrow().request_idr_frame(did);
8652                }
8653            }
8654            _ => {}
8655        }
8656        Ok(())
8657    }
8658
8659    /// A client lost frame `frame_id`: the frames after it stop predicting from it, so the next
8660    /// one decodes there without a keyframe. An encoder that cannot leave a frame out of its
8661    /// predictions codes a keyframe instead. Non-blocking, like `request_idr_frame`.
8662    fn invalidate_reference(&self, py: Python<'_>, frame_id: u16) -> PyResult<()> {
8663        self.hand_report(py, encoders::reference::ReferenceReport::Lost(frame_id));
8664        Ok(())
8665    }
8666
8667    /// Every client holds frame `frame_id`, or where not `held` was sent it (and may yet lose
8668    /// it): a session keeping long-term references pins the newest anchor they all hold and
8669    /// predicts its anchors from a frame they were all sent, so each client recovers on its own
8670    /// from a loss of any depth, without a keyframe. Non-blocking, like `invalidate_reference`.
8671    #[pyo3(signature = (frame_id, held = true))]
8672    fn acknowledge_reference(&self, py: Python<'_>, frame_id: u16, held: bool) -> PyResult<()> {
8673        use encoders::reference::ReferenceReport;
8674        let report = if held {
8675            ReferenceReport::Held(frame_id)
8676        } else {
8677            ReferenceReport::Sent(frame_id)
8678        };
8679        self.hand_report(py, report);
8680        Ok(())
8681    }
8682
8683    /// Apply a live target-bitrate (kbps) change to the running capture.
8684    ///
8685    /// On the X11 path the dirty flag is Release-published after the payload store, so the encode
8686    /// thread's Acquire read can never observe the flag set against a stale bitrate.
8687    fn update_video_bitrate(&self, py: Python<'_>, kbps: i32) -> PyResult<()> {
8688        let (backend, controls, did) = {
8689            let st = self.inner.lock().unwrap();
8690            (st.backend, st.x11_controls(), st.wl_display)
8691        };
8692        match backend {
8693            0 | 1 => {
8694                if let Some(c) = &controls {
8695                    c.bitrate_kbps.store(kbps, Ordering::Relaxed);
8696                    c.rate_dirty.store(true, Ordering::Release);
8697                }
8698            }
8699            2 => wayland_update_rate(py, did, Some(kbps), None, None),
8700            _ => {}
8701        }
8702        Ok(())
8703    }
8704
8705    fn update_framerate(&self, py: Python<'_>, fps: f64) -> PyResult<()> {
8706        let (backend, controls, did) = {
8707            let st = self.inner.lock().unwrap();
8708            (st.backend, st.x11_controls(), st.wl_display)
8709        };
8710        match backend {
8711            0 | 1 => {
8712                if let Some(c) = &controls {
8713                    c.fps_bits.store(fps.max(1.0).to_bits(), Ordering::Relaxed);
8714                    c.rate_dirty.store(true, Ordering::Release);
8715                    py.detach(crate::x11::follow_frame_rate);
8716                }
8717            }
8718            2 => wayland_update_rate(py, did, None, None, Some(fps)),
8719            _ => {}
8720        }
8721        Ok(())
8722    }
8723
8724    /// Live CBR VBV change, as a multiple of one frame's bit budget (<= 0 = policy default).
8725    fn update_vbv_multiplier(&self, py: Python<'_>, multiplier: f64) -> PyResult<()> {
8726        let (backend, controls, did) = {
8727            let st = self.inner.lock().unwrap();
8728            (st.backend, st.x11_controls(), st.wl_display)
8729        };
8730        match backend {
8731            0 | 1 => {
8732                if let Some(c) = &controls {
8733                    c.vbv_mult_milli
8734                        .store((multiplier * 1000.0).round() as i32, Ordering::Relaxed);
8735                    c.rate_dirty.store(true, Ordering::Release);
8736                }
8737            }
8738            2 => wayland_update_rate(py, did, None, Some(multiplier), None),
8739            _ => {}
8740        }
8741        Ok(())
8742    }
8743
8744    /// Apply the live-tunable subset of `settings` (quality, paint-over, streaming mode,
8745    /// cursor overlay, keyframe interval) to the running capture -- no restart, no encoder
8746    /// re-init. Structural changes (encoder, chroma, RC mode, device) still need a restart.
8747    fn update_tunables(&self, py: Python<'_>, settings: &Bound<'_, PyAny>) -> PyResult<()> {
8748        let rs = extract_settings(settings)?;
8749        let t = LiveTunables::from_settings(&rs);
8750        let (backend, controls, did) = {
8751            let st = self.inner.lock().unwrap();
8752            (st.backend, st.x11_controls(), st.wl_display)
8753        };
8754        match backend {
8755            0 | 1 => {
8756                if let Some(c) = &controls {
8757                    c.capture_cursor.store(t.capture_cursor, Ordering::Relaxed);
8758                    *c.tunables.lock().unwrap() = Some(t);
8759                    c.tunables_dirty.store(true, Ordering::Release);
8760                    crate::x11::cursor::set_size_cap(rs.cursor_size_cap);
8761                }
8762            }
8763            2 => wayland_update_tunables(py, did, t),
8764            _ => {}
8765        }
8766        Ok(())
8767    }
8768
8769    /// Move/resize the live X11 capture region (root-relative). The capture loop drains
8770    /// in-flight frames, re-targets its surfaces, and the encoder follows in place where
8771    /// it can (NVENC reconfigure / stripe re-derive) -- no capture restart. `width`/
8772    /// `height` <= 0 mean "to the root edge". On Wayland the output IS the capture
8773    /// region: restart the capture with new dimensions instead (in-place there too).
8774    fn update_capture_region(&self, x: i32, y: i32, width: i32, height: i32) -> PyResult<()> {
8775        let controls = {
8776            let st = self.inner.lock().unwrap();
8777            if st.backend == 2 {
8778                return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
8779                    "update_capture_region is X11-only; on Wayland restart the capture with new dimensions",
8780                ));
8781            }
8782            st.x11_controls()
8783        };
8784        if let Some(c) = &controls {
8785            *c.region.lock().unwrap() = (x.max(0), y.max(0), width, height);
8786            c.region_dirty.store(true, Ordering::Release);
8787        }
8788        Ok(())
8789    }
8790
8791    #[getter]
8792    fn is_capturing(&self) -> bool {
8793        let st = self.inner.lock().unwrap();
8794        match st.backend {
8795            1 => st
8796                .controls
8797                .as_ref()
8798                .map(|c| !c.stop.load(Ordering::Relaxed))
8799                .unwrap_or(false),
8800            2 => {
8801                wayland_owners().lock().unwrap().get(&st.wl_display) == Some(&self.id)
8802                    && wayland_alive().lock().unwrap().contains(&st.wl_display)
8803            }
8804            _ => false,
8805        }
8806    }
8807
8808    fn inject_key(&self, py: Python<'_>, scancode: u32, state: u32) -> PyResult<()> {
8809        wayland_backend_running(py).map_or(Ok(()), |be| {
8810            be.bind(py).borrow().inject_key(scancode, state)
8811        })
8812    }
8813    /// Inject an ordered run of `(keycode, state)` events in one message.
8814    fn inject_keys(&self, py: Python<'_>, events: Vec<(u32, u32)>) -> PyResult<()> {
8815        wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_keys(events))
8816    }
8817    fn set_keymap_string(&self, py: Python<'_>, text: String) -> PyResult<()> {
8818        wayland_backend_running(py)
8819            .map_or(Ok(()), |be| be.bind(py).borrow().set_keymap_string(text))
8820    }
8821    /// Bind explicit `(keycode, keysym)` pairs onto the current base keymap in one swap;
8822    /// false when no backend runs or no base keymap is installed. See the backend method
8823    /// for the split of responsibility.
8824    fn set_keymap_overlay(&self, py: Python<'_>, binds: Vec<(u32, u32)>) -> PyResult<()> {
8825        wayland_backend_running(py)
8826            .map_or(Ok(()), |be| be.bind(py).borrow().set_keymap_overlay(binds))
8827    }
8828    /// Compositor apps run under (a nested labwc/kwin session pixelflux captures),
8829    /// the target for Computer-Use text injection and, under KWin, for relative
8830    /// pointer motion through its fake-input device; selkies resolves it and hands
8831    /// it over. Empty clears it. Stored process-wide since the CU server is per-process.
8832    fn set_app_wayland_display(&self, display: String) {
8833        crate::computer_use::set_app_wayland_display(if display.is_empty() {
8834            None
8835        } else {
8836            Some(display)
8837        });
8838    }
8839    /// Type `text` through `display`'s zwp_virtual_keyboard_manager_v1 as a one-shot
8840    /// client: selkies' text-injection path, targeting whichever compositor the apps
8841    /// live under (the nested session's, or pixelflux's own in a direct session).
8842    /// Blocking; releases the GIL for the duration. Raises
8843    /// [`VirtualKeyboardUnavailable`] when the compositor lacks the protocol.
8844    fn type_text_wayland(&self, py: Python<'_>, display: String, text: String) -> PyResult<()> {
8845        py.detach(move || {
8846            let path = crate::wayland::wlclient::socket_path(&display)
8847                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8848            crate::wayland::vkclient::type_text_to(&path, &text)
8849        })
8850        .map_err(|e: String| {
8851            if e.contains("zwp_virtual_keyboard_manager_v1") {
8852                VirtualKeyboardUnavailable::new_err(e)
8853            } else {
8854                pyo3::exceptions::PyRuntimeError::new_err(e)
8855            }
8856        })
8857    }
8858    /// Tap `keysyms` in order through `display`'s virtual keyboard, verbatim: the
8859    /// caller owns which keysym spells which character (selkies' policy layer);
8860    /// this owns delivery. Same one-shot client and errors as `type_text_wayland`.
8861    fn type_keysyms_wayland(
8862        &self,
8863        py: Python<'_>,
8864        display: String,
8865        keysyms: Vec<u32>,
8866    ) -> PyResult<()> {
8867        py.detach(move || {
8868            let path = crate::wayland::wlclient::socket_path(&display)
8869                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8870            crate::wayland::vkclient::type_keysyms_to(&path, &keysyms)
8871        })
8872        .map_err(|e: String| {
8873            if e.contains("zwp_virtual_keyboard_manager_v1") {
8874                VirtualKeyboardUnavailable::new_err(e)
8875            } else {
8876                pyo3::exceptions::PyRuntimeError::new_err(e)
8877            }
8878        })
8879    }
8880    /// Scale the app compositor's `index`-th screen, so its applications draw
8881    /// larger while the capture keeps its full resolution. False when that
8882    /// compositor manages no outputs for clients (KWin), whose scale follows
8883    /// the capture output's instead.
8884    #[pyo3(signature = (display, scale, index = 0))]
8885    fn set_app_output_scale(
8886        &self,
8887        py: Python<'_>,
8888        display: String,
8889        scale: f64,
8890        index: usize,
8891    ) -> PyResult<bool> {
8892        py.detach(move || {
8893            let path = crate::wayland::wlclient::socket_path(&display)
8894                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8895            crate::wayland::outclient::set_output_scale(&path, index, scale)
8896        })
8897        .map(|outcome| matches!(outcome, crate::wayland::outclient::ScaleOutcome::Applied))
8898        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
8899    }
8900    /// Give the app compositor's `index`-th screen this mode and scale in one
8901    /// configuration, so the session lays its desktop out once. Setting them
8902    /// separately exposes a geometry that never exists — the pre-connect mode at
8903    /// the new scale — and a client that does not lay out again keeps it.
8904    /// False = that compositor manages no outputs for clients: a nested KWin
8905    /// accepts a scale over its own output management and then ignores it, its
8906    /// screens taking their scale from the host window's preferred fractional
8907    /// scale, which is the capture output's.
8908    #[pyo3(signature = (display, width, height, scale, index = 0))]
8909    fn set_app_screen_geometry(
8910        &self,
8911        py: Python<'_>,
8912        display: String,
8913        width: i32,
8914        height: i32,
8915        scale: f64,
8916        index: usize,
8917    ) -> PyResult<bool> {
8918        py.detach(move || {
8919            let path = crate::wayland::wlclient::socket_path(&display)
8920                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8921            crate::wayland::outclient::set_screen_geometry(&path, index, (width, height), scale)
8922        })
8923        .map(|outcome| matches!(outcome, crate::wayland::outclient::ScaleOutcome::Applied))
8924        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
8925    }
8926    /// Lay the app compositor's screens out at `rects` — `(x, y, width, height)`
8927    /// per screen in screen order — so the session arranges its desktop the way
8928    /// the capture outputs were placed rather than by its own default rule.
8929    /// A fallback ladder: `zwlr_output_management_v1` first, then KWin's
8930    /// `kde_output_management_v2` (positions only there — a nested KWin
8931    /// screen's size follows its host window). Returns how many were
8932    /// positioned; 0 = that compositor manages no outputs for clients.
8933    fn set_app_screen_layout(
8934        &self,
8935        py: Python<'_>,
8936        display: String,
8937        rects: Vec<(i32, i32, i32, i32)>,
8938    ) -> PyResult<usize> {
8939        py.detach(move || {
8940            let path = crate::wayland::wlclient::socket_path(&display)
8941                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8942            let placed = crate::wayland::outclient::set_screen_layout(&path, rects.clone())?;
8943            if placed == 0 {
8944                crate::wayland::kdeclient::set_screen_layout(&path, rects)
8945            } else {
8946                Ok(placed)
8947            }
8948        })
8949        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
8950    }
8951    /// The app compositor's enabled screens as `(name, x, y, width, height)` in
8952    /// screen order, which is what it did with a layout and a mode rather than
8953    /// what it was asked for — the readback `set_app_screen_geometry` has no
8954    /// other way to be checked against. The same ladder as
8955    /// `set_app_screen_layout` (zwlr, then KWin's output devices). Empty =
8956    /// that compositor manages no outputs for clients.
8957    fn list_app_screens(
8958        &self,
8959        py: Python<'_>,
8960        display: String,
8961    ) -> PyResult<Vec<crate::wayland::AppScreen>> {
8962        py.detach(move || {
8963            let path = crate::wayland::wlclient::socket_path(&display)
8964                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8965            let screens = crate::wayland::outclient::list_screens(&path)?;
8966            if screens.is_empty() {
8967                crate::wayland::kdeclient::list_screens(&path)
8968            } else {
8969                Ok(screens)
8970            }
8971        })
8972        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
8973    }
8974    /// Whether the app compositor serves KWin's `zkde_screencast_unstable_v1`
8975    /// virtual outputs -- the rung selkies offers a second display on when the
8976    /// session serves no control socket. A registry read that touches no
8977    /// screen: whether the compositor registers the screen it grows is proven
8978    /// when a display asks for one (`add_app_screen` refuses one that does not).
8979    fn app_screen_control_offered(&self, py: Python<'_>, display: String) -> PyResult<bool> {
8980        py.detach(move || {
8981            let path = crate::wayland::wlclient::socket_path(&display)
8982                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8983            crate::wayland::kdeclient::screen_control_offered(&path)
8984        })
8985        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
8986    }
8987    /// Grow a screen named `name` on the app compositor, held open until
8988    /// `remove_app_screen`. The name is what `list_app_screens` reports and
8989    /// removal addresses; `width`/`height`/`scale` only seed the screen, whose
8990    /// size follows the capture output that adopts its host window.
8991    #[pyo3(signature = (display, name, width = 1920, height = 1080, scale = 1.0))]
8992    fn add_app_screen(
8993        &self,
8994        py: Python<'_>,
8995        display: String,
8996        name: String,
8997        width: i32,
8998        height: i32,
8999        scale: f64,
9000    ) -> PyResult<()> {
9001        py.detach(move || {
9002            let path = crate::wayland::wlclient::socket_path(&display)
9003                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
9004            crate::wayland::kdeclient::add_screen(&path, &name, (width, height), scale)
9005        })
9006        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9007    }
9008    /// Remove a screen `add_app_screen` grew; the compositor's own
9009    /// output-destroy path returns its windows to the primary. False when no
9010    /// such screen is held.
9011    fn remove_app_screen(&self, py: Python<'_>, name: String) -> PyResult<bool> {
9012        Ok(py.detach(move || crate::wayland::kdeclient::remove_screen(&name)))
9013    }
9014    /// Hold the app compositor's screens past the first `keep` at a small size,
9015    /// so a session that opened more screens than there are capture outputs does
9016    /// not lay its desktop out across one nobody sees. Returns how many were
9017    /// resized (0 = that compositor manages no outputs for clients).
9018    fn hold_spare_app_screens(
9019        &self,
9020        py: Python<'_>,
9021        display: String,
9022        keep: usize,
9023        width: i32,
9024        height: i32,
9025    ) -> PyResult<usize> {
9026        py.detach(move || {
9027            let path = crate::wayland::wlclient::socket_path(&display)
9028                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
9029            crate::wayland::outclient::hold_spare_screens(&path, keep, (width, height))
9030        })
9031        .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9032    }
9033    /// Mimes the app compositor's current selection offers (empty = nothing copied).
9034    fn clipboard_types_app(&self, py: Python<'_>, display: String) -> PyResult<Vec<String>> {
9035        py.detach(move || crate::wayland::dcclient::list_types(&app_socket_path(&display)?))
9036            .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9037    }
9038    /// The app compositor selection's payload for `mime`, or None when nothing is
9039    /// copied or the selection does not offer that mime.
9040    fn clipboard_read_app(
9041        &self,
9042        py: Python<'_>,
9043        display: String,
9044        mime: String,
9045    ) -> PyResult<Option<Py<pyo3::types::PyBytes>>> {
9046        let data = py
9047            .detach(move || crate::wayland::dcclient::read(&app_socket_path(&display)?, &mime))
9048            .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
9049        Ok(data.map(|d| pyo3::types::PyBytes::new(py, &d).unbind()))
9050    }
9051    /// Take the app compositor's selection, serving `entries` (mime, bytes) to
9052    /// every paster from a background thread until another client copies.
9053    fn clipboard_write_app(
9054        &self,
9055        py: Python<'_>,
9056        display: String,
9057        entries: Vec<(String, Vec<u8>)>,
9058    ) -> PyResult<()> {
9059        py.detach(move || crate::wayland::dcclient::write(&app_socket_path(&display)?, entries))
9060            .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9061    }
9062    /// Drop the app compositor's selection.
9063    fn clipboard_clear_app(&self, py: Python<'_>, display: String) -> PyResult<()> {
9064        py.detach(move || crate::wayland::dcclient::clear(&app_socket_path(&display)?))
9065            .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9066    }
9067    /// Invoke `callback(mimes: list[str])` from a background thread on every
9068    /// selection change in the app compositor (including the one current at call
9069    /// time), with no mimes for a cleared selection. A second watch for the same
9070    /// display replaces the first.
9071    fn clipboard_watch_app(
9072        &self,
9073        py: Python<'_>,
9074        display: String,
9075        callback: Py<PyAny>,
9076    ) -> PyResult<()> {
9077        py.detach(move || crate::wayland::dcclient::watch(&app_socket_path(&display)?, callback))
9078            .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9079    }
9080    /// Stop the selection watch for `display` (no-op without one).
9081    fn clipboard_unwatch_app(&self, py: Python<'_>, display: String) {
9082        let _ = py.detach(move || {
9083            crate::wayland::dcclient::unwatch(&app_socket_path(&display)?);
9084            Ok::<(), String>(())
9085        });
9086    }
9087    /// The move's number (`pointer_location`), or None without a running compositor.
9088    fn inject_mouse_move(&self, py: Python<'_>, x: f64, y: f64) -> PyResult<Option<u64>> {
9089        wayland_backend_running(py).map_or(Ok(None), |be| {
9090            be.bind(py).borrow().inject_mouse_move(x, y).map(Some)
9091        })
9092    }
9093    /// The move's number (`pointer_location`), or None without a running compositor.
9094    fn inject_relative_mouse_move(
9095        &self,
9096        py: Python<'_>,
9097        dx: f64,
9098        dy: f64,
9099    ) -> PyResult<Option<u64>> {
9100        wayland_backend_running(py).map_or(Ok(None), |be| {
9101            be.bind(py)
9102                .borrow()
9103                .inject_relative_mouse_move(dx, dy)
9104                .map(Some)
9105        })
9106    }
9107    fn inject_mouse_button(&self, py: Python<'_>, btn: u32, state: u32) -> PyResult<()> {
9108        wayland_backend_running(py).map_or(Ok(()), |be| {
9109            be.bind(py).borrow().inject_mouse_button(btn, state)
9110        })
9111    }
9112    fn inject_mouse_scroll(&self, py: Python<'_>, x: f64, y: f64) -> PyResult<()> {
9113        wayland_backend_running(py)
9114            .map_or(Ok(()), |be| be.bind(py).borrow().inject_mouse_scroll(x, y))
9115    }
9116    /// `WaylandBackend.inject_finger_scroll`; nothing without a running compositor.
9117    fn inject_finger_scroll(&self, py: Python<'_>, dx: f64, dy: f64) -> PyResult<()> {
9118        wayland_backend_running(py).map_or(Ok(()), |be| {
9119            be.bind(py).borrow().inject_finger_scroll(dx, dy)
9120        })
9121    }
9122    /// `WaylandBackend.inject_finger_scroll_end`; nothing without a running compositor.
9123    fn inject_finger_scroll_end(&self, py: Python<'_>) -> PyResult<()> {
9124        wayland_backend_running(py)
9125            .map_or(Ok(()), |be| be.bind(py).borrow().inject_finger_scroll_end())
9126    }
9127    /// Where the Wayland compositor's pointer is, as `(x, y, scale, motion)`
9128    /// (`WaylandBackend.pointer_location`), or None without a running compositor, before its
9129    /// first pass, or under host capture. An X11 session reads its pointer from the X server.
9130    fn pointer_location(&self, py: Python<'_>) -> Option<(f64, f64, f64, u64)> {
9131        wayland_backend_running(py).and_then(|be| be.bind(py).borrow().pointer_location())
9132    }
9133    /// Toggle compositing the cursor into captured frames (the alternative to the
9134    /// out-of-band cursor callback): the X11 grab re-reads the flag per frame, Wayland
9135    /// forwards to the compositor.
9136    fn set_cursor_rendering(&self, py: Python<'_>, enabled: bool) -> PyResult<()> {
9137        let controls = self.inner.lock().unwrap().x11_controls();
9138        if let Some(c) = &controls {
9139            c.capture_cursor.store(enabled, Ordering::Relaxed);
9140            return Ok(());
9141        }
9142        wayland_backend_running(py).map_or(Ok(()), |be| {
9143            be.bind(py).borrow().set_cursor_rendering(enabled)
9144        })
9145    }
9146    /// Register the client-copy cursor callback for whichever backend runs: the X11 cursor
9147    /// monitor reads it from its shared slot (re-delivering the current cursor to a late
9148    /// registration), and the Wayland backend takes it directly — or stashes it in
9149    /// `PENDING_CURSOR_CALLBACK`, applied by `ensure_wayland_backend` at creation; the
9150    /// backend slot lock is held across the check so a concurrent creation cannot miss the
9151    /// stash. Both slots are process-wide and outlive the capture that set them, so they carry
9152    /// the latest live registration (`CURSOR_REGISTRATIONS`): `None` withdraws this capture's
9153    /// own, releasing what it holds and restoring the one registered before it.
9154    fn set_cursor_callback(&self, py: Python<'_>, callback: Option<Py<PyAny>>) -> PyResult<()> {
9155        let callback = {
9156            let mut regs = CURSOR_REGISTRATIONS.lock().unwrap();
9157            regs.retain(|(id, _)| *id != self.id);
9158            if let Some(cb) = callback {
9159                regs.push((self.id, cb));
9160            }
9161            regs.last().map(|(_, cb)| cb.clone_ref(py))
9162        };
9163        crate::x11::cursor::set_callback(callback.as_ref().map(|c| c.clone_ref(py)));
9164        let slot = WAYLAND_BACKEND.get_or_init(|| Mutex::new(None));
9165        let g = slot.lock().unwrap();
9166        match g.as_ref() {
9167            Some(be) => be.bind(py).borrow().set_cursor_callback(callback),
9168            None => {
9169                *PENDING_CURSOR_CALLBACK.lock().unwrap() = callback;
9170                Ok(())
9171            }
9172        }
9173    }
9174
9175    /// Withdraw this capture's cursor callback, releasing what it holds; another capture's
9176    /// registration takes the slots back. Present only where the slots can be emptied, so a
9177    /// consumer can probe for it by name.
9178    fn clear_cursor_callback(&self, py: Python<'_>) -> PyResult<()> {
9179        self.set_cursor_callback(py, None)
9180    }
9181
9182    fn get_xkb_keymap_string(&self, py: Python<'_>) -> PyResult<String> {
9183        wayland_backend_running(py).map_or(Ok(String::new()), |be| {
9184            be.bind(py).borrow().get_xkb_keymap_string(py)
9185        })
9186    }
9187    /// cb(entries: list[tuple[str, bytes]]) fires when a client app copies to the clipboard,
9188    /// with the flavors of the copy: the picture, or the markup and the text beneath it, and a
9189    /// password manager's hint beside a secret; a client clearing the clipboard fires it with
9190    /// no entries.
9191    fn set_clipboard_callback(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<()> {
9192        match wayland_backend_running(py) {
9193            Some(be) => be.bind(py).borrow().set_clipboard_callback(callback),
9194            None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
9195                "wayland backend not running",
9196            )),
9197        }
9198    }
9199    /// Compositor-side clipboard offer: serve one payload per `(mime, data)` entry.
9200    fn set_clipboard(&self, py: Python<'_>, entries: Vec<(String, Vec<u8>)>) -> PyResult<()> {
9201        match wayland_backend_running(py) {
9202            Some(be) => be.bind(py).borrow().set_clipboard(entries),
9203            None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
9204                "wayland backend not running",
9205            )),
9206        }
9207    }
9208    /// Set the seat's BASE xkb layout from RMLVO names; false when no backend runs or the
9209    /// layout fails to compile.
9210    #[pyo3(signature = (layout, variant = String::new(), options = String::new(), model = String::new(), rules = String::new()))]
9211    fn set_xkb_layout(
9212        &self,
9213        py: Python<'_>,
9214        layout: String,
9215        variant: String,
9216        options: String,
9217        model: String,
9218        rules: String,
9219    ) -> PyResult<bool> {
9220        wayland_backend_running(py).map_or(Ok(false), |be| {
9221            be.bind(py)
9222                .borrow()
9223                .set_xkb_layout(py, layout, variant, options, model, rules)
9224        })
9225    }
9226    /// Seat keyboard readback: `(pressed_keycodes, modifier_mask)`; empty when no backend runs.
9227    fn get_keyboard_state(&self, py: Python<'_>) -> PyResult<(Vec<u32>, u32)> {
9228        wayland_backend_running(py).map_or(Ok((Vec::new(), 0)), |be| {
9229            be.bind(py).borrow().get_keyboard_state(py)
9230        })
9231    }
9232    /// The capture geometry actually live on the given display `(width, height, scale)`;
9233    /// `None` when no Wayland backend runs or the compositor did not answer in time (a
9234    /// timeout is distinct from a real answer so the caller treats it as unknown rather than
9235    /// "nothing to reconcile"). X11 has no compositor to read back and returns `None`.
9236    #[pyo3(signature = (display_id = 0))]
9237    fn get_realized_geometry(
9238        &self,
9239        py: Python<'_>,
9240        display_id: u32,
9241    ) -> PyResult<Option<(i32, i32, f64)>> {
9242        wayland_backend_running(py).map_or(Ok(None), |be| {
9243            be.bind(py).borrow().get_realized_geometry(py, display_id)
9244        })
9245    }
9246    /// Lifecycle of this capture as `(state, last_error)`: `state` is `"running"`, `"failed"`,
9247    /// or `"idle"`; `last_error` gives the reason a start failed, or a caveat a live capture
9248    /// came up with (encoder fell back to CPU, host connect refused, a refused resize). The
9249    /// Wayland outcome is recorded by the compositor thread; the X11 outcome is the capture
9250    /// thread's own exit error. `display_id` selects a Wayland output; X11 ignores it.
9251    #[pyo3(signature = (display_id = 0))]
9252    fn capture_state(&self, display_id: u32) -> (String, Option<String>) {
9253        let (backend, running, err) = {
9254            let st = self.inner.lock().unwrap();
9255            let running = st
9256                .controls
9257                .as_ref()
9258                .map(|c| !c.stop.load(Ordering::Relaxed))
9259                .unwrap_or(false);
9260            let err = st
9261                .err
9262                .as_ref()
9263                .and_then(|e| e.lock().ok().and_then(|g| g.clone()));
9264            (st.backend, running, err)
9265        };
9266        match backend {
9267            1 => {
9268                let state = if running {
9269                    "running"
9270                } else if err.is_some() {
9271                    "failed"
9272                } else {
9273                    "idle"
9274                };
9275                (state.to_string(), err)
9276            }
9277            2 => wayland_capture_state(display_id),
9278            _ => ("idle".to_string(), None),
9279        }
9280    }
9281    /// Create an additional Wayland output (see `WaylandBackend.create_output`); false when
9282    /// no backend runs.
9283    // The parameter list is the Python signature; grouping it would change the ABI.
9284    #[allow(clippy::too_many_arguments)]
9285    #[pyo3(signature = (id, width, height, x = 0, y = 0, scale = 1.0))]
9286    fn create_output(
9287        &self,
9288        py: Python<'_>,
9289        id: u32,
9290        width: i32,
9291        height: i32,
9292        x: i32,
9293        y: i32,
9294        scale: f64,
9295    ) -> PyResult<bool> {
9296        wayland_backend_running(py).map_or(Ok(false), |be| {
9297            be.bind(py)
9298                .borrow()
9299                .create_output(py, id, width, height, x, y, scale)
9300        })
9301    }
9302    /// Add a display over a rectangle of an existing Wayland output (see
9303    /// `WaylandBackend.create_view`); false when no backend runs.
9304    #[allow(clippy::too_many_arguments)]
9305    #[pyo3(signature = (id, owner, x, y, width, height))]
9306    fn create_view(
9307        &self,
9308        py: Python<'_>,
9309        id: u32,
9310        owner: u32,
9311        x: i32,
9312        y: i32,
9313        width: i32,
9314        height: i32,
9315    ) -> PyResult<bool> {
9316        wayland_backend_running(py).map_or(Ok(false), |be| {
9317            be.bind(py)
9318                .borrow()
9319                .create_view(py, id, owner, x, y, width, height)
9320        })
9321    }
9322    /// Resize a Wayland screen's output in place (see `WaylandBackend.resize_output`);
9323    /// false when no backend runs.
9324    #[pyo3(signature = (id, width, height, scale = 1.0))]
9325    fn resize_output(
9326        &self,
9327        py: Python<'_>,
9328        id: u32,
9329        width: i32,
9330        height: i32,
9331        scale: f64,
9332    ) -> PyResult<bool> {
9333        wayland_backend_running(py).map_or(Ok(false), |be| {
9334            be.bind(py)
9335                .borrow()
9336                .resize_output(py, id, width, height, scale)
9337        })
9338    }
9339    /// Destroy a secondary Wayland output; false when no backend runs.
9340    fn destroy_output(&self, py: Python<'_>, id: u32) -> PyResult<bool> {
9341        wayland_backend_running(py)
9342            .map_or(Ok(false), |be| be.bind(py).borrow().destroy_output(py, id))
9343    }
9344    /// Move a Wayland output (the primary included) to layout offset `(x, y)`; false when
9345    /// no backend runs or the id is unknown.
9346    fn reposition_output(&self, py: Python<'_>, id: u32, x: i32, y: i32) -> PyResult<bool> {
9347        wayland_backend_running(py).map_or(Ok(false), |be| {
9348            be.bind(py).borrow().reposition_output(py, id, x, y)
9349        })
9350    }
9351    /// Recreate the Wayland cursor theme at `size` pixels (named-cursor callbacks and the
9352    /// burned-in overlay); false when no backend runs or the size is non-positive.
9353    fn set_cursor_size(&self, py: Python<'_>, size: i32) -> PyResult<bool> {
9354        wayland_backend_running(py).map_or(Ok(false), |be| {
9355            be.bind(py).borrow().set_cursor_size(py, size)
9356        })
9357    }
9358    /// Every live Wayland output as `(id, x, y, width, height, scale, capturing)`; empty
9359    /// when no backend runs.
9360    fn list_outputs(&self, py: Python<'_>) -> PyResult<Vec<OutputDesc>> {
9361        wayland_backend_running(py)
9362            .map_or(Ok(Vec::new()), |be| be.bind(py).borrow().list_outputs(py))
9363    }
9364    /// Display capacity (see `WaylandBackend.output_capacity`); -1 when no backend runs.
9365    fn output_capacity(&self, py: Python<'_>) -> PyResult<i64> {
9366        wayland_backend_running(py).map_or(Ok(-1), |be| be.bind(py).borrow().output_capacity(py))
9367    }
9368    /// Move a window onto an output (fullscreened there); false when no backend runs.
9369    fn move_window_to_output(
9370        &self,
9371        py: Python<'_>,
9372        window_id: u32,
9373        output_id: u32,
9374    ) -> PyResult<bool> {
9375        wayland_backend_running(py).map_or(Ok(false), |be| {
9376            be.bind(py)
9377                .borrow()
9378                .move_window_to_output(py, window_id, output_id)
9379        })
9380    }
9381    /// Every mapped window as `(window_id, title, app_id, output_id, waiting)`; empty when
9382    /// no backend runs.
9383    fn list_windows(&self, py: Python<'_>) -> PyResult<Vec<WindowDesc>> {
9384        wayland_backend_running(py)
9385            .map_or(Ok(Vec::new()), |be| be.bind(py).borrow().list_windows(py))
9386    }
9387}
9388
9389/// Best-effort teardown: flag the capture thread to exit without joining.
9390///
9391/// Joining would need the GIL (the thread calls back into Python), which `Drop` cannot safely take,
9392/// so this only sets the stop flag; the actual join is left to an explicit `stop_capture` or the
9393/// atexit sweep.
9394impl Drop for ScreenCapture {
9395    fn drop(&mut self) {
9396        if let Ok(mut st) = self.inner.lock() {
9397            if let Some(c) = &st.controls {
9398                c.stop.store(true, Ordering::Relaxed);
9399            }
9400            // A Wayland capture is owned by this instance: release it, or a GC'd handle
9401            // would leave the compositor encoding and delivering forever.
9402            if st.backend == 2 && !crate::PY_SHUTDOWN.load(Ordering::Relaxed) {
9403                let did = st.wl_display;
9404                let owned = {
9405                    let mut owners = wayland_owners().lock().unwrap();
9406                    if owners.get(&did) == Some(&self.id) {
9407                        owners.remove(&did);
9408                        true
9409                    } else {
9410                        false
9411                    }
9412                };
9413                if owned
9414                    && let Some(slot) = WAYLAND_BACKEND.get()
9415                    && let Some(be) = slot.lock().unwrap().as_ref()
9416                {
9417                    Python::attach(|py| {
9418                        let _ = be.bind(py).borrow().stop_capture(did);
9419                    });
9420                }
9421            }
9422            // Pair the cursor-monitor acquire from start_capture: a dropped
9423            // handle that never got stop_capture would pin the refcount and its
9424            // monitor thread forever.
9425            if std::mem::take(&mut st.cursor_ref) && !crate::PY_SHUTDOWN.load(Ordering::Relaxed) {
9426                Python::attach(crate::x11::cursor::release);
9427            }
9428        }
9429    }
9430}
9431
9432/// Build a Python dict from a recorder status snapshot (one shape for status and stop).
9433fn recording_status_dict(
9434    py: Python<'_>,
9435    s: &crate::recorder::RecordingStatus,
9436) -> PyResult<Py<PyAny>> {
9437    let d = pyo3::types::PyDict::new(py);
9438    d.set_item("active", s.active)?;
9439    d.set_item("path", &s.path)?;
9440    d.set_item("backend", s.backend)?;
9441    d.set_item("mode", s.mode)?;
9442    d.set_item("frames", s.frames)?;
9443    d.set_item("sync_frames", s.sync_frames)?;
9444    d.set_item("audio_frames", s.audio_frames)?;
9445    d.set_item("dropped", s.dropped)?;
9446    d.set_item("skipped_non_h264", s.skipped_non_h264)?;
9447    d.set_item("bytes", s.bytes)?;
9448    d.set_item("duration_s", s.duration_s)?;
9449    d.set_item("width", s.width)?;
9450    d.set_item("height", s.height)?;
9451    d.set_item("error", s.error.as_deref())?;
9452    Ok(d.into_any().unbind())
9453}
9454
9455/// Start the built-in MP4 recorder. Works with no capture and no client running: the
9456/// recorder owns an independent capture (X11 root, or a Wayland output of the in-process
9457/// compositor) and taps a live streaming session instead of restarting it. `settings` is an
9458/// optional `CaptureSettings` for a recorder-owned capture (H.264 only; `display_id`
9459/// selects the Wayland output); when omitted, `PIXELFLUX_RECORD_*` environment variables
9460/// and full-screen defaults apply. `audio_socket` names a Unix socket serving an Ogg Opus
9461/// stream, the shape pcmflux's `output_socket` serves, recorded as the audio track.
9462#[pyfunction]
9463#[pyo3(signature = (path, settings = None, audio_socket = ""))]
9464fn start_recording(
9465    py: Python<'_>,
9466    path: String,
9467    settings: Option<&Bound<'_, PyAny>>,
9468    audio_socket: &str,
9469) -> PyResult<Py<PyAny>> {
9470    let mut opts = crate::recorder::RecordOptions::from_env(path);
9471    if !audio_socket.is_empty() {
9472        opts.audio_socket = audio_socket.to_string();
9473    }
9474    if let Some(s) = settings {
9475        let rs = extract_settings(s)?;
9476        if rs.codec != Codec::H264 {
9477            return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
9478                "recording requires H.264 capture settings (codec='h264')",
9479            ));
9480        }
9481        opts.display_id = read_display_id(s);
9482        if let Some(explicit) = s
9483            .getattr("use_wayland")
9484            .ok()
9485            .and_then(|v| v.extract::<bool>().ok())
9486        {
9487            opts.backend = Some(if explicit {
9488                crate::recorder::PreferredBackend::Wayland
9489            } else {
9490                crate::recorder::PreferredBackend::X11
9491            });
9492        }
9493        // Explicit settings are authoritative over the env knobs they subsume.
9494        opts.fps = 0.0;
9495        opts.bitrate_kbps = 0;
9496        opts.capture = Some(rs);
9497    }
9498    let status = py
9499        .detach(|| crate::recorder::start(opts))
9500        .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)?;
9501    recording_status_dict(py, &status)
9502}
9503
9504/// Stop the active recording, finalize the MP4, and return the final status dict. Raises
9505/// when no recording is active or nothing recordable was captured.
9506#[pyfunction]
9507fn stop_recording(py: Python<'_>) -> PyResult<Py<PyAny>> {
9508    let status = py
9509        .detach(crate::recorder::stop)
9510        .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)?;
9511    recording_status_dict(py, &status)
9512}
9513
9514/// Status of the live recording (or the last finished one); `None` if this process has
9515/// never recorded.
9516#[pyfunction]
9517fn recording_status(py: Python<'_>) -> PyResult<Py<PyAny>> {
9518    match crate::recorder::status() {
9519        Some(s) => recording_status_dict(py, &s),
9520        None => Ok(py.None()),
9521    }
9522}
9523
9524/// Bring the Wayland compositor socket up before any capture so apps launched early can
9525/// connect (sets WAYLAND_DISPLAY for children of this process). Idempotent; the running
9526/// backend keeps its node/dimensions on later calls. `render_node` is an explicit
9527/// /dev/dri/renderD* path; `auto_gpu` is a truthy string or vendor/driver token; empty
9528/// values mean software rendering. Returns the compositor's actual socket name (the
9529/// auto-picked `wayland-N`, which need not match any configured index); empty string only
9530/// if the socket did not come up in time.
9531#[pyfunction]
9532#[pyo3(signature = (width = 0, height = 0, render_node = String::new(), auto_gpu = String::new(), cursor_size = -1))]
9533fn ensure_wayland_display(
9534    py: Python<'_>,
9535    width: i32,
9536    height: i32,
9537    render_node: String,
9538    auto_gpu: String,
9539    cursor_size: i32,
9540) -> PyResult<String> {
9541    ensure_wayland_backend(
9542        py,
9543        width,
9544        height,
9545        render_node,
9546        auto_gpu,
9547        String::new(),
9548        cursor_size,
9549    )?;
9550    Ok(py
9551        .detach(|| wait_socket_name(Duration::from_secs(5)))
9552        .unwrap_or_default())
9553}
9554
9555/// Whether a Wayland session here would be hardware accelerated, and whether there is a GPU
9556/// to accelerate it.
9557///
9558/// Device paths do not answer this: a render node can exist with no working allocator or EGL
9559/// stack behind it, and a GPU can be present with no node at all (an NVIDIA container without
9560/// the graphics driver capability). So the compositor's own bring-up is run against the node
9561/// it would resolve, through allocating a render target and exporting it as a dmabuf. Nothing
9562/// is left running; a compositor started afterwards is unaffected.
9563///
9564/// A node that only reaches a software rasterizer is reported unaccelerated: Mesa answers
9565/// with llvmpipe rather than failing, so the bring-up succeeding is not on its own proof of
9566/// a GPU behind it.
9567///
9568/// `render_node` and `auto_gpu` take the same values as the capture settings of those names.
9569/// Returns `node` (the resolved render node, empty when none), `accelerated`, `gpu` (a GPU is
9570/// exposed here at all), `renderer` (the GL renderer reached, empty when none was), and
9571/// `error` (the step that failed; empty when accelerated).
9572#[pyfunction]
9573#[pyo3(signature = (render_node = String::new(), auto_gpu = String::new()))]
9574fn probe_wayland_gpu(py: Python<'_>, render_node: String, auto_gpu: String) -> PyResult<Py<PyAny>> {
9575    let (node, name, error) = py.detach(|| {
9576        let node = if render_node.is_empty() {
9577            auto_render_node(&auto_gpu)
9578        } else {
9579            Some(render_node)
9580        };
9581        let Some(node) = node else {
9582            return (String::new(), String::new(), "No render node".to_string());
9583        };
9584        let mut name = String::new();
9585        let result =
9586            gpu_render_init(std::path::Path::new(&node)).and_then(|(gbm, mut renderer)| {
9587                let bo = gbm
9588                    .create_buffer_object::<()>(
9589                        64,
9590                        64,
9591                        GbmFormat::Argb8888,
9592                        BufferObjectFlags::RENDERING,
9593                    )
9594                    .map_err(|_| "Failed to allocate GBM buffer")?;
9595                bo.fd()
9596                    .map_err(|e| format!("Failed to export dmabuf: {e:?}"))?;
9597                name = gl_renderer_name(&mut renderer);
9598                let lowered = name.to_lowercase();
9599                if ["llvmpipe", "softpipe", "swrast", "software rasterizer"]
9600                    .iter()
9601                    .any(|sw| lowered.contains(sw))
9602                {
9603                    return Err(format!("Software rasterizer only ({name})"));
9604                }
9605                Ok(())
9606            });
9607        (node, name, result.err().unwrap_or_default())
9608    });
9609    let d = pyo3::types::PyDict::new(py);
9610    d.set_item("node", &node)?;
9611    d.set_item("accelerated", error.is_empty())?;
9612    d.set_item("gpu", gpu_exposed())?;
9613    d.set_item("renderer", &name)?;
9614    d.set_item("error", &error)?;
9615    Ok(d.into_any().unbind())
9616}
9617
9618/// The hardware encoder of each video codec the GPU behind an encode node serves, by codec
9619/// name (`"nvenc"` or `"vaapi"`), the hardware half of what `SOFTWARE_ENCODERS` says of the
9620/// build: a codec absent from both has no path on this host, one absent from this alone runs
9621/// in software whatever `use_cpu` says. Probed once per node and remembered, so a caller
9622/// reads it at startup and never pays for it again. `encode_node_index` and `auto_gpu` are
9623/// the capture settings of those names, resolved as a capture resolves them: an explicit
9624/// index reads that node, -1 (software only) serves nothing, and anything lower (no pick)
9625/// follows the `auto_gpu` selection, the first node where that picks none.
9626#[pyfunction]
9627#[pyo3(signature = (encode_node_index = -2, auto_gpu = ""))]
9628fn hardware_encoders(
9629    py: Python<'_>,
9630    encode_node_index: i32,
9631    auto_gpu: &str,
9632) -> PyResult<Py<PyAny>> {
9633    let d = pyo3::types::PyDict::new(py);
9634    for (codec, backend, ..) in probe_hardware(py, encode_node_index, auto_gpu) {
9635        d.set_item(codec.name(), backend)?;
9636    }
9637    Ok(d.into_any().unbind())
9638}
9639
9640/// The video codecs the GPU behind an encode node encodes 4:4:4, by name: those of
9641/// `hardware_encoders` whose engine takes a `video_fullcolor` session as 4:4:4 rather than
9642/// 4:2:0, read from the same probe. Arguments as `hardware_encoders`.
9643#[pyfunction]
9644#[pyo3(signature = (encode_node_index = -2, auto_gpu = ""))]
9645fn hardware_fullcolor(py: Python<'_>, encode_node_index: i32, auto_gpu: &str) -> Vec<&'static str> {
9646    probe_hardware(py, encode_node_index, auto_gpu)
9647        .into_iter()
9648        .filter(|&(_, _, formats)| formats.fullcolor)
9649        .map(|(codec, ..)| codec.name())
9650        .collect()
9651}
9652
9653/// Every format the GPU behind an encode node encodes, by codec name: `"420-8"` for each codec
9654/// of `hardware_encoders`, and beside it `"444-8"`, `"420-10"`, and `"444-10"` where the
9655/// engine takes a `video_fullcolor` or 10-bit `video_bit_depth` session as asked, read from
9656/// the same probe. Arguments as `hardware_encoders`.
9657#[pyfunction]
9658#[pyo3(signature = (encode_node_index = -2, auto_gpu = ""))]
9659fn hardware_formats(py: Python<'_>, encode_node_index: i32, auto_gpu: &str) -> PyResult<Py<PyAny>> {
9660    let d = pyo3::types::PyDict::new(py);
9661    for (codec, _, formats) in probe_hardware(py, encode_node_index, auto_gpu) {
9662        d.set_item(codec.name(), formats.names())?;
9663    }
9664    Ok(d.into_any().unbind())
9665}
9666
9667/// The hardware table of the node `encode_node_index` and `auto_gpu` resolve to, as a capture
9668/// resolves them; empty for -1, software only.
9669fn probe_hardware(
9670    py: Python<'_>,
9671    encode_node_index: i32,
9672    auto_gpu: &str,
9673) -> encoders::HardwareEncoders {
9674    let node = match encode_node_index {
9675        -1 => return Vec::new(),
9676        index if index < -1 => auto_render_node(auto_gpu)
9677            .and_then(|picked| render_node_index(&picked))
9678            .or_else(first_render_node_index)
9679            .unwrap_or(0),
9680        index => index,
9681    };
9682    py.detach(|| encoders::hardware_encoders(node))
9683}
9684
9685/// The running compositor's Wayland socket name (e.g. "wayland-1"), or None when no
9686/// compositor thread has been started (this never creates one).
9687#[pyfunction]
9688fn get_wayland_display_name(py: Python<'_>) -> Option<String> {
9689    wayland_backend_running(py)?;
9690    py.detach(|| wait_socket_name(Duration::from_secs(2)))
9691}
9692
9693/// Stop every live capture (registered with atexit) before interpreter finalization.
9694///
9695/// The interpreter-teardown gate is set first so no detached thread may attach to a finalizing
9696/// interpreter, and the cursor callbacks (the X11 monitor's and a never-applied Wayland stash)
9697/// are dropped while the GIL is held. Every X11 capture's stop flag is set, and a live Wayland
9698/// capture is stopped over the command channel (the compositor thread clears its callback and
9699/// encoder on `StopCapture`). A brief grace sleep lets the stops be observed before Python
9700/// finalizes.
9701#[pyfunction]
9702fn _stop_all_captures(py: Python<'_>) {
9703    PY_SHUTDOWN.store(true, Ordering::Relaxed);
9704    // Finalize any active recording first so its last buffered MP4 sample is flushed and
9705    // its own capture (if any) is stopped through the normal path.
9706    py.detach(crate::recorder::finalize_on_exit);
9707    *PENDING_CURSOR_CALLBACK.lock().unwrap() = None;
9708    crate::x11::cursor::shutdown();
9709    crate::wayland::dcclient::unwatch_all();
9710    // Flag every live X11 capture to stop, then wait (bounded) for each thread to actually
9711    // return: it joins its own encode thread on the way out, dropping the NVENC/CUDA
9712    // session, and letting the interpreter finalize while that drop is in flight segfaults —
9713    // the same hazard the Wayland Barrier below fences. A snapshot is taken so the registry
9714    // mutex is not held across the wait.
9715    let x11: Vec<Arc<crate::x11::Controls>> = live_x11().lock().unwrap().iter().cloned().collect();
9716    for c in &x11 {
9717        c.stop.store(true, Ordering::Relaxed);
9718    }
9719    if !x11.is_empty() {
9720        let deadline = Instant::now() + Duration::from_secs(2);
9721        py.detach(|| {
9722            for c in &x11 {
9723                while !c.finished.load(Ordering::Acquire) && Instant::now() < deadline {
9724                    std::thread::sleep(Duration::from_millis(5));
9725                }
9726            }
9727        });
9728    }
9729    // Clone the backend handle out of the slot so the WAYLAND_BACKEND mutex is released
9730    // before the Barrier wait below: a GC-triggered ScreenCapture::Drop under the GIL takes
9731    // that same mutex, and holding it across the detached wait would block that Drop until
9732    // the Barrier times out into the unsafe exit.
9733    let be = WAYLAND_BACKEND
9734        .get()
9735        .and_then(|slot| slot.lock().unwrap().as_ref().map(|b| b.clone_ref(py)));
9736    if let Some(be) = be {
9737        let be = be.bind(py).borrow();
9738        let mut displays: Vec<u32> = wayland_alive().lock().unwrap().iter().copied().collect();
9739        if !displays.contains(&0) {
9740            displays.push(0);
9741        }
9742        for did in displays {
9743            let _ = be.stop_capture(did);
9744        }
9745        // Wait (bounded) until the calloop finished processing the stops: dropping a
9746        // hardware encoder session (NVENC/CUDA) mid-process-exit segfaults, so the
9747        // interpreter must not finalize while that teardown is still running.
9748        let (ack_tx, ack_rx) = std::sync::mpsc::channel::<()>();
9749        if be.send(ThreadCommand::Barrier { reply: ack_tx }).is_ok() {
9750            let _ = py.detach(move || ack_rx.recv_timeout(Duration::from_secs(2)));
9751        }
9752    }
9753    wayland_owners().lock().unwrap().clear();
9754    wayland_alive().lock().unwrap().clear();
9755    py.detach(|| std::thread::sleep(Duration::from_millis(50)));
9756}
9757
9758/// The `pixelflux` Python module: registers the exported classes and functions, and hooks
9759/// `_stop_all_captures` into `atexit` so every live capture is stopped before interpreter shutdown.
9760/// Socket path for a Wayland display name, with the ABI methods' error shape.
9761fn app_socket_path(display: &str) -> Result<String, String> {
9762    crate::wayland::wlclient::socket_path(display)
9763        .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())
9764}
9765
9766pyo3::create_exception!(
9767    pixelflux,
9768    VirtualKeyboardUnavailable,
9769    pyo3::exceptions::PyRuntimeError,
9770    "The target compositor does not advertise zwp_virtual_keyboard_manager_v1."
9771);
9772
9773/// Start the Computer-Use HTTP server on comma-separated entries: a bare port
9774/// listens on the loopback addresses only, `host:port` names the address to
9775/// listen on. Every request has to carry `token` (else `PIXELFLUX_CU_TOKEN`) as
9776/// a bearer token, and the server does not start without one. Idempotent; the
9777/// PIXELFLUX_CU env var remains the standalone fallback.
9778#[pyfunction]
9779#[pyo3(signature = (bind, token = None))]
9780fn start_computer_use(bind: String, token: Option<String>) -> PyResult<()> {
9781    crate::computer_use::start_cu_server(&bind, token.as_deref())
9782        .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)
9783}
9784
9785/// PNG of one display's framebuffer with the cursor drawn in, the same image the
9786/// Computer-Use server serves: the in-process Wayland compositor's output when one runs
9787/// (`display` 0 is the primary, else a live output id), otherwise the root of the X server
9788/// named by DISPLAY. No capture has to be running.
9789#[pyfunction]
9790#[pyo3(signature = (display = 0))]
9791fn screenshot_png(py: Python<'_>, display: u32) -> PyResult<Py<PyAny>> {
9792    let png = py
9793        .detach(|| crate::computer_use::resolve_backend().and_then(|b| b.screenshot_png(display)))
9794        .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)?;
9795    Ok(pyo3::types::PyBytes::new(py, &png).into_any().unbind())
9796}
9797
9798/// `gil_used = true`: the module has not been audited for free-threaded Python. The
9799/// detached compositor, capture, encode, and delivery threads attach to the interpreter
9800/// and several native encoder sessions (NVENC/CUDA, VA-API) assume the GIL serializes
9801/// their Python-facing access; until that is proven safe the interpreter re-enables the
9802/// GIL for this module on a free-threaded build rather than silently defaulting to the
9803/// thread-safe claim pyo3 0.28+ makes.
9804///
9805/// `SVT_LOG` defaults to errors here: SVT-AV1's stderr banner follows that variable alone,
9806/// and import is the one moment no thread exists to race the environment write.
9807#[pymodule(gil_used = true)]
9808fn pixelflux(m: &Bound<'_, PyModule>) -> PyResult<()> {
9809    if std::env::var_os("SVT_LOG").is_none() {
9810        unsafe { std::env::set_var("SVT_LOG", "1") };
9811    }
9812    m.add_class::<WaylandBackend>()?;
9813    m.add_class::<StripeFrame>()?;
9814    m.add_class::<CaptureSettings>()?;
9815    m.add_class::<ScreenCapture>()?;
9816    m.add_class::<webcam::VirtualCamera>()?;
9817    m.add_class::<webcam::VirtualCameraSettings>()?;
9818    // The software encoder of each video codec this build runs on this machine, by codec name:
9819    // what a CPU session encodes with, so a consumer can pick rate-control defaults and name
9820    // it, and a codec absent here has no software path on this host.
9821    let software = pyo3::types::PyDict::new(m.py());
9822    for codec in Codec::VIDEO {
9823        if let Some(enc) = encoders::software_encoder(codec) {
9824            software.set_item(codec.name(), enc.library)?;
9825        }
9826    }
9827    m.add("SOFTWARE_ENCODERS", software)?;
9828    // The codecs whose software encoder above takes a `video_fullcolor` session as 4:4:4.
9829    let fullcolor: Vec<&str> = Codec::VIDEO
9830        .into_iter()
9831        .filter(|&codec| encoders::software_fullcolor(codec))
9832        .map(|codec| codec.name())
9833        .collect();
9834    m.add("SOFTWARE_FULLCOLOR", fullcolor)?;
9835    // The formats each of those software encoders codes its codec in, named as
9836    // `hardware_formats` names an engine's.
9837    let formats = pyo3::types::PyDict::new(m.py());
9838    for codec in Codec::VIDEO {
9839        if encoders::software_encoder(codec).is_some() {
9840            formats.set_item(codec.name(), encoders::software_formats(codec).names())?;
9841        }
9842    }
9843    m.add("SOFTWARE_FORMATS", formats)?;
9844    m.add_function(wrap_pyfunction!(hardware_encoders, m)?)?;
9845    m.add_function(wrap_pyfunction!(hardware_fullcolor, m)?)?;
9846    m.add_function(wrap_pyfunction!(hardware_formats, m)?)?;
9847    m.add_function(wrap_pyfunction!(stripe_frame_from_buffer, m)?)?;
9848    m.add_function(wrap_pyfunction!(ensure_wayland_display, m)?)?;
9849    m.add_function(wrap_pyfunction!(get_wayland_display_name, m)?)?;
9850    m.add_function(wrap_pyfunction!(probe_wayland_gpu, m)?)?;
9851    m.add_function(wrap_pyfunction!(start_recording, m)?)?;
9852    m.add_function(wrap_pyfunction!(stop_recording, m)?)?;
9853    m.add_function(wrap_pyfunction!(recording_status, m)?)?;
9854    m.add_function(wrap_pyfunction!(start_computer_use, m)?)?;
9855    m.add_function(wrap_pyfunction!(screenshot_png, m)?)?;
9856    m.add(
9857        "VirtualKeyboardUnavailable",
9858        m.py().get_type::<VirtualKeyboardUnavailable>(),
9859    )?;
9860    m.add_function(wrap_pyfunction!(_stop_all_captures, m)?)?;
9861    if let Ok(atexit) = m.py().import("atexit") {
9862        let _ = atexit.call_method1("register", (m.getattr("_stop_all_captures")?,));
9863    }
9864    // Standalone CU entry point: with PIXELFLUX_CU set the server binds at import, serving
9865    // X11 (via DISPLAY) until a Wayland compositor registers itself as the backend.
9866    crate::computer_use::spawn_cu_from_env();
9867    // PIXELFLUX_RECORD=<path>: start recording from process start (X11 immediately, or as
9868    // soon as the in-process Wayland compositor comes up).
9869    crate::recorder::autostart_from_env();
9870
9871    Ok(())
9872}
9873
9874#[cfg(test)]
9875mod annexb_frame_type_tests {
9876    //! The wire type byte read from real encoder output: the vectors are x264
9877    //! access units (truncated past the classifier's decision point) whose
9878    //! ground truth came from libavcodec's own slice-header trace, an open-GOP
9879    //! stream being the one place non-IDR I frames occur.
9880    use crate::encoders::h264_frame_type as annexb_frame_type;
9881
9882    const IDR_AU: &[u8] = &[
9883        0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x0d, 0xac, 0xd9,
9884        0x41, 0x41, 0xfb, 0x01, 0x10, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x03, 0x03, 0xc0,
9885        0xf1, 0x42, 0x99, 0x60, 0x00, 0x00, 0x00, 0x01, 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, 0x00,
9886        0x00, 0x01, 0x06, 0x05, 0xff, 0xff, 0xa8, 0xdc, 0x45, 0xe9, 0xbd, 0xe6, 0xd9, 0x48, 0xb7,
9887        0x96, 0x2c, 0xd8, 0x20, 0xd9, 0x23, 0xee, 0xef, 0x78, 0x32, 0x36, 0x34, 0x20, 0x2d, 0x20,
9888        0x63, 0x6f, 0x72, 0x65, 0x20, 0x31, 0x36, 0x34, 0x20, 0x72, 0x33, 0x30, 0x39, 0x35, 0x20,
9889        0x62, 0x61, 0x65, 0x65, 0x34, 0x30, 0x30, 0x20, 0x2d, 0x20, 0x48, 0x2e, 0x32, 0x36, 0x34,
9890        0x2f, 0x4d, 0x50, 0x45, 0x47, 0x2d, 0x34, 0x20, 0x41, 0x56, 0x43, 0x20, 0x63, 0x6f, 0x64,
9891        0x65, 0x63, 0x20, 0x2d, 0x20, 0x43, 0x6f, 0x70, 0x79, 0x6c, 0x65, 0x66, 0x74, 0x20, 0x32,
9892        0x30, 0x30, 0x33, 0x2d, 0x32, 0x30, 0x32, 0x32, 0x20, 0x2d, 0x20, 0x68, 0x74, 0x74, 0x70,
9893        0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x6c, 0x61, 0x6e,
9894        0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x78, 0x32, 0x36, 0x34, 0x2e, 0x68, 0x74, 0x6d, 0x6c, 0x20,
9895        0x2d, 0x20, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x20, 0x63, 0x61, 0x62, 0x61,
9896        0x63, 0x3d, 0x31, 0x20, 0x72, 0x65, 0x66, 0x3d, 0x33, 0x20, 0x64, 0x65, 0x62, 0x6c, 0x6f,
9897        0x63, 0x6b, 0x3d, 0x31, 0x3a, 0x30, 0x3a, 0x30, 0x20, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73,
9898        0x65, 0x3d, 0x30, 0x78, 0x33, 0x3a, 0x30, 0x78, 0x31, 0x31, 0x33, 0x20, 0x6d, 0x65, 0x3d,
9899        0x68, 0x65, 0x78, 0x20, 0x73, 0x75, 0x62, 0x6d, 0x65, 0x3d, 0x37, 0x20, 0x70, 0x73, 0x79,
9900        0x3d, 0x31, 0x20, 0x70, 0x73, 0x79, 0x5f, 0x72, 0x64, 0x3d, 0x31, 0x2e, 0x30, 0x30, 0x3a,
9901        0x30, 0x2e, 0x30, 0x30, 0x20, 0x6d, 0x69, 0x78, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x66, 0x3d,
9902        0x31, 0x20, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x3d, 0x31, 0x36, 0x20, 0x63,
9903        0x68, 0x72, 0x6f, 0x6d, 0x61, 0x5f, 0x6d, 0x65, 0x3d, 0x31, 0x20, 0x74, 0x72, 0x65, 0x6c,
9904        0x6c, 0x69, 0x73, 0x3d, 0x31, 0x20, 0x38, 0x78, 0x38, 0x64, 0x63, 0x74, 0x3d, 0x31, 0x20,
9905        0x63, 0x71, 0x6d, 0x3d, 0x30, 0x20, 0x64, 0x65, 0x61, 0x64, 0x7a, 0x6f, 0x6e, 0x65, 0x3d,
9906        0x32, 0x31, 0x2c, 0x31, 0x31, 0x20, 0x66, 0x61, 0x73, 0x74, 0x5f, 0x70, 0x73, 0x6b, 0x69,
9907        0x70, 0x3d, 0x31, 0x20, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x61, 0x5f, 0x71, 0x70, 0x5f, 0x6f,
9908        0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x2d, 0x32, 0x20, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64,
9909        0x73, 0x3d, 0x37, 0x20, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x5f, 0x74,
9910        0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x3d, 0x31, 0x20, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x64,
9911        0x5f, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x3d, 0x30, 0x20, 0x6e, 0x72, 0x3d, 0x30,
9912        0x20, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x3d, 0x31, 0x20, 0x69, 0x6e, 0x74,
9913        0x65, 0x72, 0x6c, 0x61, 0x63, 0x65, 0x64, 0x3d, 0x30, 0x20, 0x62, 0x6c, 0x75, 0x72, 0x61,
9914        0x79, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x3d, 0x30, 0x20, 0x63, 0x6f, 0x6e, 0x73,
9915        0x74, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x64, 0x5f, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x3d, 0x30,
9916        0x20, 0x62, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x3d, 0x32, 0x20, 0x62, 0x5f, 0x70, 0x79,
9917        0x72, 0x61, 0x6d, 0x69, 0x64, 0x3d, 0x32, 0x20, 0x62, 0x5f, 0x61, 0x64, 0x61, 0x70, 0x74,
9918        0x3d, 0x31, 0x20, 0x62, 0x5f, 0x62, 0x69, 0x61, 0x73, 0x3d, 0x30, 0x20, 0x64, 0x69, 0x72,
9919        0x65, 0x63, 0x74, 0x3d, 0x31, 0x20, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x62, 0x3d, 0x31,
9920        0x20, 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x67, 0x6f, 0x70, 0x3d, 0x31, 0x20, 0x77, 0x65, 0x69,
9921        0x67, 0x68, 0x74, 0x70, 0x3d, 0x32, 0x20, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, 0x3d, 0x31,
9922        0x32, 0x20, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, 0x5f, 0x6d, 0x69, 0x6e, 0x3d, 0x37, 0x20,
9923        0x73, 0x63, 0x65, 0x6e, 0x65, 0x63, 0x75, 0x74, 0x3d, 0x34, 0x30, 0x20, 0x69, 0x6e, 0x74,
9924        0x72, 0x61, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x3d, 0x30, 0x20, 0x72, 0x63,
9925        0x5f, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x3d, 0x31, 0x32, 0x20, 0x72,
9926        0x63, 0x3d, 0x63, 0x72, 0x66, 0x20, 0x6d, 0x62, 0x74, 0x72, 0x65, 0x65, 0x3d, 0x31, 0x20,
9927        0x63, 0x72, 0x66, 0x3d, 0x32, 0x33, 0x2e, 0x30, 0x20, 0x71, 0x63, 0x6f, 0x6d, 0x70, 0x3d,
9928        0x30, 0x2e, 0x36, 0x30, 0x20, 0x71, 0x70, 0x6d, 0x69, 0x6e, 0x3d, 0x30, 0x20, 0x71, 0x70,
9929        0x6d, 0x61, 0x78, 0x3d, 0x36, 0x39, 0x20, 0x71, 0x70, 0x73, 0x74, 0x65, 0x70, 0x3d, 0x34,
9930        0x20, 0x69, 0x70, 0x5f, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x3d, 0x31, 0x2e, 0x34, 0x30, 0x20,
9931        0x61, 0x71, 0x3d, 0x31, 0x3a, 0x31, 0x2e, 0x30, 0x30, 0x00, 0x80, 0x00, 0x00, 0x01, 0x65,
9932        0x88, 0x84, 0x00, 0x47, 0xda, 0xe3, 0x98, 0x5b, 0xd7, 0x57, 0xda, 0x42, 0x3e, 0x83, 0x89,
9933        0x96, 0xcb, 0xc5, 0x1b,
9934    ];
9935    const INTRA_AU: &[u8] = &[
9936        0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x0d, 0xac, 0xd9,
9937        0x41, 0x41, 0xfb, 0x01, 0x10, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x03, 0x03, 0xc0,
9938        0xf1, 0x42, 0x99, 0x60, 0x00, 0x00, 0x00, 0x01, 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, 0x00,
9939        0x00, 0x01, 0x06, 0x06, 0x01, 0xc4, 0x80, 0x00, 0x00, 0x01, 0x41, 0x88, 0xc3, 0x05, 0xff,
9940        0xd4, 0x57, 0x6d, 0x62, 0x78, 0xad, 0x3e, 0x89, 0xb4, 0xb5, 0x2a, 0xde, 0xcb, 0x0c, 0x64,
9941    ];
9942    const P_AU: &[u8] = &[
9943        0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x23, 0x6c, 0x45, 0x7f, 0xb7,
9944        0xe7, 0xf9, 0x23, 0x5b, 0xd9, 0xab, 0xc7, 0x7f, 0xcc, 0x03, 0xc4, 0xaa, 0x6f,
9945    ];
9946    const B_AU: &[u8] = &[
9947        0x00, 0x00, 0x01, 0x09, 0x50, 0x00, 0x00, 0x01, 0x41, 0x9e, 0x41, 0x78, 0x85, 0x3f, 0xfd,
9948        0xb1, 0x75, 0x99, 0x50, 0x2e, 0xe9, 0x5c, 0x1a, 0x0f, 0x32, 0x6a, 0xa7, 0x3d,
9949    ];
9950    const INTRA_VCL: &[u8] = &[
9951        0x00, 0x00, 0x01, 0x41, 0x88, 0xc3, 0x05, 0xff, 0xd4, 0x57, 0x6d, 0x62, 0x78, 0xad, 0x3e,
9952        0x89, 0xb4, 0xb5, 0x2a, 0xde, 0xcb, 0x0c, 0x64, 0x72, 0xe8, 0x3d, 0x54,
9953    ];
9954    const P_VCL: &[u8] = &[
9955        0x00, 0x00, 0x01, 0x41, 0x9a, 0x23, 0x6c, 0x45, 0x7f, 0xb7, 0xe7, 0xf9, 0x23, 0x5b, 0xd9,
9956        0xab, 0xc7, 0x7f, 0xcc, 0x03, 0xc4, 0xaa, 0x6f, 0x9a, 0xd8, 0xcf, 0x4a,
9957    ];
9958    /// An I slice whose huge `first_mb_in_slice` forces an emulation-prevention
9959    /// byte inside the header itself, so parsing only succeeds through the
9960    /// stripper.
9961    const EPB_INTRA_VCL: &[u8] = &[
9962        0x00, 0x00, 0x01, 0x41, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x08, 0x88,
9963    ];
9964
9965    #[test]
9966    fn idr_is_key() {
9967        assert_eq!(annexb_frame_type(IDR_AU), 0x01);
9968    }
9969
9970    #[test]
9971    fn open_gop_intra_is_intra_not_key() {
9972        assert_eq!(annexb_frame_type(INTRA_AU), 0x02);
9973    }
9974
9975    #[test]
9976    fn predicted_frames_are_delta() {
9977        assert_eq!(annexb_frame_type(P_AU), 0x00);
9978        assert_eq!(annexb_frame_type(B_AU), 0x00);
9979    }
9980
9981    #[test]
9982    fn one_predicted_slice_downgrades_a_mixed_unit() {
9983        let mixed = [INTRA_VCL, P_VCL].concat();
9984        assert_eq!(annexb_frame_type(&mixed), 0x00);
9985    }
9986
9987    #[test]
9988    fn unparsable_slice_header_is_delta() {
9989        assert_eq!(annexb_frame_type(&[0x00, 0x00, 0x01, 0x41]), 0x00);
9990        assert_eq!(annexb_frame_type(&[]), 0x00);
9991    }
9992
9993    #[test]
9994    fn emulation_prevention_is_stripped_before_parsing() {
9995        assert_eq!(annexb_frame_type(EPB_INTRA_VCL), 0x02);
9996    }
9997}
9998
9999#[cfg(test)]
10000mod encode_node_tests {
10001    //! How an unset encode node resolves: the index a render node path counts as, and no
10002    //! pick at all where auto-GPU selection is off.
10003    use super::{auto_render_node, render_node_index};
10004
10005    #[test]
10006    fn render_node_path_counts_from_128() {
10007        assert_eq!(render_node_index("/dev/dri/renderD128"), Some(0));
10008        assert_eq!(render_node_index("/dev/dri/renderD130"), Some(2));
10009        assert_eq!(render_node_index("/dev/dri/card0"), None);
10010    }
10011
10012    #[test]
10013    fn auto_gpu_off_picks_nothing() {
10014        assert_eq!(auto_render_node("false"), None);
10015        assert_eq!(auto_render_node(""), None);
10016    }
10017}
10018
10019#[cfg(test)]
10020mod shm_usage_tests {
10021    //! What the session reports as its /dev/shm usage: allocated blocks, so a sparse
10022    //! file is not counted as memory anyone holds.
10023    use super::shm_usage_in;
10024
10025    #[test]
10026    fn only_allocated_blocks_count() {
10027        let dir = std::env::temp_dir().join(format!("pixelflux-shm-{}", std::process::id()));
10028        std::fs::create_dir_all(&dir).unwrap();
10029        let path = dir.to_str().unwrap().to_string();
10030
10031        std::fs::File::create(dir.join("sparse"))
10032            .unwrap()
10033            .set_len(8 * 1024 * 1024 * 1024)
10034            .unwrap();
10035        let sparse = shm_usage_in(&path);
10036
10037        std::fs::write(dir.join("dense"), vec![0u8; 4 * 1024 * 1024]).unwrap();
10038        let dense = shm_usage_in(&path);
10039
10040        std::fs::remove_dir_all(&dir).ok();
10041        assert!(
10042            sparse < 1024 * 1024,
10043            "an 8 GiB sparse file counted {sparse} bytes"
10044        );
10045        assert!(
10046            dense - sparse >= 4 * 1024 * 1024,
10047            "a 4 MiB written file added only {} bytes",
10048            dense - sparse
10049        );
10050    }
10051}
10052
10053#[cfg(test)]
10054mod capture_state_tests {
10055    //! The `(state, last_error)` a Wayland display's capture reports: a live pipeline is
10056    //! "running" (with a caveat surfaced alongside), a recorded failure with no live
10057    //! pipeline is "failed", and a clean stop is "idle".
10058    use super::{set_wayland_capture_err, wayland_alive, wayland_capture_state};
10059
10060    #[test]
10061    fn state_tracks_liveness_and_recorded_error() {
10062        // A display id unlikely to collide with any concurrent test's use of the globals.
10063        let did = 987_654;
10064        set_wayland_capture_err(did, None);
10065        wayland_alive().lock().unwrap().remove(&did);
10066        assert_eq!(wayland_capture_state(did), ("idle".to_string(), None));
10067
10068        // Recorded failure, no live pipeline -> failed with the reason.
10069        set_wayland_capture_err(did, Some("no output".to_string()));
10070        assert_eq!(
10071            wayland_capture_state(did),
10072            ("failed".to_string(), Some("no output".to_string()))
10073        );
10074
10075        // A live pipeline reads running even with a caveat still recorded.
10076        wayland_alive().lock().unwrap().insert(did);
10077        set_wayland_capture_err(did, Some("using CPU encode".to_string()));
10078        assert_eq!(
10079            wayland_capture_state(did),
10080            ("running".to_string(), Some("using CPU encode".to_string()))
10081        );
10082
10083        // Clean stop clears both.
10084        wayland_alive().lock().unwrap().remove(&did);
10085        set_wayland_capture_err(did, None);
10086        assert_eq!(wayland_capture_state(did), ("idle".to_string(), None));
10087    }
10088}
10089
10090#[cfg(test)]
10091mod host_layout_tests {
10092    //! How the mode a host announced resolves for a capture configured at the requested
10093    //! size: that size, or no mode at all -> nothing to do; any other mode -> follow it,
10094    //! whatever the host answered about the request.
10095    use super::host_layout_resolution;
10096
10097    #[test]
10098    fn the_capture_follows_the_mode_the_host_runs() {
10099        let want = (1920, 1080);
10100        // Running the requested size: converged.
10101        assert_eq!(host_layout_resolution(want, Some(want)), None);
10102        // No mode announced: nothing to follow, the request stands.
10103        assert_eq!(host_layout_resolution(want, None), None);
10104        // Running another mode: the capture follows the host. This covers a host that
10105        // refused the request and one that acknowledged it without applying it, which
10106        // reads identically here and is what keeps a fixed-mode host from renegotiating
10107        // with a resizing client forever.
10108        assert_eq!(
10109            host_layout_resolution(want, Some((2560, 1440))),
10110            Some((2560, 1440))
10111        );
10112        assert_eq!(
10113            host_layout_resolution(want, Some((1280, 720))),
10114            Some((1280, 720))
10115        );
10116    }
10117}
10118
10119#[cfg(test)]
10120mod output_overlap_tests {
10121    //! Invariants of the output-placement intersection predicate: strict interior
10122    //! intersection (touching edges never overlap), containment and identity overlap,
10123    //! empty/negative rectangles never overlap, and extreme coordinates do not wrap.
10124    use super::rects_overlap;
10125
10126    #[test]
10127    fn disjoint_rects_do_not_overlap() {
10128        assert!(!rects_overlap((0, 0, 100, 100), (200, 0, 100, 100)));
10129        assert!(!rects_overlap((0, 0, 100, 100), (0, 200, 100, 100)));
10130    }
10131
10132    #[test]
10133    fn touching_edges_do_not_overlap() {
10134        // Right edge of a meets left edge of b, and bottom meets top.
10135        assert!(!rects_overlap((0, 0, 100, 100), (100, 0, 100, 100)));
10136        assert!(!rects_overlap((0, 0, 100, 100), (0, 100, 100, 100)));
10137        // Corner touch only.
10138        assert!(!rects_overlap((0, 0, 100, 100), (100, 100, 50, 50)));
10139    }
10140
10141    #[test]
10142    fn one_pixel_intrusion_overlaps() {
10143        assert!(rects_overlap((0, 0, 100, 100), (99, 0, 100, 100)));
10144        assert!(rects_overlap((0, 0, 100, 100), (0, 99, 100, 100)));
10145    }
10146
10147    #[test]
10148    fn containment_and_identity_overlap() {
10149        assert!(rects_overlap((0, 0, 100, 100), (25, 25, 10, 10)));
10150        assert!(rects_overlap((25, 25, 10, 10), (0, 0, 100, 100)));
10151        assert!(rects_overlap((5, 5, 50, 50), (5, 5, 50, 50)));
10152    }
10153
10154    #[test]
10155    fn empty_or_negative_rects_never_overlap() {
10156        assert!(!rects_overlap((10, 10, 0, 50), (0, 0, 100, 100)));
10157        assert!(!rects_overlap((10, 10, 50, 0), (0, 0, 100, 100)));
10158        assert!(!rects_overlap((10, 10, -5, 5), (0, 0, 100, 100)));
10159        assert!(!rects_overlap((0, 0, 100, 100), (10, 10, 0, 0)));
10160    }
10161
10162    #[test]
10163    fn negative_origins_overlap_correctly() {
10164        assert!(rects_overlap((-50, -50, 100, 100), (0, 0, 100, 100)));
10165        assert!(!rects_overlap((-100, -100, 100, 100), (0, 0, 100, 100)));
10166    }
10167
10168    #[test]
10169    fn extreme_coordinates_do_not_wrap() {
10170        assert!(!rects_overlap(
10171            (i32::MAX - 10, 0, 10, 10),
10172            (i32::MIN, 0, 10, 10)
10173        ));
10174        assert!(rects_overlap(
10175            (i32::MAX - 10, 0, 10, 10),
10176            (i32::MAX - 5, 0, 10, 10)
10177        ));
10178    }
10179}
10180
10181#[cfg(test)]
10182mod wl_frame_pool_tests {
10183    //! Invariants under test: try_begin is non-blocking and hands out a buffer ONLY while the
10184    //! publish slot is empty (so publish can never block the calloop); take blocks until a
10185    //! frame or shutdown; recycle/cancel return buffers for reuse; every published frame is
10186    //! observed exactly once and in order (the H.264 reference chain depends on it).
10187    use super::*;
10188
10189    fn frame(id: usize, buf: Vec<u8>, n: u16) -> WlFrame {
10190        WlFrame {
10191            id,
10192            buf,
10193            frame_id: n,
10194            damage: Vec::new(),
10195            is_animated: false,
10196            captured_ns: 0,
10197        }
10198    }
10199
10200    #[test]
10201    fn begin_gated_on_slot_and_free_list() {
10202        let p = WlFramePool::new(2, 16);
10203        let (a, abuf) = p.try_begin().expect("first buffer");
10204        let (b, bbuf) = p.try_begin().expect("second buffer");
10205        assert_ne!(a, b);
10206        assert!(p.try_begin().is_none(), "free list exhausted");
10207        p.publish(frame(a, abuf, 0));
10208        p.cancel(b, bbuf);
10209        assert!(p.try_begin().is_none(), "slot occupied blocks begin");
10210        let f = p.take().expect("published frame");
10211        assert_eq!(f.frame_id, 0);
10212        p.recycle(f.id, f.buf);
10213        assert!(p.try_begin().is_some(), "drained slot re-enables begin");
10214    }
10215
10216    #[test]
10217    fn frames_flow_in_order_and_buffers_recycle() {
10218        let p = Arc::new(WlFramePool::new(2, 4));
10219        let p2 = p.clone();
10220        let consumer = thread::spawn(move || {
10221            let mut seen = Vec::new();
10222            while let Some(f) = p2.take() {
10223                seen.push(f.frame_id);
10224                p2.recycle(f.id, f.buf);
10225            }
10226            seen
10227        });
10228        let mut published = 0u16;
10229        while published < 50 {
10230            if let Some((id, buf)) = p.try_begin() {
10231                p.publish(frame(id, buf, published));
10232                published += 1;
10233            } else {
10234                thread::sleep(Duration::from_micros(50));
10235            }
10236        }
10237        thread::sleep(Duration::from_millis(50));
10238        p.shutdown();
10239        let seen = consumer.join().unwrap();
10240        assert_eq!(seen, (0..50).collect::<Vec<u16>>(), "every frame, in order");
10241    }
10242
10243    #[test]
10244    fn shutdown_unblocks_take() {
10245        let p = Arc::new(WlFramePool::new(1, 4));
10246        let p2 = p.clone();
10247        let t = thread::spawn(move || p2.take());
10248        thread::sleep(Duration::from_millis(30));
10249        p.shutdown();
10250        assert!(t.join().unwrap().is_none(), "take returns None on shutdown");
10251    }
10252
10253    #[test]
10254    fn cancel_returns_buffer_for_reuse() {
10255        let p = WlFramePool::new(1, 8);
10256        let (id, buf) = p.try_begin().expect("buffer");
10257        assert!(p.try_begin().is_none());
10258        p.cancel(id, buf);
10259        assert!(p.try_begin().is_some(), "canceled reservation reusable");
10260    }
10261}
10262
10263#[cfg(test)]
10264mod auto_gpu_token_tests {
10265    //! Invariant: AUTO_GPU tokens match a card's kernel-reported identity —
10266    //! driver name exactly (no table), raw PCI vendor ID, devicetree compatible
10267    //! prefix literally, or a human vendor name via the small embedded aliases —
10268    //! so users may pass either "amd" or "amdgpu" (etc.) interchangeably.
10269    use super::{CardIdentity, card_matches_token};
10270
10271    fn pci(driver: &str, vendor: u32) -> CardIdentity {
10272        CardIdentity {
10273            driver: driver.into(),
10274            pci_vendor: Some(vendor),
10275            compatibles: vec![],
10276        }
10277    }
10278
10279    fn dt(driver: &str, compatibles: &[&str]) -> CardIdentity {
10280        CardIdentity {
10281            driver: driver.into(),
10282            pci_vendor: None,
10283            compatibles: compatibles.iter().map(|c| c.to_string()).collect(),
10284        }
10285    }
10286
10287    #[test]
10288    fn driver_names_match_without_any_table() {
10289        assert!(card_matches_token("amdgpu", &pci("amdgpu", 0x1002)));
10290        assert!(card_matches_token(
10291            "panfrost",
10292            &dt("panfrost", &["rockchip,rk3399-mali"])
10293        ));
10294        assert!(card_matches_token("nouveau", &pci("nouveau", 0x10de)));
10295        assert!(!card_matches_token("i915", &pci("amdgpu", 0x1002)));
10296    }
10297
10298    #[test]
10299    fn vendor_names_and_raw_ids_match_pci_identity() {
10300        let nv = pci("nouveau", 0x10de);
10301        assert!(card_matches_token("nvidia", &nv));
10302        assert!(card_matches_token("0x10de", &nv));
10303        assert!(card_matches_token("10de", &nv));
10304        assert!(!card_matches_token("amd", &nv));
10305        assert!(card_matches_token("ati", &pci("radeon", 0x1002)));
10306    }
10307
10308    #[test]
10309    fn devicetree_prefixes_match_literally_and_via_aliases() {
10310        let mali = dt("panfrost", &["rockchip,rk3399-mali", "arm,mali-t860"]);
10311        assert!(card_matches_token("rockchip", &mali));
10312        assert!(card_matches_token("arm", &mali));
10313        assert!(card_matches_token("mali", &mali));
10314        let adreno = dt("msm", &["qcom,adreno-630", "qcom,adreno"]);
10315        assert!(card_matches_token("qcom", &adreno));
10316        assert!(card_matches_token("qualcomm", &adreno));
10317        assert!(card_matches_token("adreno", &adreno));
10318        assert!(!card_matches_token("brcm", &adreno));
10319        assert!(card_matches_token(
10320            "videocore",
10321            &dt("v3d", &["brcm,bcm2711-v3d"])
10322        ));
10323    }
10324
10325    #[test]
10326    fn missing_identity_fields_never_false_match() {
10327        let bare = CardIdentity {
10328            driver: String::new(),
10329            pci_vendor: None,
10330            compatibles: vec![],
10331        };
10332        for t in ["nvidia", "amdgpu", "0x10de", "qcom"] {
10333            assert!(!card_matches_token(t, &bare));
10334        }
10335    }
10336}