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: a Rust library, and with the
17//! `python` feature the `pixelflux` Python extension module built on it (`python`). It
18//! supports two independent backends — **X11** (XShm + XFixes) and **Wayland**
19//! (a headless [Smithay](https://github.com/Smithay/smithay) compositor) — and a shared
20//! encoding layer that dispatches to software (striped JPEG, H.264 through the build's
21//! software encoder — libx264 with the `gpl` feature, OpenH264 without — and the other
22//! codecs through the software encoders the build links: x265 or kvazaar, libvpx, SVT-AV1) or
23//! hardware (NVENC, VA-API) encoders based on the available GPU and operator settings.
24//!
25//! ## Crate structure
26//!
27//! | Module | Purpose |
28//! |--------|---------|
29//! | [`encoders`] | Encoder backends: software H.264 (libx264 or OpenH264) / JPEG, NVENC, VA-API, watermark overlay |
30//! | [`wayland`] | Headless Smithay compositor, cursor rendering |
31//! | [`x11`] | X11/XShm capture loop, XFixes out-of-band cursor monitor, and stripe dispatch |
32//! | [`pipeline`] | Frame-processing policy shared by both backends (send/QP/keyframe decisions) |
33//! | [`recording_sink`] | Unix-socket H.264 fan-out for external recording |
34//! | [`recorder`] | Built-in MP4 recorder (fMP4 muxer + API/env/REST control surfaces) |
35//! | [`sink`] | What a capture hands its consumer, and the traits a consumer implements to receive it |
36//! | `python` | The Python bridge, with the `python` feature: the classes and functions `import pixelflux` sees |
37//! | [`computer_use`] | AI-agent desktop control (screenshots, input injection, the recorder's commands) |
38//! | [`nvgpufilter`] | Multi-GPU NVENC device filtering via ioctl |
39//! | [`webcam`] | Virtual camera: client webcam uplink decoded into a V4L2 device (interposer ring, v4l2loopback, PipeWire node) |
40//!
41//! ## Data flow
42//!
43//! ```text
44//! Consumer  ──►  CaptureSettings  ──►  X11 / Wayland backend
45//!                                         │
46//!                                    frame pixels
47//!                                         │
48//!                                    ┌────┴────┐
49//!                                    │ Encoder │  (NVENC / VAAPI / x264 or OpenH264 / JPEG)
50//!                                    └────┬────┘
51//!                                         │
52//!                                   EncodedStripe(s)
53//!                                         │
54//!                                    FrameSink (a Rust consumer, or the Python callback)
55//! ```
56
57#![allow(dead_code)]
58
59use std::fs::File;
60use std::sync::Arc;
61use std::thread;
62use std::time::{Duration, Instant};
63
64use gbm::{BufferObject, BufferObjectFlags, Device as RawGbmDevice, Format as GbmFormat};
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::reexports::wayland_server::protocol::wl_shm;
69use smithay::wayland::cursor_shape::CursorShapeManagerState;
70use smithay::wayland::image_capture_source::{ImageCaptureSourceState, OutputCaptureSourceState};
71use smithay::wayland::image_copy_capture::{CaptureFailureReason, ImageCopyCaptureState};
72use smithay::wayland::presentation::{PresentationState, Refresh};
73use smithay::wayland::selection::ext_data_control::DataControlState as ExtDataControlState;
74use smithay::wayland::selection::wlr_data_control::DataControlState;
75use smithay::wayland::single_pixel_buffer::SinglePixelBufferState;
76use smithay::wayland::viewporter::ViewporterState;
77use smithay::{
78    backend::{
79        allocator::{
80            Buffer as _, Fourcc, Modifier,
81            dmabuf::{Dmabuf, DmabufFlags},
82            gbm::GbmDevice,
83        },
84        drm::DrmNode,
85        egl::{EGLContext, EGLDisplay},
86        input::{Axis, AxisSource, KeyState, Keycode},
87        renderer::{
88            Bind, ExportMem, Frame as _, ImportAll, ImportDma, ImportEgl, ImportMem, Renderer as _,
89            damage::OutputDamageTracker,
90            element::{
91                AsRenderElements, Element, Kind, RenderElement, Wrap,
92                memory::MemoryRenderBufferRenderElement, surface::WaylandSurfaceRenderElement,
93            },
94            gles::GlesRenderer,
95            pixman::PixmanRenderer,
96            sync::SyncPoint,
97            utils::Buffer as SurfaceBuffer,
98        },
99    },
100    desktop::{LayerMap, PopupManager, layer_map_for_output},
101    desktop::{Space, space::SpaceRenderElements},
102    input::{
103        SeatState,
104        keyboard::XkbConfig,
105        pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent, RelativeMotionEvent},
106    },
107    output::{Mode as OutputMode, Output, PhysicalProperties, Scale as OutputScale, Subpixel},
108    reexports::{
109        calloop::{
110            EventLoop, Interest, Mode, PostAction,
111            generic::Generic,
112            timer::{TimeoutAction, Timer},
113        },
114        pixman,
115        wayland_server::{Display, DisplayHandle},
116    },
117    utils::{Clock, Logical, Physical, Point, Rectangle, Scale, Size, Transform},
118    wayland::selection::primary_selection::PrimarySelectionState,
119    wayland::shell::wlr_layer::WlrLayerShellState,
120    wayland::xdg_activation::XdgActivationState,
121    wayland::{
122        compositor::{CompositorState, with_states},
123        dmabuf::{DmabufFeedbackBuilder, DmabufState, get_dmabuf},
124        foreign_toplevel_list::ForeignToplevelListState,
125        fractional_scale::FractionalScaleManagerState,
126        output::OutputManagerState,
127        pointer_constraints::PointerConstraintsState,
128        pointer_warp::PointerWarpManager,
129        relative_pointer::RelativePointerManagerState,
130        seat::WaylandFocus,
131        selection::data_device::DataDeviceState,
132        shell::xdg::XdgShellState,
133        shell::xdg::decoration::XdgDecorationState,
134        shm::{ShmState, with_buffer_contents},
135        socket::ListeningSocketSource,
136    },
137};
138
139/// Encoder backends and the codec identities, wire framing, and rate-control policy they share.
140pub mod encoders;
141/// The one error type the public API returns.
142pub mod error;
143pub use error::Error;
144/// The crate version compiled in, for a host that names the pixelflux it links.
145pub const VERSION: &str = env!("CARGO_PKG_VERSION");
146/// The debug switch behind every backend's tagged line.
147pub mod log;
148/// The Python bridge: the classes and functions the `pixelflux` extension module exports.
149#[cfg(feature = "python")]
150mod python;
151
152/// The Anthropic Computer Use actions for AI agent desktop control.
153pub mod computer_use;
154/// Per-block stillness and cleanliness of a region, for refining blocks while others move.
155pub mod ledger;
156/// When a capture is due a frame, shared by the X11 and Wayland backends.
157pub mod pace;
158/// Built-in MP4 recorder: independent capture-to-file with API/env/REST control.
159pub mod recorder;
160/// Unix-socket H.264 recording fan-out for external capture tools.
161pub mod recording_sink;
162/// What each capture streams and how it got there, as values a caller reads.
163pub mod report;
164/// What a capture hands its consumer, and the traits a consumer implements to receive it.
165pub mod sink;
166/// Kernel uinput devices, the first rung of host-capture input injection.
167pub mod uinput;
168/// Headless Wayland compositor and cursor rendering.
169pub mod wayland;
170/// The window policy interface a window manager implements for the compositor.
171pub mod wm;
172
173#[cfg(test)]
174mod cleanup_bench;
175/// Multi-GPU NVENC device filtering via kernel ioctl.
176pub mod nvgpufilter;
177/// Frame-processing policy shared by the X11 and Wayland backends.
178pub mod pipeline;
179/// Run-time libpipewire binding and SPA pod encoding shared by the webcam sink and host capture.
180pub mod pipewire;
181/// X11/XShm capture loop, stripe dispatch, and per-stripe change detection.
182pub mod x11;
183
184pub mod webcam;
185
186pub use encoders::nvenc;
187pub use encoders::software::StripeState;
188
189fn get_process_rss_bytes() -> usize {
190    if let Ok(contents) = std::fs::read_to_string("/proc/self/statm")
191        && let Some(rss_pages) = contents.split_whitespace().nth(1)
192        && let Ok(pages) = rss_pages.parse::<usize>()
193    {
194        return pages * 4096;
195    }
196    0
197}
198
199/// Blocks actually allocated, not apparent size, so a sparse file is not reported as
200/// memory the process took. Feeds the debug log line only.
201fn shm_usage_in(dir: &str) -> u64 {
202    use std::os::unix::fs::MetadataExt;
203    let mut total_size = 0;
204    if let Ok(entries) = std::fs::read_dir(dir) {
205        for entry in entries.flatten() {
206            if let Ok(metadata) = entry.metadata() {
207                total_size += metadata.blocks() * 512;
208            }
209        }
210    }
211    total_size
212}
213
214fn get_shm_usage_bytes() -> u64 {
215    shm_usage_in("/dev/shm")
216}
217
218use encoders::overlay::OverlayState;
219use encoders::software::MAX_STRIPE_CAPACITY;
220use encoders::{Codec, FrameEncoder, FrameSource};
221
222use smithay::reexports::wayland_protocols_misc::zwp_virtual_keyboard_v1::server::zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1;
223
224use pace::{FramePace, TickTrigger};
225use wayland::cursor::{Cursor, CursorJob};
226use wayland::frontend::{AppState, ClientState, FocusTarget, next_serial, wayland_time};
227
228smithay::backend::renderer::element::render_elements! {
229    pub CompositionElements<R, E> where R: ImportAll + ImportMem;
230    Space=SpaceRenderElements<R, E>,
231    Window=Wrap<E>,
232    Cursor=MemoryRenderBufferRenderElement<R>,
233    Surface=WaylandSurfaceRenderElement<R>,
234    Scaled=smithay::backend::renderer::element::utils::RescaleRenderElement<WaylandSurfaceRenderElement<R>>,
235    Ghost=smithay::backend::renderer::element::texture::TextureRenderElement<<R as smithay::backend::renderer::RendererSuper>::TextureId>,
236}
237
238/// Push a window's elements for an output at `origin` and `scale`: through the window policy's
239/// effect where one runs on it (the surface tree scaled into the effect's rectangle and blended
240/// at its alpha, the frame parts scaled and blended with it), else as the window stands with its
241/// frame parts beside it.
242fn push_window_elements<R>(
243    space: &smithay::desktop::Space<smithay::desktop::Window>,
244    wm: Option<&wayland::wm::WmState>,
245    renderer: &mut R,
246    elements: &mut Vec<CompositionElements<R, WaylandSurfaceRenderElement<R>>>,
247    window: &smithay::desktop::Window,
248    origin: Point<i32, smithay::utils::Logical>,
249    scale: f64,
250) where
251    R: smithay::backend::renderer::Renderer + ImportAll + ImportMem,
252    R::TextureId: Clone + Send + 'static,
253{
254    use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
255    use smithay::backend::renderer::element::utils::RescaleRenderElement;
256    let window_loc = space.element_location(window).unwrap_or_default() - origin;
257    let id = wayland::frontend::window_meta(window).map(|m| m.id);
258    let effect = id.and_then(|id| wm.and_then(|wm| wm.effects.get(&id).copied()));
259    if let Some(effect) = effect {
260        if effect.alpha <= 0.0 || effect.rect.w <= 0 || effect.rect.h <= 0 {
261            return;
262        }
263        let geometry = window.geometry();
264        if let Some(id) = id {
265            // The frame was drawn for the client area the policy configured, which the buffer
266            // on screen reaches only once the client commits it, so the parts scale from that
267            // area rather than from the buffer's size.
268            let client_loc = space.element_location(window).unwrap_or_default();
269            let client = wm
270                .and_then(|wm| wm.rects.get(&id).copied())
271                .unwrap_or(wm::Rect::new(
272                    client_loc.x,
273                    client_loc.y,
274                    geometry.size.w,
275                    geometry.size.h,
276                ));
277            elements.extend(
278                wayland::wm::frame_elements_through(
279                    wm, renderer, id, client, &effect, origin, scale,
280                )
281                .into_iter()
282                .map(CompositionElements::Cursor),
283            );
284        }
285        let sx = f64::from(effect.rect.w) / f64::from(geometry.size.w.max(1));
286        let sy = f64::from(effect.rect.h) / f64::from(geometry.size.h.max(1));
287        let target: Point<i32, smithay::utils::Logical> =
288            (effect.rect.x - origin.x, effect.rect.y - origin.y).into();
289        let origin_phys = target.to_physical_precise_round(scale);
290        let surface_loc = (target - geometry.loc).to_physical_precise_round(scale);
291        if let Some(surface) = window.wl_surface() {
292            let surface_elements: Vec<WaylandSurfaceRenderElement<R>> =
293                render_elements_from_surface_tree(
294                    renderer,
295                    &surface,
296                    surface_loc,
297                    Scale::from(scale),
298                    effect.alpha,
299                    smithay::backend::renderer::element::Kind::Unspecified,
300                );
301            elements.extend(surface_elements.into_iter().map(|element| {
302                CompositionElements::Scaled(RescaleRenderElement::from_element(
303                    element,
304                    origin_phys,
305                    Scale::from((sx, sy)),
306                ))
307            }));
308        }
309        return;
310    }
311    // The space places a window's geometry; the surface is drawn geometry offset to the left
312    // and up of it, where a client-side shadow lives, as the space's own render and its hit
313    // test agree.
314    let surface_loc = window_loc - window.geometry().loc;
315    elements.extend(
316        window
317            .render_elements(
318                renderer,
319                surface_loc.to_physical_precise_round(scale),
320                Scale::from(scale),
321                1.0,
322            )
323            .into_iter()
324            .map(CompositionElements::Space),
325    );
326    if let Some(id) = id {
327        let client_loc = space.element_location(window).unwrap_or_default();
328        elements.extend(
329            wayland::wm::frame_elements(wm, renderer, id, client_loc, origin, scale, 1.0)
330                .into_iter()
331                .map(CompositionElements::Cursor),
332        );
333    }
334}
335
336/// Push the ghosts of closing windows the policy animates, over the windows: each its last
337/// picture and frame scaled and blended through its effect.
338fn push_ghost_elements<R>(
339    renderer: &mut R,
340    elements: &mut Vec<CompositionElements<R, WaylandSurfaceRenderElement<R>>>,
341    wm: Option<&wayland::wm::WmState>,
342    origin: Point<i32, smithay::utils::Logical>,
343    scale: f64,
344) where
345    R: smithay::backend::renderer::Renderer + ImportAll + ImportMem,
346    R::TextureId: Clone + Send + 'static,
347{
348    let Some(state) = wm else {
349        return;
350    };
351    for id in state.ghosts.keys().copied() {
352        let ghost = wayland::wm::ghost_elements(wm, renderer, id, origin, scale);
353        elements.extend(ghost.frame.into_iter().map(CompositionElements::Cursor));
354        if let Some(element) = ghost.texture {
355            elements.push(CompositionElements::Ghost(element));
356        }
357        if let Some(element) = ghost.memory {
358            elements.push(CompositionElements::Cursor(element));
359        }
360    }
361}
362
363/// Push an output's wlr-layer surfaces on `target_layer` into `elements`, top-most first, the
364/// order `layer_map.layers()` reversed gives, each with its popups and subsurfaces. Shared by
365/// both renderers and by both of a frame's layer passes, which differ only in the layers they
366/// name.
367fn push_layer_elements<R>(
368    renderer: &mut R,
369    elements: &mut Vec<CompositionElements<R, WaylandSurfaceRenderElement<R>>>,
370    layer_map: &LayerMap,
371    target_layer: smithay::wayland::shell::wlr_layer::Layer,
372    scale: f64,
373) where
374    R: smithay::backend::renderer::Renderer + ImportAll + ImportMem,
375    R::TextureId: Clone + 'static,
376{
377    for surface in layer_map.layers().rev() {
378        if surface.layer() == target_layer
379            && let Some(geo) = layer_map.layer_geometry(surface)
380        {
381            elements.extend(
382                AsRenderElements::<R>::render_elements::<WaylandSurfaceRenderElement<R>>(
383                    surface,
384                    renderer,
385                    geo.loc.to_physical_precise_round(scale),
386                    Scale::from(scale),
387                    1.0,
388                )
389                .into_iter()
390                .map(CompositionElements::Surface),
391            );
392        }
393    }
394}
395
396/// The buffer a display is rendered into and a hardware encoder reads in place: one plane, for
397/// rendering. Intel's video processor, converting to 4:2:0, reads a linear surface at a pitch
398/// rounded up to 64 bytes whatever pitch its import declares, and Mesa gives a linear buffer
399/// allocated for rendering alone the pitch of its width, so a width that is no multiple of 16
400/// reaches the encoder sheared. A buffer the display engine may scan out carries that engine's aligned pitch, so a
401/// linear allocation whose pitch is not a multiple of 64 is made again for scanout as well, and
402/// that one is kept only where it came out aligned.
403pub(crate) fn alloc_render_target<T: std::os::fd::AsFd>(
404    gbm: &RawGbmDevice<T>,
405    width: u32,
406    height: u32,
407    format: GbmFormat,
408) -> std::io::Result<BufferObject<()>> {
409    let aligned = |bo: &BufferObject<()>| {
410        Into::<u64>::into(bo.modifier()) != 0 || bo.stride().is_multiple_of(64)
411    };
412    let bo = gbm.create_buffer_object::<()>(width, height, format, BufferObjectFlags::RENDERING)?;
413    if aligned(&bo) {
414        return Ok(bo);
415    }
416    match gbm.create_buffer_object::<()>(
417        width,
418        height,
419        format,
420        BufferObjectFlags::RENDERING | BufferObjectFlags::SCANOUT,
421    ) {
422        Ok(scanout) if aligned(&scanout) => Ok(scanout),
423        _ => Ok(bo),
424    }
425}
426
427/// Export the offscreen GBM render target as a Dmabuf so the very same GPU pixels can be both
428/// rendered into and encoded with no intervening copy — the linchpin of the zero-copy capture path.
429/// The returned dmabuf is the one handle the GLES renderer binds as its framebuffer AND a hardware
430/// encoder (NVENC through CUDA, or VAAPI) imports to read those pixels directly, which only works if
431/// the buffer is described precisely enough (fd, stride, DRM modifier) for the importer to interpret
432/// it. The render targets and host capture slots have one plane; retain the BO's format so an
433/// opaque host slot is not imported as a buffer with alpha.
434pub(crate) fn create_dmabuf_from_bo(bo: &BufferObject<()>) -> Dmabuf {
435    let fd = bo.fd().expect("Failed to get FD from GBM BO");
436    let modifier = bo.modifier();
437    let stride = bo.stride();
438    let width = bo.width();
439    let height = bo.height();
440
441    let drm_modifier = Modifier::from(Into::<u64>::into(modifier));
442
443    let mut builder = Dmabuf::builder(
444        (width as i32, height as i32),
445        bo.format(),
446        drm_modifier,
447        DmabufFlags::empty(),
448    );
449
450    builder.add_plane(fd, 0, stride);
451    builder.build().expect("Failed to build Dmabuf from GBM BO")
452}
453
454/// The full set of capture + encode parameters a consumer hands the backend.
455///
456/// A single value configures a capture session end to end: which display and backend, capture
457/// geometry and frame rate, the codec (striped JPEG or a full-frame video codec), the video
458/// quality and rate-control knobs, cursor and watermark options, the encode-device selection,
459/// and the optional recording socket. It derives `PartialEq` so the backend can detect when a
460/// live setting actually changed, and `Clone` so each capture pipeline can own its own copy. `Default` is the library's own baseline; the
461/// Python `CaptureSettings` class carries the defaults selkies sessions start from.
462#[derive(Clone, Debug, PartialEq)]
463pub struct CaptureSettings {
464    /// Wayland display key this capture binds to (0 = primary output); ignored on X11.
465    pub display_id: u32,
466    /// Backend choice: `Some(true)` or `Some(false)` force Wayland or X11; `None` follows
467    /// `WAYLAND_DISPLAY` (`wants_wayland`).
468    pub use_wayland: Option<bool>,
469    /// Compositor render node (Wayland): an explicit path wins; empty with `auto_gpu` set lets
470    /// the library pick one; empty without falls back to the encoder node.
471    pub render_node_path: String,
472    /// The encoder's render node by path, where the consumer names one beside
473    /// `encode_node_index`; the Wayland backend takes it as the fallback render node.
474    pub encode_node_path: String,
475    /// Auto-GPU request: empty for off, `true` for the first GPU, any other token for the
476    /// first GPU whose kernel identity matches (vendor name, driver name, DT prefix, PCI id).
477    pub auto_gpu: String,
478    /// Compositor cursor-theme size in pixels; `<= 0` keeps the theme default (24).
479    pub cursor_size: i32,
480    pub width: i32,
481    pub height: i32,
482    pub scale: f64,
483    pub capture_x: i32,
484    pub capture_y: i32,
485    pub target_fps: f64,
486    pub jpeg_quality: i32,
487    pub paint_over_jpeg_quality: i32,
488    pub use_paint_over_quality: bool,
489    /// Still frames before a cleanup, counted at `TRIGGER_FPS` (`paint_over_trigger`).
490    pub paint_over_trigger_frames: u32,
491    pub damage_block_threshold: u32,
492    pub damage_block_duration: u32,
493    pub codec: Codec,
494    pub video_crf: i32,
495    pub video_paintover_crf: i32,
496    pub video_paintover_burst_frames: i32,
497    pub video_fullcolor: bool,
498    /// Bits per sample asked of the encoder, 8 or 10; a session that cannot carry 10 stays at
499    /// 8 (`encoders::session_bit_depth` says which it runs).
500    pub video_bit_depth: i32,
501    pub video_streaming_mode: bool,
502    pub capture_cursor: bool,
503    /// Longest cursor edge the out-of-band cursor callback delivers; larger images are
504    /// downscaled (`<= 0` = uncapped). Compositing via `capture_cursor` is unaffected.
505    pub cursor_size_cap: i32,
506    pub watermark_path: String,
507    pub watermark_location_enum: i32,
508    pub encode_node_index: i32,
509    pub use_cpu: bool,
510    /// Whether a software session threads within a frame (`session::encode_threads`); off,
511    /// each software encoder codes and converts on one thread, and JPEG its stripes one after
512    /// another.
513    pub cpu_encode_threading: bool,
514    pub debug_logging: bool,
515    pub auto_adjust_screen_capture_size: bool,
516    pub recording_socket: String,
517    /// Wayland display of an EXTERNAL compositor to capture (host-capture mode);
518    /// empty composites own clients as usual.
519    pub wayland_host_display: String,
520    /// When true, encoders emit the raw payload without the per-stripe header byte block;
521    /// stripe metadata is then carried only on the frame attributes.
522    pub omit_stripe_headers: bool,
523    /// The consumer acknowledges the frames every client holds (`acknowledge_reference`), so an
524    /// anchor predicts from a frame every client was sent, and where a session keeps two
525    /// long-term references (H.265, and AV1, which keeps one otherwise) the newest every client
526    /// holds stays pinned beside the one the schedule marks. H.264 keeps one.
527    pub acknowledge_references: bool,
528    /// The most frames an H.264 or H.265 stream keeps for reference (its decoded picture
529    /// buffer), 0 for as many as the level admits up to `REFERENCE_FRAMES`: a decoder that holds
530    /// its whole buffer before it shows a picture (GStreamer's hardware decoders in WebKitGTK and
531    /// WPE) shows each that many frames later.
532    pub video_reference_frames: u32,
533    pub video_cbr_mode: bool,
534    pub video_bitrate_kbps: i32,
535    /// VBV size as a multiple of one frame's bit budget (bitrate/framerate), so it rescales with
536    /// live bitrate/fps changes. Under CBR, `<= 0` selects the policy default: 1.5 on an
537    /// infinite GOP, 3 when scheduled keyframes are enabled. Under CRF, a positive value caps
538    /// the session at a peak of `video_bitrate_kbps` with this buffer, which a live bitrate
539    /// change moves without a key frame (`encoders::CAPPED_VBV_FRAMES`), and `<= 0` leaves it
540    /// uncapped. Whether a session is capped is fixed when it opens.
541    pub video_vbv_multiplier: f64,
542    /// The rate the path a stream crosses carries, as its transport measures it, which a still
543    /// screen's constant-rate cleanup may run at (`pipeline::cleanup_rate`); 0 where none was
544    /// said, which keeps the cleanup to the session's rate. Live, through `update_link_rate`.
545    pub video_link_kbps: i32,
546    /// Seconds between scheduled recovery keyframes; `<= 0` keeps the GOP infinite
547    /// (IDRs only on demand: client join / reset, recorder connect).
548    pub keyframe_interval_s: f64,
549    /// Rate-controlled (CBR) QP clamp: `video_max_qp` bounds the quality FLOOR (screen text stays
550    /// legible under motion at the cost of overshooting impossible targets), `video_min_qp` bounds
551    /// bit WASTE on easy content. 0 keeps the encoder's own default, which for the software
552    /// H.264 session is the paint-over quality while the screen moves and none while a still
553    /// screen is cleaned up (`encoders::software::encode_cpu`); CRF/CQP modes pin their QP
554    /// directly and ignore these.
555    pub video_min_qp: i32,
556    pub video_max_qp: i32,
557}
558
559/// The per-frame decision/quality knobs every encoder re-reads from the settings on each
560/// tick, so they retune a running capture: x264, x265, and libvpx reconfigure, NVENC and
561/// VA-API retarget their constant quantizer, kvazaar and SVT-AV1 re-open, JPEG is stateless.
562/// A changed reference depth (`video_reference_frames`) re-opens an x264 session, on a key
563/// frame; a hardware session keeps the depth it opened with. Applied on the thread that owns
564/// the settings copy. Structural switches (encoder, chroma, RC mode, device) still need a
565/// capture restart.
566#[derive(Clone, Copy, Debug)]
567pub struct LiveTunables {
568    pub jpeg_quality: i32,
569    pub paint_over_jpeg_quality: i32,
570    pub use_paint_over_quality: bool,
571    pub paint_over_trigger_frames: u32,
572    pub video_crf: i32,
573    pub video_paintover_crf: i32,
574    pub video_paintover_burst_frames: i32,
575    pub video_streaming_mode: bool,
576    pub keyframe_interval_s: f64,
577    pub capture_cursor: bool,
578    pub cursor_size_cap: i32,
579    pub video_reference_frames: u32,
580}
581
582impl LiveTunables {
583    /// Snapshot the live-tunable subset out of a full settings value.
584    pub fn from_settings(s: &CaptureSettings) -> Self {
585        Self {
586            jpeg_quality: s.jpeg_quality,
587            paint_over_jpeg_quality: s.paint_over_jpeg_quality,
588            use_paint_over_quality: s.use_paint_over_quality,
589            paint_over_trigger_frames: s.paint_over_trigger_frames,
590            video_crf: s.video_crf,
591            video_paintover_crf: s.video_paintover_crf,
592            video_paintover_burst_frames: s.video_paintover_burst_frames,
593            video_streaming_mode: s.video_streaming_mode,
594            keyframe_interval_s: s.keyframe_interval_s,
595            capture_cursor: s.capture_cursor,
596            cursor_size_cap: s.cursor_size_cap,
597            video_reference_frames: s.video_reference_frames,
598        }
599    }
600
601    /// Write these live tunables back into a full settings value in place.
602    pub fn apply_to(&self, s: &mut CaptureSettings) {
603        s.jpeg_quality = self.jpeg_quality;
604        s.paint_over_jpeg_quality = self.paint_over_jpeg_quality;
605        s.use_paint_over_quality = self.use_paint_over_quality;
606        s.paint_over_trigger_frames = self.paint_over_trigger_frames;
607        s.video_crf = self.video_crf;
608        s.video_paintover_crf = self.video_paintover_crf;
609        s.video_paintover_burst_frames = self.video_paintover_burst_frames;
610        s.video_streaming_mode = self.video_streaming_mode;
611        s.keyframe_interval_s = self.keyframe_interval_s;
612        s.cursor_size_cap = self.cursor_size_cap;
613        s.capture_cursor = self.capture_cursor;
614        s.video_reference_frames = self.video_reference_frames;
615    }
616}
617
618impl Default for CaptureSettings {
619    fn default() -> Self {
620        Self {
621            display_id: 0,
622            use_wayland: None,
623            render_node_path: String::new(),
624            encode_node_path: String::new(),
625            auto_gpu: String::new(),
626            cursor_size: -1,
627            width: 1024,
628            height: 768,
629            scale: 1.0,
630            capture_x: 0,
631            capture_y: 0,
632            target_fps: 60.0,
633            jpeg_quality: 75,
634            paint_over_jpeg_quality: 95,
635            use_paint_over_quality: true,
636            paint_over_trigger_frames: 15,
637            damage_block_threshold: 10,
638            damage_block_duration: 30,
639            codec: Codec::Jpeg,
640            video_crf: 25,
641            video_paintover_crf: 18,
642            video_paintover_burst_frames: 5,
643            video_fullcolor: false,
644            video_bit_depth: 8,
645            video_streaming_mode: false,
646            capture_cursor: false,
647            cursor_size_cap: 128,
648            watermark_path: String::new(),
649            watermark_location_enum: 0,
650            encode_node_index: -2,
651            use_cpu: false,
652            cpu_encode_threading: true,
653            debug_logging: false,
654            auto_adjust_screen_capture_size: false,
655            recording_socket: String::new(),
656            wayland_host_display: String::new(),
657            omit_stripe_headers: false,
658            acknowledge_references: false,
659            video_reference_frames: 0,
660            video_cbr_mode: false,
661            video_bitrate_kbps: 4000,
662            video_vbv_multiplier: 0.0,
663            video_link_kbps: 0,
664            keyframe_interval_s: 0.0,
665            video_min_qp: 0,
666            video_max_qp: 0,
667        }
668    }
669}
670
671/// The frame rate `paint_over_trigger_frames` is counted at.
672const TRIGGER_FPS: f64 = 60.0;
673
674impl CaptureSettings {
675    /// Whether this capture goes through the Wayland backend: an explicit `use_wayland` wins
676    /// (selkies forwards --wayland / SELKIES_WAYLAND there); left unset, capture goes through
677    /// Wayland exactly when the session exposes a `WAYLAND_DISPLAY`.
678    pub fn wants_wayland(&self) -> bool {
679        if let Some(explicit) = self.use_wayland {
680            return explicit;
681        }
682        std::env::var("WAYLAND_DISPLAY")
683            .map(|v| !v.is_empty())
684            .unwrap_or(false)
685    }
686
687    /// Whether a still screen is cleaned up (`pipeline::cleanup_due`): a constant-rate video
688    /// stream always, whose rate control alone stops short of a clean picture where the rate is
689    /// low for the frame rate, to the paint-over quality (`video_paintover_crf`), the finest a
690    /// still screen is refined to, as fast as its path carries; otherwise only with the
691    /// paint-over on.
692    pub fn cleans_up(&self) -> bool {
693        self.use_paint_over_quality || (self.video_cbr_mode && self.codec.is_video())
694    }
695
696    /// `paint_over_trigger_frames` at the session's frame rate: below `TRIGGER_FPS` the same
697    /// time (2 frames of 15 at 8 fps), above it the same frames.
698    pub fn paint_over_trigger(&self) -> u32 {
699        let frames = self.paint_over_trigger_frames.max(1);
700        let scaled = (f64::from(frames) * self.target_fps / TRIGGER_FPS).ceil();
701        (scaled as u32).clamp(1, frames)
702    }
703}
704
705/// One live output as `(id, x, y, width, height, scale, capturing)`. Sizes are physical
706/// pixels and `(x, y)` is the layout offset.
707pub type OutputDesc = (u32, i32, i32, i32, i32, f64, bool);
708
709/// One mapped window as `(window_id, title, app_id, output_id, parked)`. A parked window
710/// has no output yet — a nested session's spare screens.
711pub type WindowDesc = (u32, String, String, u32, bool);
712
713/// Control messages sent from the `Compositor` handle's methods to the compositor thread.
714///
715/// Every interaction with a running capture crosses the thread boundary as one of these variants
716/// over the command channel: starting and stopping, injecting keyboard / pointer input, swapping
717/// the xkb keymap, serving the clipboard, changing live rate and per-frame tunables, and the
718/// computer-use queries that read back the screen, cursor, and geometry.
719pub enum ThreadCommand {
720    /// Start (or in-place reconfigure) the capture bound to output `display_id`.
721    /// `callback` receives every frame; `None` starts an internal capture with no consumer
722    /// (the built-in recorder taps the delivery layer).
723    StartCapture {
724        display_id: u32,
725        callback: Option<Arc<dyn crate::sink::FrameSink>>,
726        settings: CaptureSettings,
727    },
728    /// Stop the capture bound to output `display_id` (other displays keep running).
729    StopCapture {
730        display_id: u32,
731    },
732    /// Create an additional output: `WxH` physical pixels at fractional `scale`, mapped
733    /// into the layout at offset `(x, y)`. Replies false when the id is taken/reserved or
734    /// the GPU render target cannot be allocated.
735    CreateOutput {
736        id: u32,
737        width: i32,
738        height: i32,
739        x: i32,
740        y: i32,
741        scale: f64,
742        reply: std::sync::mpsc::Sender<bool>,
743    },
744    /// Create a view: another display over a rectangle of `owner`'s output, capturing it
745    /// without publishing a screen of its own. Replies false when the id is taken, the
746    /// owner is unknown or itself a view, or the rectangle leaves the owner's output.
747    CreateView {
748        id: u32,
749        owner: u32,
750        x: i32,
751        y: i32,
752        width: i32,
753        height: i32,
754        reply: std::sync::mpsc::Sender<bool>,
755    },
756    /// Resize a screen's output in place, keeping its position. Replies false for a view,
757    /// an unknown id, or a size that would leave one of the screen's views outside it.
758    ResizeOutput {
759        id: u32,
760        width: i32,
761        height: i32,
762        scale: f64,
763        reply: std::sync::mpsc::Sender<bool>,
764    },
765    /// Destroy a secondary output: its capture ends, its windows relocate to the primary
766    /// output. Replies false for the primary (id 0) or an unknown id.
767    DestroyOutput {
768        id: u32,
769        reply: std::sync::mpsc::Sender<bool>,
770    },
771    /// Remap an existing output (the primary included) to layout offset `(x, y)`: the
772    /// Space mapping, the offsets used for absolute input injection and cursor
773    /// compositing, and the windows placed on it all follow, and the output is damaged so
774    /// the next frames render correctly. Replies false for an unknown id.
775    RepositionOutput {
776        id: u32,
777        x: i32,
778        y: i32,
779        reply: std::sync::mpsc::Sender<bool>,
780    },
781    /// Reply with every live output as `(id, x, y, width, height, scale, capturing)`.
782    ListOutputs {
783        reply: std::sync::mpsc::Sender<Vec<OutputDesc>>,
784    },
785    /// Reply with how many displays this backend can back with real content: -1 when
786    /// self-compositing (outputs are created on demand), the host compositor's output
787    /// count in host-capture mode.
788    OutputCapacity {
789        reply: std::sync::mpsc::Sender<i64>,
790    },
791    /// Move the window with the given id onto output `output_id` (fullscreened there).
792    MoveWindowToOutput {
793        window_id: u32,
794        output_id: u32,
795        reply: std::sync::mpsc::Sender<bool>,
796    },
797    /// Reply with every mapped window as `(window_id, title, app_id, output_id)`.
798    ListWindows {
799        reply: std::sync::mpsc::Sender<Vec<WindowDesc>>,
800    },
801    /// Reply with the desktop as the installed window policy holds it; `None` without one.
802    DesktopView {
803        reply: std::sync::mpsc::Sender<Option<wm::DesktopView>>,
804    },
805    /// Hand the installed window policy a request about a window it placed, as a taskbar
806    /// would; reply whether a policy took it.
807    WindowRequest {
808        id: wm::WindowId,
809        request: wm::Request,
810        reply: std::sync::mpsc::Sender<bool>,
811    },
812    SetCursorCallback(Option<Arc<dyn crate::sink::CursorSink>>),
813    SetClipboardCallback(Arc<dyn crate::sink::ClipboardSink>),
814    /// Install the window policy that manages the compositor's windows from here on.
815    InstallWindowPolicy(Box<dyn crate::wm::WindowPolicy>),
816    /// Start Xwayland for the compositor's X11 clients; `notify` is told its display number.
817    StartXwayland {
818        notify: Option<std::sync::mpsc::Sender<u32>>,
819    },
820    /// Server-side clipboard offer: the compositor owns the selection and serves one payload
821    /// per `(mime, data)` entry (plus text aliases), so a paste takes the flavor it asks for.
822    SetClipboard {
823        entries: Vec<(String, Vec<u8>)>,
824    },
825    KeyboardKey {
826        scancode: u32,
827        state: u32,
828    },
829    /// A whole ordered run of key events in one message. Typing a paste one event at a
830    /// time costs a channel send and a calloop wake per event, which competes with the
831    /// render loop on the same thread; the caller still decides the sequence.
832    KeyboardKeys {
833        events: Vec<(u32, u32)>,
834    },
835    /// Set the seat's BASE keymap from a full XKB_KEYMAP_FORMAT_TEXT_V1 string. The
836    /// compositor's keymap policy rebuilds on top: overlay binds are re-spliced onto the new
837    /// base (same keycodes) and the combined keymap is applied in one swap.
838    SetKeymapString(String),
839    /// Set the seat's BASE layout from RMLVO names (empty strings = xkbcommon defaults);
840    /// replies whether compilation succeeded. Overlay binds rebuild on top as for
841    /// `SetKeymapString`.
842    SetXkbLayout {
843        rules: String,
844        model: String,
845        layout: String,
846        variant: String,
847        options: String,
848        reply: std::sync::mpsc::Sender<bool>,
849    },
850    /// Resolve keysyms to `(keycode, level)` against the seat keymap, overlay-binding every
851    /// keysym the base cannot produce — ONE keymap swap for the whole batch, and a keycode that
852    /// is currently pressed is never recycled. `(0, 0)` marks an unbindable keysym. Serves
853    /// computer-use only; selkies resolves its own keysyms and injects plain keycodes.
854    BindKeysyms {
855        keysyms: Vec<u32>,
856        reply: std::sync::mpsc::Sender<Vec<(u32, u32)>>,
857    },
858    /// Bind explicit `(keycode, keysym)` pairs onto the CURRENT base keymap and deliver it,
859    /// in one swap. The caller decides the assignment; this only assembles and applies, so the
860    /// base is neither re-sent nor recompiled and its reverse map is not rebuilt. Ordered with
861    /// key events on the one command channel, so the keys that need the new binds cannot
862    /// overtake it — no reply is awaited, and the caller's loop is never blocked on the swap.
863    SetKeymapOverlay {
864        binds: Vec<(u32, u32)>,
865    },
866    /// Debug/verification readback: currently pressed xkb keycodes plus the modifier state
867    /// bitmask (1 ctrl, 2 shift, 4 alt, 8 logo, 16 caps, 32 num, 64 altgr, 128 level5).
868    GetKeyboardState {
869        reply: std::sync::mpsc::Sender<(Vec<u32>, u32)>,
870    },
871    /// Reply with the smithay keyboard's keymap as an XKB_KEYMAP_FORMAT_TEXT_V1 string so a
872    /// consumer (selkies) can build its reverse keysym map from the IDENTICAL keymap.
873    GetXkbKeymap {
874        reply: std::sync::mpsc::Sender<String>,
875    },
876    /// Ack once every previously queued command has been fully processed (the channel is
877    /// FIFO). The atexit sweep sends StopCapture + Barrier and waits, so the interpreter never
878    /// exits while the calloop thread is still mid-teardown (an NVENC/CUDA session drop racing
879    /// process exit segfaults).
880    Barrier {
881        reply: std::sync::mpsc::Sender<()>,
882    },
883    /// `motion` numbers the move for `pointer_location` (`WaylandBackend`); zero leaves it
884    /// unnumbered.
885    PointerMotion {
886        x: f64,
887        y: f64,
888        motion: u64,
889    },
890    PointerRelativeMotion {
891        dx: f64,
892        dy: f64,
893        motion: u64,
894    },
895    /// `btn` is an evdev `BTN_` code by contract (e.g. 272 = BTN_LEFT, 273 = BTN_RIGHT,
896    /// 274 = BTN_MIDDLE, 0x113 = BTN_SIDE / 0x114 = BTN_EXTRA for back/forward) and is passed
897    /// straight through to smithay's pointer.
898    PointerButton {
899        btn: u32,
900        state: u32,
901    },
902    PointerAxis {
903        x: f64,
904        y: f64,
905    },
906    /// A finger's scroll by (dx, dy) physical pixels, as a touchpad delivers it: no discrete
907    /// steps, ended by `PointerScrollEnd`.
908    PointerScroll {
909        dx: f64,
910        dy: f64,
911    },
912    /// The finger lifted: an axis stop, which a toolkit takes as the start of its kinetic
913    /// scroll.
914    PointerScrollEnd,
915    UpdateCursorConfig {
916        render_on_framebuffer: bool,
917    },
918    /// Recreate the cursor theme handles at a new pixel size — the calloop's compositing
919    /// helper (the burned-in cursor) and, through its job channel, the `wl-cursor` worker's
920    /// (named-cursor PNG delivery). Replies false for a non-positive size.
921    SetCursorSize {
922        size: i32,
923        reply: std::sync::mpsc::Sender<bool>,
924    },
925    /// On-demand keyframe request (client reconnect / decoder reset) for one display's
926    /// capture: forces a send and an IDR even on a static screen.
927    RequestIdr {
928        display_id: u32,
929    },
930    /// A client lost frame `frame_id` of one display's capture, or its sender withheld it
931    /// (`withheld`): the frames after it stop predicting from it, so the next one decodes without a
932    /// keyframe.
933    InvalidateReference {
934        display_id: u32,
935        frame_id: u16,
936        withheld: bool,
937    },
938    /// Every client holds frame `frame_id` of one display's capture, or where not `held` was
939    /// sent it.
940    AcknowledgeReference {
941        display_id: u32,
942        frame_id: u16,
943        held: bool,
944    },
945    /// Live rate-control change for one display's capture (parity with the X11 `rate_dirty`
946    /// path). Each field is `None` when that dimension is unchanged.
947    UpdateRate {
948        display_id: u32,
949        bitrate_kbps: Option<i32>,
950        vbv_multiplier: Option<f64>,
951        fps: Option<f64>,
952    },
953    /// The rate the path one display's stream crosses carries
954    /// (`CaptureSettings::video_link_kbps`).
955    UpdateLinkRate {
956        display_id: u32,
957        link_kbps: i32,
958    },
959    /// Live per-frame tunables (quality / paint-over / streaming / cursor) for one
960    /// display's capture, mirrored to its readback encode thread — no restart.
961    UpdateTunables {
962        display_id: u32,
963        tunables: LiveTunables,
964    },
965    /// Owned pixels from one output's next render, compressed by the requesting thread.
966    CuScreenshot {
967        display_id: u32,
968        resp: std::sync::mpsc::Sender<Result<computer_use::ScreenshotFrame, String>>,
969        canceled: Arc<AtomicBool>,
970        deadline: Instant,
971    },
972    CuCursorPosition {
973        resp: std::sync::mpsc::Sender<(f64, f64)>,
974    },
975    CuGetInfo {
976        display_id: u32,
977        resp: std::sync::mpsc::Sender<(i32, i32, f64)>,
978    },
979}
980
981/// `memfd_create(2)` through the raw system call: glibc wraps it only from 2.27, and the wheels
982/// hold to the glibc 2.17 manylinux floor. The descriptor, or -1 with `errno` set.
983pub(crate) fn memfd_create(name: &std::ffi::CStr, flags: libc::c_uint) -> libc::c_int {
984    unsafe { libc::syscall(libc::SYS_memfd_create, name.as_ptr(), flags) as libc::c_int }
985}
986
987/// Read the kernel driver bound to a render node for encoder routing.
988///
989/// An `nvidia` driver name routes to NVENC; anything else routes to VA-API. The name is
990/// lowercased for case-insensitive substring matching, and is empty when the node has no driver
991/// link (treated as "no detectable GPU" by the selection logic).
992///
993/// sysfs lists every card the kernel sees, not the ones this process was given: a container
994/// handed a single GPU still finds the host's others under `/sys/class/drm`. A driver read
995/// from a node with no device file would route the encoder at hardware it cannot open, so an
996/// absent node answers the same as an unknown one.
997///
998/// # Arguments
999///
1000/// * `card_index` - DRM card index (maps to `/sys/class/drm/renderD{128 + card_index}`).
1001///
1002/// # Returns
1003///
1004/// Lowercased driver name, or an empty string if the node is unusable or has no driver link.
1005pub(crate) fn get_gpu_driver(card_index: i32) -> String {
1006    if !std::path::Path::new(&format!("/dev/dri/renderD{}", 128 + card_index)).exists() {
1007        return String::new();
1008    }
1009    let path = format!("/sys/class/drm/renderD{}/device/driver", 128 + card_index);
1010    match std::fs::read_link(&path) {
1011        Ok(link_path) => link_path.to_string_lossy().to_lowercase(),
1012        Err(_) => String::new(),
1013    }
1014}
1015
1016/// Whether an encode node's driver name routes to NVENC: NVIDIA, or unknown (no readable
1017/// render-node sysfs entry, the usual shape of an NVIDIA container without /dev/dri), so the
1018/// NVENC attempt runs before the CPU fallback. Any other named driver routes to VA-API. Shared
1019/// by the X11 and Wayland paths so both pick the same encoder for the same node.
1020pub(crate) fn driver_selects_nvenc(encode_driver: &str) -> bool {
1021    encode_driver.is_empty() || encode_driver.contains("nvidia")
1022}
1023
1024/// A DRM card's identity as the kernel reports it, read from the device's
1025/// sysfs `uevent` (DRIVER=, PCI_ID=, OF_COMPATIBLE_n=) with per-file fallbacks
1026/// (`vendor`, `modalias`, the `driver` symlink). uevent is uniform across buses
1027/// (PCI, platform/devicetree, USB) and readable in unprivileged containers.
1028struct CardIdentity {
1029    driver: String,
1030    pci_vendor: Option<u32>,
1031    compatibles: Vec<String>,
1032}
1033
1034/// Read a DRM card's `CardIdentity` from sysfs.
1035///
1036/// The device's `uevent` file is the primary source (`DRIVER=`, `PCI_ID=`, `OF_COMPATIBLE_n=`)
1037/// because it is uniform across buses and readable in unprivileged containers. Each field has a
1038/// fallback for cards whose `uevent` omits it: the `vendor` file for the PCI vendor, `modalias`
1039/// (`of:...C<compatible>`) for the devicetree compatibles, and the `driver` symlink for the name.
1040fn read_card_identity(device: &std::path::Path) -> CardIdentity {
1041    let mut id = CardIdentity {
1042        driver: String::new(),
1043        pci_vendor: None,
1044        compatibles: Vec::new(),
1045    };
1046    if let Ok(uevent) = std::fs::read_to_string(device.join("uevent")) {
1047        for line in uevent.lines() {
1048            if let Some(v) = line.strip_prefix("DRIVER=") {
1049                id.driver = v.trim().to_lowercase();
1050            } else if let Some(v) = line.strip_prefix("PCI_ID=") {
1051                id.pci_vendor = v
1052                    .split(':')
1053                    .next()
1054                    .and_then(|h| u32::from_str_radix(h, 16).ok());
1055            } else if line.starts_with("OF_COMPATIBLE_")
1056                && !line.starts_with("OF_COMPATIBLE_N")
1057                && let Some(v) = line.split_once('=').map(|x| x.1)
1058            {
1059                id.compatibles.push(v.trim().to_lowercase());
1060            }
1061        }
1062    }
1063    if id.pci_vendor.is_none() {
1064        id.pci_vendor = std::fs::read_to_string(device.join("vendor"))
1065            .ok()
1066            .and_then(|v| u32::from_str_radix(v.trim().trim_start_matches("0x"), 16).ok());
1067    }
1068    if id.compatibles.is_empty()
1069        && let Ok(modalias) = std::fs::read_to_string(device.join("modalias"))
1070    {
1071        let modalias = modalias.trim();
1072        if let Some(rest) = modalias.strip_prefix("of:") {
1073            id.compatibles
1074                .extend(rest.split('C').skip(1).map(|c| c.to_lowercase()));
1075        }
1076    }
1077    if id.driver.is_empty() {
1078        id.driver = std::fs::read_link(device.join("driver"))
1079            .map(|p| {
1080                p.file_name()
1081                    .map(|n| n.to_string_lossy().to_lowercase())
1082                    .unwrap_or_default()
1083            })
1084            .unwrap_or_default();
1085    }
1086    id
1087}
1088
1089/// Human vendor name -> PCI vendor IDs. The kernel has no such table (the
1090/// grouping is conventional), and pci.ids/hwdata is often absent in containers,
1091/// so this is the one mapping that must be embedded — kept to the names only.
1092const VENDOR_PCI_IDS: &[(&str, &[u32])] = &[
1093    ("nvidia", &[0x10de, 0x12d2]),
1094    ("amd", &[0x1002, 0x1022]),
1095    ("ati", &[0x1002]),
1096    ("intel", &[0x8086, 0x8087]),
1097    ("arm", &[0x13b5]),
1098    ("qualcomm", &[0x5143, 0x17cb]),
1099    ("broadcom", &[0x14e4]),
1100    ("apple", &[0x106b]),
1101    ("mediatek", &[0x14c3]),
1102    ("samsung", &[0x144d]),
1103    ("vmware", &[0x15ad]),
1104    ("microsoft", &[0x1414]),
1105    ("virtio", &[0x1af4]),
1106];
1107
1108/// Human name -> devicetree vendor prefix, only where they differ (a token
1109/// equal to the prefix itself, e.g. "qcom" or "rockchip", matches directly).
1110const OF_PREFIX_ALIASES: &[(&str, &str)] = &[
1111    ("mali", "arm"),
1112    ("qualcomm", "qcom"),
1113    ("adreno", "qcom"),
1114    ("broadcom", "brcm"),
1115    ("videocore", "brcm"),
1116    ("imagination", "img"),
1117    ("powervr", "img"),
1118];
1119
1120/// Does a card match the requested token? Accepted token forms, checked against
1121/// the identity the kernel itself reports: a kernel DRIVER name (exact, no
1122/// table), a raw PCI vendor ID ("0x10de"/"10de"), a devicetree vendor prefix
1123/// (literal first segment of any compatible), or a human vendor name resolved
1124/// through the small embedded alias maps above.
1125fn card_matches_token(token: &str, id: &CardIdentity) -> bool {
1126    if !id.driver.is_empty() && token == id.driver {
1127        return true;
1128    }
1129    if let Some(vid) = id.pci_vendor {
1130        if u32::from_str_radix(token.trim_start_matches("0x"), 16) == Ok(vid) {
1131            return true;
1132        }
1133        if let Some((_, ids)) = VENDOR_PCI_IDS.iter().find(|(n, _)| *n == token)
1134            && ids.contains(&vid)
1135        {
1136            return true;
1137        }
1138    }
1139    if !id.compatibles.is_empty() {
1140        let prefix = OF_PREFIX_ALIASES
1141            .iter()
1142            .find(|(n, _)| *n == token)
1143            .map(|(_, p)| *p)
1144            .unwrap_or(token);
1145        let want = format!("{prefix},");
1146        if id.compatibles.iter().any(|c| c.starts_with(&want)) {
1147            return true;
1148        }
1149    }
1150    false
1151}
1152
1153/// Parse an auto-GPU request (the CaptureSettings `auto_gpu` field, which selkies
1154/// fills from --auto-gpu / SELKIES_AUTO_GPU). `None` = disabled; `Some(None)` =
1155/// pick the first GPU overall ("true"); `Some(Some(token))` = pick the first GPU
1156/// whose kernel identity matches the case-insensitive token (vendor name, kernel
1157/// driver name, devicetree vendor prefix, or raw PCI vendor id).
1158fn parse_auto_gpu(value: &str) -> Option<Option<String>> {
1159    let value = value.to_lowercase();
1160    match value.as_str() {
1161        "" | "false" | "0" | "off" | "no" => None,
1162        "true" | "1" | "on" | "yes" => Some(None),
1163        token => Some(Some(token.to_string())),
1164    }
1165}
1166
1167/// The render node an `auto_gpu` request picks: None where the request is off or matches
1168/// no node.
1169fn auto_render_node(auto_gpu: &str) -> Option<String> {
1170    auto_select_render_node(parse_auto_gpu(auto_gpu)?.as_deref())
1171}
1172
1173/// The index `encode_node_index` counts a render node path by (`/dev/dri/renderD{128 + idx}`).
1174fn render_node_index(path: &str) -> Option<i32> {
1175    Some(path.strip_prefix("/dev/dri/renderD")?.parse::<i32>().ok()? - 128)
1176}
1177
1178/// The index of the first render node present, which a capture that names none encodes on:
1179/// renderD128 need not exist, as in a container handed one GPU of several.
1180fn first_render_node_index() -> Option<i32> {
1181    render_node_index(&auto_select_render_node(None)?)
1182}
1183
1184/// Resolve a usable `/dev/dri/renderD*` node, optionally matching a vendor/driver token.
1185///
1186/// Cards under `/sys/class/drm` are walked in numeric order, skipping cards with no render node
1187/// (e.g. IPMI/VGA). When `/sys/class/drm` is unreadable (container without `/sys`), falls
1188/// through to scanning `/dev/dri` directly — that fallback has no device identity so a `token`
1189/// request cannot be satisfied there.
1190///
1191/// # Arguments
1192///
1193/// * `token` - Optional vendor/driver filter: a kernel driver name, PCI vendor ID (`"0x10de"`),
1194///   devicetree vendor prefix, or human vendor name (`"nvidia"`, `"intel"`). `None` picks the
1195///   first available node.
1196///
1197/// # Returns
1198///
1199/// A `/dev/dri/renderD*` path, or `None` if no matching node exists.
1200fn auto_select_render_node(token: Option<&str>) -> Option<String> {
1201    let mut cards: Vec<(u32, std::path::PathBuf)> = std::fs::read_dir("/sys/class/drm")
1202        .into_iter()
1203        .flatten()
1204        .flatten()
1205        .filter_map(|e| {
1206            let num = e
1207                .file_name()
1208                .into_string()
1209                .ok()?
1210                .strip_prefix("card")?
1211                .parse::<u32>()
1212                .ok()?;
1213            Some((num, e.path()))
1214        })
1215        .collect();
1216    cards.sort_by_key(|(n, _)| *n);
1217    for (_, path) in &cards {
1218        if let Some(t) = token
1219            && !card_matches_token(t, &read_card_identity(&path.join("device")))
1220        {
1221            continue;
1222        }
1223        if let Ok(drm_entries) = std::fs::read_dir(path.join("device/drm")) {
1224            for de in drm_entries.flatten() {
1225                let name = de.file_name().into_string().unwrap_or_default();
1226                if name.starts_with("renderD") {
1227                    let dev = format!("/dev/dri/{}", name);
1228                    if std::path::Path::new(&dev).exists() {
1229                        return Some(dev);
1230                    }
1231                }
1232            }
1233        }
1234    }
1235    if token.is_some() {
1236        return None;
1237    }
1238    let mut nodes: Vec<String> = std::fs::read_dir("/dev/dri")
1239        .ok()?
1240        .flatten()
1241        .filter_map(|e| e.file_name().into_string().ok())
1242        .filter(|n| n.starts_with("renderD"))
1243        .collect();
1244    nodes.sort();
1245    nodes.first().map(|n| format!("/dev/dri/{}", n))
1246}
1247
1248/// One captured frame in flight from the calloop (render/readback) to the Wayland encode
1249/// thread: the slot's buffer (a readback's packed rows, or the zero-copy session's GPU target)
1250/// plus the per-frame inputs of the encode dispatch (damage, overlay animation); the IDR
1251/// request travels separately via the controls atomic.
1252pub struct WlFrame<B = Vec<u8>> {
1253    /// Pool slot id; travels with the buffer so recycle returns it to the right slot.
1254    id: usize,
1255    buf: B,
1256    /// Zero-copy: a buffer encoded in place of the slot's own, the host frame the host
1257    /// compositor blitted into or a client's own buffer (`scanout`); the slot's buffer then
1258    /// only holds the place in flight.
1259    source: Option<Dmabuf>,
1260    /// `source` is a client's buffer encoded as it stands (`scanout_candidate`): the encode
1261    /// thread waits out the client's GPU work on it first, and a session that cannot import
1262    /// it refuses the direct path for the capture rather than failing the session.
1263    scanout: bool,
1264    /// Readback: a client's shm buffer whose rows are encoded in place of the slot's
1265    /// (`scanout_shm`), read inside its pool's mapping on the encode thread; the slot's buffer
1266    /// then only holds the place in flight.
1267    shm: Option<SurfaceBuffer>,
1268    frame_id: u16,
1269    damage: Vec<Rectangle<i32, Physical>>,
1270    is_animated: bool,
1271    /// CLOCK_MONOTONIC nanoseconds at which the pixels were in hand.
1272    captured_ns: i64,
1273}
1274
1275/// Interior state of `WlFramePool`: the free-buffer list plus the single publish slot.
1276struct WlPoolInner<B> {
1277    free: Vec<(usize, B)>,
1278    slot: Option<WlFrame<B>>,
1279}
1280
1281/// Render->encode handoff for the Wayland readback paths, mirroring the X11 FramePool's
1282/// single-slot non-dropping design with one deliberate difference: the calloop thread is also
1283/// the compositor + input dispatcher, so it must NEVER block on the pool. `try_begin` hands
1284/// out a buffer only while the publish slot is empty, so `publish` cannot block, and a
1285/// saturated encoder throttles capture by SKIPPING ticks (compositor damage accumulates via
1286/// buffer age, so nothing is lost). Every published frame is encoded, in order: the H.264
1287/// reference chain stays contiguous exactly as on X11.
1288///
1289/// The zero-copy session runs the same split on GPU buffers: the calloop composites into one
1290/// of the pool's render targets and publishes it, and the encode thread encodes it in place,
1291/// so the compositor thread is free again as soon as the composite is.
1292pub struct WlFramePool<B = Vec<u8>> {
1293    inner: Mutex<WlPoolInner<B>>,
1294    cv: Condvar,
1295    stop: AtomicBool,
1296}
1297
1298impl WlFramePool<Vec<u8>> {
1299    /// Pre-allocate all `n` capture buffers up front (each `buf_len` bytes, all initially
1300    /// free) so the steady-state render/encode loop hands buffers around without ever allocating on
1301    /// the hot path.
1302    fn new(n: usize, buf_len: usize) -> Self {
1303        Self::of((0..n).map(|_| vec![0u8; buf_len]).collect())
1304    }
1305}
1306
1307impl<B> WlFramePool<B> {
1308    /// A pool of the buffers given, all initially free, numbered in order.
1309    fn of(buffers: Vec<B>) -> Self {
1310        Self {
1311            inner: Mutex::new(WlPoolInner {
1312                free: buffers.into_iter().enumerate().collect(),
1313                slot: None,
1314            }),
1315            cv: Condvar::new(),
1316            stop: AtomicBool::new(false),
1317        }
1318    }
1319
1320    /// Calloop: reserve a buffer for the next render/readback, NON-blocking. None means the
1321    /// encoder is still behind (slot full or every buffer in flight) -- skip this tick.
1322    /// Because the calloop is the only producer, a successful reservation guarantees the
1323    /// following `publish` finds the slot empty (the consumer only ever drains it).
1324    fn try_begin(&self) -> Option<(usize, B)> {
1325        let mut g = self.inner.lock().unwrap();
1326        if g.slot.is_some() {
1327            return None;
1328        }
1329        g.free.pop()
1330    }
1331
1332    /// Calloop: hand the filled buffer to the encode thread. Never blocks (see `try_begin`).
1333    fn publish(&self, frame: WlFrame<B>) {
1334        let mut g = self.inner.lock().unwrap();
1335        debug_assert!(g.slot.is_none());
1336        g.slot = Some(frame);
1337        drop(g);
1338        self.cv.notify_all();
1339    }
1340
1341    /// Calloop: return an unused reservation (render failed, readback skipped).
1342    fn cancel(&self, id: usize, buf: B) {
1343        self.inner.lock().unwrap().free.push((id, buf));
1344    }
1345
1346    /// Encode: block until a frame is published (Some) or the pool shuts down (None). The
1347    /// wait is bounded, re-checking `stop` as defense-in-depth against a lost wakeup.
1348    fn take(&self) -> Option<WlFrame<B>> {
1349        let mut g = self.inner.lock().unwrap();
1350        loop {
1351            if let Some(f) = g.slot.take() {
1352                return Some(f);
1353            }
1354            if self.stop.load(Ordering::Acquire) {
1355                return None;
1356            }
1357            let (gg, _) = self.cv.wait_timeout(g, WL_POOL_WAKE_QUANTUM).unwrap();
1358            g = gg;
1359        }
1360    }
1361
1362    /// Encode: return an encoded frame's buffer so the calloop can capture into it again.
1363    /// No notify: the only waiter (take) waits on the slot, and try_begin never blocks.
1364    fn recycle(&self, id: usize, buf: B) {
1365        self.inner.lock().unwrap().free.push((id, buf));
1366    }
1367
1368    /// Store stop under the lock so take() can't check stop==false and then park after the
1369    /// notify already fired (lost wakeup); notify after unlocking.
1370    fn shutdown(&self) {
1371        let g = self.inner.lock().unwrap();
1372        self.stop.store(true, Ordering::Release);
1373        drop(g);
1374        self.cv.notify_all();
1375    }
1376}
1377
1378/// Cross-thread controls for the Wayland encode thread, the X11 `Controls` scheme: the
1379/// UpdateRate handler stores the current values then flips `rate_dirty` with Release; the
1380/// encode thread swaps it with Acquire and re-reads the payload, never seeing it half-applied.
1381/// `force_idr` is swapped just before each encode, so an on-demand keyframe lands on the
1382/// frame ALREADY in flight instead of waiting one pipeline stage for the next publish.
1383/// `reference_reports` are what clients said of frames (lost, or held by every one of them),
1384/// drained ahead of the same encode.
1385pub struct WlEncodeControls {
1386    rate_dirty: AtomicBool,
1387    bitrate_kbps: AtomicI32,
1388    vbv_mult_milli: AtomicI32,
1389    fps_bits: AtomicU64,
1390    /// The rate the path carries (`CaptureSettings::video_link_kbps`), read every frame.
1391    link_kbps: AtomicI32,
1392    force_idr: AtomicBool,
1393    /// The zero-copy session refused a client's buffer handed to it as it stood (a layout
1394    /// its driver does not import), so the capture composites every frame from then on.
1395    scanout_refused: AtomicBool,
1396    /// The planes of one client dmabuf the session's encoder imports as it stands
1397    /// (`FrameEncoder::dmabuf_planes`), published by the encode thread; a buffer of more planes
1398    /// is composited.
1399    scanout_planes: AtomicU32,
1400    reference_reports: Mutex<Vec<encoders::reference::ReferenceReport>>,
1401    /// Pending per-frame tunables for the encode thread (mutex, not atomics: one struct, set
1402    /// rarely, read only when the dirty flag says so).
1403    tunables_dirty: AtomicBool,
1404    tunables: Mutex<Option<LiveTunables>>,
1405}
1406
1407impl WlEncodeControls {
1408    fn new() -> Self {
1409        Self {
1410            rate_dirty: AtomicBool::new(false),
1411            bitrate_kbps: AtomicI32::new(0),
1412            vbv_mult_milli: AtomicI32::new(0),
1413            fps_bits: AtomicU64::new(0),
1414            link_kbps: AtomicI32::new(0),
1415            force_idr: AtomicBool::new(false),
1416            scanout_refused: AtomicBool::new(false),
1417            scanout_planes: AtomicU32::new(1),
1418            reference_reports: Mutex::new(Vec::new()),
1419            tunables_dirty: AtomicBool::new(false),
1420            tunables: Mutex::new(None),
1421        }
1422    }
1423}
1424
1425/// Two buffers: one being encoded while the calloop fills the other. try_begin gates on the
1426/// publish slot, so a deeper pool would only add latency (staler frames), never overlap.
1427const WL_POOL_SURFACES: usize = 2;
1428/// The encode thread's bounded idle wait in `WlFramePool::take`.
1429const WL_POOL_WAKE_QUANTUM: Duration = Duration::from_millis(20);
1430/// How long a reconfigured output holds its frames while its clients answer the new size.
1431/// It is measured from the start, which rebuilds the encode path before there is any frame
1432/// to hold: the first one arrives roughly 300 ms later, so a window shorter than that
1433/// expires before it can suppress anything and the hold does nothing at all. This one
1434/// outlasts the rebuild and a client's own repaint, and is the whole delay an output that
1435/// no client ever draws on pays before its content reaches the viewer regardless.
1436const WL_CONTENT_HOLD: Duration = Duration::from_millis(500);
1437
1438/// Upper bounds for consumer-supplied capture geometry: keeps a hostile or buggy setting from
1439/// turning into a multi-GB `vec![]` allocation that would abort the process. 16384 covers every
1440/// real display wall; the fps/scale bounds keep timing math finite and sane.
1441const MAX_CAPTURE_DIM: i32 = 16384;
1442const MAX_FPS: f64 = 1000.0;
1443/// How often frame callbacks go out while nothing is being captured. Fast enough that a
1444/// client blocked on one keeps making progress, slow enough that an unwatched session is
1445/// not drawing frames nobody asked for.
1446const IDLE_FRAME_INTERVAL: Duration = Duration::from_millis(250);
1447const DEFAULT_FPS: f64 = 60.0;
1448
1449/// The frame period for a target rate. Settings are sanitized at the API boundary; a rate
1450/// outside the sane range takes the default here so a non-finite one never reaches
1451/// `Duration::from_secs_f64`.
1452fn frame_period(fps: f64) -> Duration {
1453    let fps = if fps.is_finite() && fps > 0.0 {
1454        fps.min(MAX_FPS)
1455    } else {
1456        DEFAULT_FPS
1457    };
1458    Duration::from_secs_f64(1.0 / fps)
1459}
1460
1461/// The frame period a capture renders at.
1462fn capture_period(cap: &wayland::frontend::WlCapture) -> Duration {
1463    frame_period(cap.settings.target_fps)
1464}
1465const MAX_SCALE: f64 = 8.0;
1466/// Wheel v120 units per unit of injected scroll value: selkies sends 10 per notch, and one
1467/// notch is 120, so both the seat's v120 and the host virtual pointer's discrete steps derive
1468/// from the same value.
1469pub(crate) const SCROLL_V120_PER_UNIT: f64 = 12.0;
1470
1471/// Shared capture stats: whichever thread owns the encoders counts frames/stripes and
1472/// composes `desc` + `n_stripes` (the encoder half of the 1 s debug log line); the calloop
1473/// log loads, prints, and resets the counters.
1474pub struct WlEncodeStats {
1475    frames: AtomicU32,
1476    stripes: AtomicU32,
1477    n_stripes: AtomicU32,
1478    desc: Mutex<String>,
1479    /// Frames the encode thread is done with, in the order they were published: the calloop
1480    /// hands a host frame back to the host once every frame that reads it is.
1481    recycled: AtomicU64,
1482}
1483
1484impl WlEncodeStats {
1485    fn new() -> Self {
1486        Self {
1487            frames: AtomicU32::new(0),
1488            stripes: AtomicU32::new(0),
1489            n_stripes: AtomicU32::new(1),
1490            desc: Mutex::new(String::new()),
1491            recycled: AtomicU64::new(0),
1492        }
1493    }
1494}
1495
1496/// Everything the Wayland encode thread needs, fixed for the life of one capture (a
1497/// StartCapture reconfigure tears the thread down and spawns a fresh one). Rate changes
1498/// flow through `controls`; damage and the IDR request arrive per-frame in `WlFrame`.
1499struct WlEncodeConfig {
1500    settings: CaptureSettings,
1501    /// Output/display id this encode loop serves; keys the recorder's delivery-layer tap.
1502    display_id: u32,
1503    /// GLES readback is RGBA; the pixman framebuffer is BGRA. Selects CSC + encoder input kind.
1504    use_gpu: bool,
1505    /// Attempt a HW (NVENC/VAAPI) readback session before falling back to the CPU encoders.
1506    try_gpu: bool,
1507    /// HW session handed back by the previous encode thread; reconfigured in place and
1508    /// reused when still compatible, sparing the stream a session rebuild.
1509    prior: Option<FrameEncoder>,
1510    /// The outgoing encode thread of a capture being restarted, joined here — off the
1511    /// calloop — before this thread builds anything. Everything the predecessor did
1512    /// therefore precedes everything this thread does, and its readback hardware session is
1513    /// inherited rather than rebuilt.
1514    predecessor: Option<std::thread::JoinHandle<Option<FrameEncoder>>>,
1515    /// The zero-copy session this thread drives in place of building a readback one.
1516    zero_copy: Option<ZeroCopySession>,
1517    /// The frames are a host compositor's (host capture), traced as they encode.
1518    host: bool,
1519    /// Weak, so the calloop's teardown owns the sink's lifetime: a strong handle held here
1520    /// could outlive the capture and unlink a successor's freshly bound socket path.
1521    recording_sink: Option<std::sync::Weak<crate::recording_sink::RecordingSink>>,
1522    deliver_tx: std::sync::mpsc::SyncSender<Vec<EncodedStripe>>,
1523    controls: Arc<WlEncodeControls>,
1524    stats: Arc<WlEncodeStats>,
1525    report: Arc<report::StreamReport>,
1526}
1527
1528/// A zero-copy hardware session (GLES render + same-GPU dmabuf encode), driven by the
1529/// capture's encode thread. The calloop opens it and takes it back at a restart to reuse in
1530/// place; the thread holds the lock for one encode at a time, never across a delivery.
1531struct ZeroCopySession {
1532    encoder: Arc<Mutex<Option<FrameEncoder>>>,
1533    /// The renderer's EGL display (`FrameSource::Dmabuf`), as an address, for a rebuild after
1534    /// repeated encode errors.
1535    egl_display: usize,
1536}
1537
1538/// What a Wayland encode thread encodes a frame from: the packed rows a readback filled, or
1539/// the GPU buffer a zero-copy session encodes in place.
1540trait WlPayload: Send + 'static {
1541    /// Encode the frame on a full-frame session. `source` stands in for the slot's own buffer
1542    /// (`WlFrame::source`).
1543    #[allow(clippy::too_many_arguments)]
1544    fn encode(
1545        &self,
1546        source: Option<&Dmabuf>,
1547        encoder: &mut FrameEncoder,
1548        width: i32,
1549        rgba: bool,
1550        frame_number: u64,
1551        qp: u32,
1552        force_idr: bool,
1553    ) -> Result<Vec<u8>, String>;
1554
1555    /// The rows, for the CPU path; `None` for a GPU buffer, which only a
1556    /// hardware session reads.
1557    fn rows(&self) -> Option<&[u8]>;
1558}
1559
1560impl WlPayload for Vec<u8> {
1561    fn encode(
1562        &self,
1563        _source: Option<&Dmabuf>,
1564        encoder: &mut FrameEncoder,
1565        width: i32,
1566        rgba: bool,
1567        frame_number: u64,
1568        qp: u32,
1569        force_idr: bool,
1570    ) -> Result<Vec<u8>, String> {
1571        // The readback rows go to the encoder as they are — BGRA from the pixman framebuffer
1572        // or a host frame, RGBA from a GLES readback: a hardware session converts on the GPU
1573        // and a software one on its own threads, so no color conversion runs here.
1574        encoder.encode_host(
1575            self,
1576            (width.max(0) as usize) * 4,
1577            rgba,
1578            frame_number,
1579            qp,
1580            force_idr,
1581        )
1582    }
1583
1584    fn rows(&self) -> Option<&[u8]> {
1585        Some(self)
1586    }
1587}
1588
1589impl WlPayload for Dmabuf {
1590    fn encode(
1591        &self,
1592        source: Option<&Dmabuf>,
1593        encoder: &mut FrameEncoder,
1594        _width: i32,
1595        _rgba: bool,
1596        frame_number: u64,
1597        qp: u32,
1598        force_idr: bool,
1599    ) -> Result<Vec<u8>, String> {
1600        encoder.encode_dmabuf(source.unwrap_or(self), frame_number, qp, force_idr)
1601    }
1602
1603    fn rows(&self) -> Option<&[u8]> {
1604        None
1605    }
1606}
1607
1608/// Build the readback-mode frame encoder on the thread that will own and drive it: the shared
1609/// ladder with host frames (`rgba` for a GLES readback, BGRA otherwise), with the GPU rungs
1610/// skipped when `try_gpu` is off (the recovery ladder's demotion). `None` is the CPU path, where
1611/// `encode_cpu` builds its own per-region state. A compatible NVENC
1612/// session handed over from the previous encode thread is reconfigured in place rather than
1613/// rebuilt, which would stall the stream.
1614fn build_readback_encoders(
1615    settings: &mut CaptureSettings,
1616    try_gpu: bool,
1617    rgba: bool,
1618    prior: Option<FrameEncoder>,
1619) -> Option<FrameEncoder> {
1620    let mut attempt = settings.clone();
1621    if !try_gpu {
1622        attempt.use_cpu = true;
1623    }
1624    let encoder =
1625        encoders::select_frame_encoder(&mut attempt, FrameSource::Host { rgba }, prior, "Wayland");
1626    settings.codec = attempt.codec;
1627    encoder
1628}
1629
1630/// Encode-thread body for the Wayland captures: drain published frames and run the full
1631/// encode dispatch, driving the encoders for the life of the capture.
1632///
1633/// Driving the encoders on this one thread is what lets the calloop overlap the next render
1634/// with this encode — the same capture‖encode split used on X11, minus the renderer, which is
1635/// genuinely calloop-affine (EGL/GBM/dmabuf and the pixman targets). A readback path's
1636/// encoders are created, driven, and dropped here; a zero-copy session is opened by the
1637/// calloop and driven here on the GPU buffers it composites into (`ZeroCopySession`). Each
1638/// published frame is processed in order so the H.264 reference chain stays contiguous:
1639///
1640/// 1. **Apply cross-thread changes**: live tunables and rate/VBV/fps updates are read here, on the
1641///    thread that drives the encoders (each `Acquire` swap pairs with the command handler's
1642///    `Release` store, so a payload is never seen half-applied). The IDR request is swapped as
1643///    late as possible, so a request that arrived while this frame was in flight is honored one
1644///    pipeline stage earlier than the next publish.
1645/// 2. **Dispatch by encoder**: a full-frame session runs `decide_hw_fullframe`, then hands only
1646///    the frames actually being encoded to it (`WlPayload::encode`): a zero-copy session's GPU
1647///    buffer in place, or the packed rows as they are (RGBA vs BGRA source chosen by the
1648///    renderer) — NVENC uploads and converts in hardware, a VA-API session uploads and converts
1649///    on the GPU, a software session converts on its encode threads; otherwise `encode_cpu`
1650///    runs the CPU path with compositor damage — striped JPEG, or H.264 through the build's
1651///    software encoder. The software H.264 path keeps an
1652///    infinite GOP, forcing an IDR only on an explicit request or the configured interval, and
1653///    an explicit request also forces a full JPEG resend for joiners.
1654/// 3. **Recycle then deliver**: the capture buffer is recycled BEFORE delivery so a slow consumer
1655///    never pins one, then the stripes go to the delivery thread through a single-slot `send` whose
1656///    blocking is the backpressure that overlaps delivery with the next render + encode.
1657///
1658/// A zero-copy session that keeps failing is rebuilt once, as a readback one is; one that
1659/// cannot be ends the thread, and the calloop demotes the capture to a readback path. On exit
1660/// a readback hardware session is handed back to the calloop so a restart can reuse it in
1661/// place when the new settings stay compatible (a plain `StopCapture` just drops it); a
1662/// zero-copy one stays in its `ZeroCopySession`.
1663fn wayland_encode_loop<B: WlPayload>(
1664    pool: &WlFramePool<B>,
1665    cfg: WlEncodeConfig,
1666) -> Option<FrameEncoder> {
1667    crate::boost_thread_priority(-10);
1668    let _report = report::enter(&cfg.report);
1669    let mut settings = cfg.settings;
1670    let zero_copy = cfg.zero_copy;
1671    let session = match zero_copy.as_ref() {
1672        Some(z) => z.encoder.clone(),
1673        None => {
1674            let inherited = cfg.predecessor.and_then(|h| h.join().ok().flatten());
1675            let video_encoder = build_readback_encoders(
1676                &mut settings,
1677                cfg.try_gpu,
1678                cfg.use_gpu,
1679                cfg.prior.or(inherited),
1680            );
1681            if cfg.try_gpu && video_encoder.is_none() {
1682                println!(
1683                    "[Wayland] Readback encode: no hardware encoder opened; encoding in software ({}).",
1684                    encoders::software_library(settings.codec)
1685                );
1686            }
1687            let n_stripes = encoders::software::stripe_count(settings.height, settings.codec);
1688            cfg.stats
1689                .n_stripes
1690                .store(n_stripes as u32, Ordering::Relaxed);
1691            *cfg.stats.desc.lock().unwrap() =
1692                encoder_desc(&settings, video_encoder.as_ref(), false);
1693            set_wayland_active_codec(cfg.display_id, Some(settings.codec));
1694            log_stream_settings("Wayland", &settings, n_stripes, video_encoder.as_ref());
1695            Arc::new(Mutex::new(video_encoder))
1696        }
1697    };
1698
1699    let width = settings.width;
1700    let height = settings.height;
1701    let mut stripes: Vec<StripeState> = Vec::with_capacity(MAX_STRIPE_CAPACITY);
1702    let mut hw_state = StripeState::default();
1703    // Mid-stream recovery state for the hardware session.
1704    let mut hw_error_streak: u32 = 0;
1705    let mut hw_rebuilt = false;
1706
1707    while let Some(mut f) = pool.take() {
1708        let mut guard = session.lock().unwrap();
1709        let video_encoder = &mut *guard;
1710        // A zero-copy session taken back by a restart, or given up, has nothing left to
1711        // encode with.
1712        if zero_copy.is_some() && video_encoder.is_none() {
1713            drop(guard);
1714            pool.recycle(f.id, f.buf);
1715            break;
1716        }
1717        let planes = video_encoder.as_ref().map_or(1, |e| e.dmabuf_planes());
1718        cfg.controls
1719            .scanout_planes
1720            .store(planes as u32, Ordering::Relaxed);
1721        if cfg.controls.tunables_dirty.swap(false, Ordering::Acquire)
1722            && let Some(t) = cfg.controls.tunables.lock().unwrap().take()
1723        {
1724            t.apply_to(&mut settings);
1725        }
1726        settings.video_link_kbps = cfg.controls.link_kbps.load(Ordering::Relaxed);
1727        if cfg.controls.rate_dirty.swap(false, Ordering::Acquire) {
1728            settings.video_bitrate_kbps = cfg.controls.bitrate_kbps.load(Ordering::Relaxed);
1729            settings.video_vbv_multiplier =
1730                f64::from(cfg.controls.vbv_mult_milli.load(Ordering::Relaxed)) / 1000.0;
1731            let fps = f64::from_bits(cfg.controls.fps_bits.load(Ordering::Relaxed));
1732            if fps > 0.0 {
1733                settings.target_fps = fps;
1734            }
1735            if let Some(enc) = video_encoder.as_mut()
1736                && let Err(e) = enc.reconfigure_rate(&settings)
1737            {
1738                // The failed re-open left no codec context: the next encode fails, and a
1739                // full streak makes that failure run the recovery ladder at once instead
1740                // of after a window of dead frames.
1741                eprintln!("[Wayland] rate reconfigure failed: {e}");
1742                hw_error_streak = HW_ERROR_RECOVERY_THRESHOLD - 1;
1743            }
1744        }
1745
1746        // A recorder connecting counts as a request, so the decision layer sends a
1747        // decodable frame even when the screen is static. A sink whose capture has been
1748        // torn down is already gone, and its last frames are not recorded.
1749        let recording_sink = cfg.recording_sink.as_ref().and_then(|w| w.upgrade());
1750        for report in std::mem::take(&mut *cfg.controls.reference_reports.lock().unwrap()) {
1751            hw_state.take_report(report);
1752            stripes.iter_mut().for_each(|st| st.take_report(report));
1753            let taken = match (video_encoder.as_mut(), report) {
1754                (Some(encoder), report) => encoder.take_report(report),
1755                (
1756                    None,
1757                    encoders::reference::ReferenceReport::Lost(frame_id)
1758                    | encoders::reference::ReferenceReport::Withheld(frame_id),
1759                ) => encoders::software::invalidate_reference(&mut stripes, frame_id),
1760                (None, encoders::reference::ReferenceReport::Held(frame_id)) => {
1761                    encoders::software::acknowledge_reference(&mut stripes, frame_id, true);
1762                    true
1763                }
1764                (None, encoders::reference::ReferenceReport::Sent(frame_id)) => {
1765                    encoders::software::acknowledge_reference(&mut stripes, frame_id, false);
1766                    true
1767                }
1768            };
1769            if !taken {
1770                cfg.controls.force_idr.store(true, Ordering::Relaxed);
1771            }
1772        }
1773        let requested_idr = cfg.controls.force_idr.swap(false, Ordering::Relaxed)
1774            || recording_sink
1775                .as_ref()
1776                .is_some_and(|s| s.should_force_idr());
1777
1778        let mut out: Vec<EncodedStripe> = Vec::new();
1779        let mut given_up = false;
1780        if let Some(encoder) = video_encoder.as_mut() {
1781            if encoder.take_lost_cleanup() {
1782                hw_state.lost_frame();
1783            }
1784            let damage = crate::pipeline::Damage::of_rects(&f.damage, width, height);
1785            let mut decision = crate::pipeline::decide_hw_fullframe(
1786                &mut hw_state,
1787                &settings,
1788                f.frame_id,
1789                damage,
1790                f.is_animated,
1791                requested_idr,
1792                crate::pipeline::EncoderQuality::of(encoder),
1793            );
1794            // Whether a rate binds the frame, read once before it is coded: the encode may bind
1795            // or release the session, and the frame was coded as it stood before.
1796            let bound = encoder.rate_bound(settings.video_cbr_mode);
1797            crate::pipeline::plan_refinement(
1798                &mut hw_state,
1799                &settings,
1800                &mut decision,
1801                damage,
1802                Some(&f.damage),
1803                encoder.refines(),
1804                encoder.last_quality(),
1805                bound,
1806            );
1807            if !decision.send && !encoder.holds_frame() {
1808                encoder.idle();
1809            }
1810            if decision.send || encoder.holds_frame() {
1811                let force_idr = decision.force_idr;
1812                decision.prepare(encoder);
1813                let encode_start_ns = wayland::host::now_ns();
1814                let outcome = if decision.send {
1815                    // A client's buffer may still be drawn into on the GPU when its commit
1816                    // arrives, and neither encoder waits on its fences itself.
1817                    if f.scanout
1818                        && let Some(client) = f.source.as_ref()
1819                    {
1820                        wayland::host::wait_gpu_done(client);
1821                    }
1822                    match f.shm.as_ref() {
1823                        Some(shm) => encode_shm_rows(shm, width, height, |rows| {
1824                            encoder.encode_client_host(
1825                                rows,
1826                                (width.max(0) as usize) * 4,
1827                                cfg.use_gpu,
1828                                u64::from(f.frame_id),
1829                                decision.target_qp,
1830                                force_idr,
1831                            )
1832                        }),
1833                        None => f.buf.encode(
1834                            f.source.as_ref(),
1835                            encoder,
1836                            width,
1837                            cfg.use_gpu,
1838                            u64::from(f.frame_id),
1839                            decision.target_qp,
1840                            force_idr,
1841                        ),
1842                    }
1843                } else {
1844                    encoder.push_held(u64::from(f.frame_id))
1845                };
1846                crate::pipeline::refinement_coded(
1847                    &mut hw_state,
1848                    &settings,
1849                    &decision,
1850                    crate::pipeline::EncoderQuality::of(encoder),
1851                    outcome.as_ref().map_or(0, Vec::len),
1852                    bound,
1853                    encoder.coded_capped(),
1854                );
1855                match outcome {
1856                    Ok(data) => {
1857                        hw_error_streak = 0;
1858                        hw_rebuilt = false;
1859                        if !data.is_empty() {
1860                            let encode_end_ns = wayland::host::now_ns();
1861                            if cfg.host && wayland::host::trace() {
1862                                eprintln!(
1863                                    "[HostTrace] output {} frame {} encoded +{:.2}ms {}B",
1864                                    cfg.display_id,
1865                                    f.frame_id,
1866                                    (encode_end_ns - f.captured_ns) as f64 / 1e6,
1867                                    data.len()
1868                                );
1869                            }
1870                            for (data, id, reference) in encoder.delivered_units(data, f.frame_id) {
1871                                out.push(EncodedStripe {
1872                                    data: Arc::new(data),
1873                                    codec: settings.codec,
1874                                    stripe_y_start: 0,
1875                                    stripe_height: height,
1876                                    frame_id: i32::from(id),
1877                                    timing: FrameTiming {
1878                                        capture_ns: f.captured_ns,
1879                                        encode_start_ns,
1880                                        encode_end_ns,
1881                                    },
1882                                    reference,
1883                                });
1884                            }
1885                        }
1886                    }
1887                    Err(e) if e == SHM_GONE => {
1888                        // The client took its pool away under the frame; the next frame
1889                        // composites whole and a key frame carries what this one held.
1890                        crate::log::debug!("[Wayland] Output {}: {e}.", cfg.display_id);
1891                        cfg.controls.force_idr.store(true, Ordering::Relaxed);
1892                    }
1893                    Err(e) if f.scanout => {
1894                        // The session is sound; the client's buffer is one its driver does
1895                        // not import, so the capture composites from here on. The frame it
1896                        // carried is lost, and a key frame puts its changes into the stream.
1897                        println!(
1898                            "[Wayland] Output {}: the encoder does not take the client's buffers in place ({e}); compositing.",
1899                            cfg.display_id
1900                        );
1901                        cfg.controls.scanout_refused.store(true, Ordering::Relaxed);
1902                        cfg.controls.force_idr.store(true, Ordering::Relaxed);
1903                    }
1904                    Err(e) => {
1905                        // One line per recovery window: a session failing at frame rate would
1906                        // otherwise write a line per frame for the life of the capture.
1907                        if hw_error_streak.is_multiple_of(HW_ERROR_RECOVERY_THRESHOLD) {
1908                            eprintln!("[Wayland] HW encode error: {e}");
1909                        }
1910                        hw_error_streak = hw_error_streak.saturating_add(1);
1911                        if hw_error_streak >= HW_ERROR_RECOVERY_THRESHOLD {
1912                            // The session persistently fails after having worked (driver
1913                            // hiccup, CUDA pressure from a co-tenant): rebuild it once with the
1914                            // startup selection, then demote — a readback session to the
1915                            // software encoders, a zero-copy one to a readback path, which the
1916                            // calloop builds once this thread ends. Streaming black frames
1917                            // forever is not an option. A session whose encodes keep failing
1918                            // still constructs, so the rebuild only counts as recovery until
1919                            // the next streak; otherwise the stream would rebuild in a loop.
1920                            hw_error_streak = 0;
1921                            let try_gpu = !hw_rebuilt;
1922                            let zero_copy_label = if zero_copy.is_some() {
1923                                "zero-copy"
1924                            } else {
1925                                "readback"
1926                            };
1927                            if try_gpu {
1928                                eprintln!(
1929                                    "[Wayland] rebuilding {zero_copy_label} HW encoder after repeated encode errors."
1930                                );
1931                            } else if zero_copy.is_some() {
1932                                eprintln!(
1933                                    "[Wayland] zero-copy HW encoder unrecoverable; demoting to readback encode."
1934                                );
1935                            } else {
1936                                eprintln!(
1937                                    "[Wayland] readback HW encoder unrecoverable; demoting to software encoding ({}).",
1938                                    encoders::software_library(settings.codec)
1939                                );
1940                            }
1941                            // The broken session is released before its replacement is opened:
1942                            // the failure it recovers from is usually device memory pressure,
1943                            // and holding both at once is what would make the rebuild fail too.
1944                            drop(video_encoder.take());
1945                            *video_encoder = match zero_copy.as_ref() {
1946                                Some(z) if try_gpu => {
1947                                    let mut attempt = settings.clone();
1948                                    encoders::select_frame_encoder(
1949                                        &mut attempt,
1950                                        FrameSource::Dmabuf {
1951                                            egl_display: z.egl_display as *const std::ffi::c_void,
1952                                        },
1953                                        None,
1954                                        "Wayland",
1955                                    )
1956                                }
1957                                Some(_) => None,
1958                                None => build_readback_encoders(
1959                                    &mut settings,
1960                                    try_gpu,
1961                                    cfg.use_gpu,
1962                                    None,
1963                                ),
1964                            };
1965                            if zero_copy.is_some() && video_encoder.is_none() {
1966                                given_up = true;
1967                            } else {
1968                                if !try_gpu {
1969                                    report::encoder_reason(
1970                                        "the hardware encoder failed repeatedly and was given up",
1971                                    );
1972                                }
1973                                hw_rebuilt = try_gpu && video_encoder.is_some();
1974                                cfg.controls.force_idr.store(true, Ordering::Relaxed);
1975                                let n = encoders::software::stripe_count(
1976                                    settings.height,
1977                                    settings.codec,
1978                                );
1979                                cfg.stats.n_stripes.store(n as u32, Ordering::Relaxed);
1980                                *cfg.stats.desc.lock().unwrap() = encoder_desc(
1981                                    &settings,
1982                                    video_encoder.as_ref(),
1983                                    zero_copy.is_some(),
1984                                );
1985                                set_wayland_active_codec(cfg.display_id, Some(settings.codec));
1986                                log_stream_settings(
1987                                    "Wayland",
1988                                    &settings,
1989                                    n,
1990                                    video_encoder.as_ref(),
1991                                );
1992                            }
1993                        }
1994                    }
1995                }
1996            }
1997        } else if let Some(rows) = f.buf.rows() {
1998            let mut damage = std::mem::take(&mut f.damage);
1999            if f.is_animated {
2000                damage.push(Rectangle::new((0, 0).into(), (width, height).into()));
2001            }
2002            let encode_start_ns = wayland::host::now_ns();
2003            let frame_id = f.frame_id;
2004            let mut run = |rows: &[u8]| {
2005                encoders::software::encode_cpu(
2006                    &mut stripes,
2007                    rows,
2008                    width,
2009                    height,
2010                    &damage,
2011                    &settings,
2012                    frame_id,
2013                    cfg.use_gpu,
2014                    false,
2015                    requested_idr,
2016                )
2017            };
2018            out = match f.shm.as_ref() {
2019                Some(shm) => match encode_shm_rows(shm, width, height, |rows| Ok(run(rows))) {
2020                    Ok(out) => out,
2021                    Err(e) => {
2022                        crate::log::debug!("[Wayland] Output {}: {e}.", cfg.display_id);
2023                        cfg.controls.force_idr.store(true, Ordering::Relaxed);
2024                        Vec::new()
2025                    }
2026                },
2027                None => run(rows),
2028            };
2029            FrameTiming::stamp(&mut out, f.captured_ns, encode_start_ns);
2030        }
2031        drop(guard);
2032
2033        let WlFrame { id, buf, .. } = f;
2034        pool.recycle(id, buf);
2035        cfg.stats.recycled.fetch_add(1, Ordering::Release);
2036        // An unserved request stays armed: on an infinite GOP an IDR lost to an encode
2037        // error or skip would never self-heal.
2038        if requested_idr && out.is_empty() {
2039            cfg.controls.force_idr.store(true, Ordering::Relaxed);
2040        }
2041        if !out.is_empty() {
2042            cfg.stats.frames.fetch_add(1, Ordering::Relaxed);
2043            cfg.stats
2044                .stripes
2045                .fetch_add(out.len() as u32, Ordering::Relaxed);
2046            if let Some(ref socket) = recording_sink {
2047                socket.write_frame(&out, settings.width, settings.height);
2048            }
2049            crate::recorder::wayland_tap(cfg.display_id, &out);
2050            let _ = cfg.deliver_tx.send(out);
2051        }
2052        if given_up {
2053            break;
2054        }
2055    }
2056    let mut video_encoder = if zero_copy.is_some() {
2057        None
2058    } else {
2059        session.lock().unwrap().take()
2060    };
2061    // The host buffers NVENC pinned are this thread's pool's, freed once the thread ends. They
2062    // are unregistered while still mapped: an address freed while registered stays known to CUDA
2063    // at its old length, and a larger buffer a successor's pool gets there then fails its upload.
2064    if let Some(FrameEncoder::Nvenc(enc)) = video_encoder.as_mut() {
2065        enc.release_pinned_hosts();
2066    }
2067    crate::log::debug!(
2068        "[Wayland] Encode thread exiting (hw={}, stripes={}).",
2069        video_encoder.is_some(),
2070        stripes.len()
2071    );
2072    video_encoder
2073}
2074
2075/// Compose the encoder half of the 1 s debug log line (backend, colorspace, rate control) for
2076/// whichever thread owns the encoders.
2077fn encoder_desc(
2078    settings: &CaptureSettings,
2079    video_encoder: Option<&FrameEncoder>,
2080    zero_copy: bool,
2081) -> String {
2082    if !settings.codec.is_video() {
2083        return format!("JPEG Q:{}", settings.jpeg_quality);
2084    }
2085    let copy_mode = if zero_copy { "ZeroCopy" } else { "Readback" };
2086    let backend = match video_encoder {
2087        Some(enc) if enc.is_hardware() => format!("{} ({})", enc.backend_name(), copy_mode),
2088        Some(enc) => format!("CPU {}", enc.backend_name()),
2089        None => format!("CPU {}", encoders::software_library(Codec::H264)),
2090    };
2091    let is_444 = encoders::session_fullcolor(video_encoder, settings);
2092    let cs_str = if is_444 { "CS_IN:I444" } else { "CS_IN:I420" };
2093    let range_str = if encoders::session_full_range(video_encoder, settings) {
2094        "FR"
2095    } else {
2096        "LR"
2097    };
2098    format!(
2099        "{} ({}) {} {} {}",
2100        settings.codec.display(),
2101        backend,
2102        cs_str,
2103        range_str,
2104        encoders::rate_desc(
2105            settings,
2106            video_encoder.and_then(FrameEncoder::fixed_rate_control),
2107            encoders::session_capped(video_encoder, settings)
2108        )
2109    )
2110}
2111
2112/// One-shot "Stream settings active" line, printed by the thread that owns the encoders
2113/// once the selection is final (calloop for zero-copy, encode thread for readback, the
2114/// X11 zero-copy backends at their start), tagged for the backend that prints it.
2115pub(crate) fn log_stream_settings(
2116    tag: &str,
2117    settings: &CaptureSettings,
2118    n_stripes: usize,
2119    video_encoder: Option<&FrameEncoder>,
2120) {
2121    let backend = video_encoder.map(|enc| (enc.backend_name(), enc.is_hardware()));
2122    let fixed_rate = video_encoder.and_then(FrameEncoder::fixed_rate_control);
2123    let holds = video_encoder.map_or_else(
2124        || encoders::software::stripes_hold_quantizer(settings),
2125        FrameEncoder::holds_quantizer,
2126    );
2127    let fullcolor = encoders::session_fullcolor(video_encoder, settings);
2128    let full_range = encoders::session_full_range(video_encoder, settings);
2129    let capped = encoders::session_capped(video_encoder, settings);
2130    log_stream_settings_of(
2131        tag, settings, n_stripes, backend, fixed_rate, holds, capped, fullcolor, full_range,
2132    );
2133    report::bit_depth(encoders::session_bit_depth(video_encoder, settings));
2134    report::capped(capped);
2135}
2136
2137/// The "Stream settings active" line for a backend named outright, `(name, hardware)`, where
2138/// the session is not a `FrameEncoder` (NvFBC's own NVENC session); `None` is the CPU path, `fixed_rate` is the backend's one rate control where it has one
2139/// (`FrameEncoder::fixed_rate_control`), `holds` says whether the session holds a cleanup at
2140/// the quantizer asked for (`FrameEncoder::holds_quantizer`), and `capped` whether a constant
2141/// quality runs under the peak of `video_bitrate_kbps` (`encoders::session_capped`).
2142#[allow(clippy::too_many_arguments)]
2143pub(crate) fn log_stream_settings_of(
2144    tag: &str,
2145    settings: &CaptureSettings,
2146    n_stripes: usize,
2147    backend: Option<(&str, bool)>,
2148    fixed_rate: Option<&str>,
2149    holds: bool,
2150    capped: bool,
2151    fullcolor: bool,
2152    full_range: bool,
2153) {
2154    report::stream(settings, n_stripes, backend, fullcolor, full_range);
2155    println!(
2156        "{}",
2157        stream_settings_line(
2158            tag, settings, n_stripes, backend, fixed_rate, holds, capped, fullcolor, full_range
2159        )
2160    );
2161}
2162
2163/// The text of the "Stream settings active" line, which says what the session applies rather
2164/// than what it was asked for (`encoders::rate_desc`, `encoders::paint_over_desc`).
2165#[allow(clippy::too_many_arguments)]
2166fn stream_settings_line(
2167    tag: &str,
2168    settings: &CaptureSettings,
2169    n_stripes: usize,
2170    backend: Option<(&str, bool)>,
2171    fixed_rate: Option<&str>,
2172    holds: bool,
2173    capped: bool,
2174    fullcolor: bool,
2175    full_range: bool,
2176) -> String {
2177    let mut log_msg = format!(
2178        "[{tag}] Stream settings active -> Res: {}x{} | FPS: {:.1} | Stripes: {}",
2179        settings.width, settings.height, settings.target_fps, n_stripes
2180    );
2181
2182    if !settings.codec.is_video() {
2183        log_msg.push_str(&format!(
2184            " | Mode: JPEG | Quality: {}",
2185            settings.jpeg_quality
2186        ));
2187        if let Some(paint_over) = encoders::paint_over_desc(settings, fixed_rate, holds) {
2188            log_msg.push_str(&format!(" | {paint_over}"));
2189        }
2190    } else {
2191        let encoder_type = match backend {
2192            Some((name, _)) => name,
2193            None => encoders::software_library(Codec::H264),
2194        };
2195        log_msg.push_str(&format!(
2196            " | Mode: {} ({})",
2197            settings.codec.display(),
2198            encoder_type
2199        ));
2200
2201        if settings.video_streaming_mode {
2202            log_msg.push_str(" Streaming");
2203        }
2204
2205        log_msg.push_str(&format!(
2206            " | {}",
2207            encoders::rate_desc(settings, fixed_rate, capped)
2208        ));
2209        if let Some(paint_over) = encoders::paint_over_desc(settings, fixed_rate, holds) {
2210            log_msg.push_str(&format!(" | {paint_over}"));
2211        }
2212
2213        log_msg.push_str(&format!(
2214            " | Colorspace: {}",
2215            encoders::colorspace_desc(fullcolor, full_range)
2216        ));
2217    }
2218
2219    log_msg.push_str(&format!(
2220        " | Damage Thresh: {}f | Damage Dur: {}f",
2221        settings.damage_block_threshold, settings.damage_block_duration
2222    ));
2223    log_msg
2224}
2225
2226#[cfg(test)]
2227mod stream_settings_line_tests {
2228    use super::*;
2229
2230    /// The line never states a rate control the encoder does not run: a CRF session names no
2231    /// bitrate, a backend with one rate control names that one in either mode, and a JPEG
2232    /// paint-over that cannot raise the quality is not named.
2233    #[test]
2234    fn the_line_states_what_the_session_applies() {
2235        let crf = CaptureSettings {
2236            codec: Codec::H264,
2237            video_crf: 25,
2238            video_bitrate_kbps: 8000,
2239            use_paint_over_quality: false,
2240            ..Default::default()
2241        };
2242        let line = |s: &CaptureSettings, fixed: Option<&str>| {
2243            stream_settings_line(
2244                "X11",
2245                s,
2246                1,
2247                Some(("NVENC", true)),
2248                fixed,
2249                fixed.is_none(),
2250                false,
2251                false,
2252                false,
2253            )
2254        };
2255        let nvenc = line(&crf, None);
2256        assert!(
2257            nvenc.contains("| CRF: 25 |") && !nvenc.contains("VBV") && !nvenc.contains("8000"),
2258            "{nvenc}"
2259        );
2260        let v4l2 = line(&crf, Some("VBR"));
2261        assert!(
2262            v4l2.contains("| VBR 8000 |") && !v4l2.contains("CRF"),
2263            "{v4l2}"
2264        );
2265        assert!(line(&crf, Some("CBR")).contains("| CBR 8000 |"));
2266        assert!(
2267            line(
2268                &CaptureSettings {
2269                    video_cbr_mode: true,
2270                    ..crf
2271                },
2272                None
2273            )
2274            .contains("| CBR 8000 |")
2275        );
2276        let jpeg = CaptureSettings {
2277            codec: Codec::Jpeg,
2278            jpeg_quality: 75,
2279            paint_over_jpeg_quality: 95,
2280            ..Default::default()
2281        };
2282        assert!(line(&jpeg, None).contains("| PaintOver Q: 95 "));
2283        assert!(
2284            !line(
2285                &CaptureSettings {
2286                    paint_over_jpeg_quality: 60,
2287                    ..jpeg
2288                },
2289                None
2290            )
2291            .contains("PaintOver")
2292        );
2293    }
2294}
2295
2296/// Tear down a capture's encode/delivery threads and pools, returning both join handles as
2297/// `(deliver, encode)` for the caller to place. NEITHER is joined here: this runs on the
2298/// calloop thread, which also dispatches Wayland clients and input, and either thread can be
2299/// parked behind a consumer still inside Python. A restart hands each handle to its
2300/// successor thread, which joins it off the event loop — the encode successor inheriting the
2301/// readback hardware session, the deliver successor keeping stripes in capture order across
2302/// the restart — and a plain stop hands both to the reaper. The zero-copy session (if any)
2303/// is left on the capture for the caller to reuse or drop, and so are the host frames its
2304/// thread may still be reading (`release_host_inflight`).
2305fn teardown_capture(
2306    cap: &mut wayland::frontend::WlCapture,
2307) -> (
2308    Option<std::thread::JoinHandle<()>>,
2309    Option<std::thread::JoinHandle<Option<FrameEncoder>>>,
2310) {
2311    if let Some(p) = cap.encode_pool.take() {
2312        p.shutdown();
2313    }
2314    if let Some(p) = cap.gpu_pool.take() {
2315        p.shutdown();
2316    }
2317    if let Some(flag) = cap.deliver_discard.take() {
2318        flag.store(true, Ordering::Relaxed);
2319    }
2320    let encode_join = cap.encode_join.take();
2321    if let Some(tx) = cap.deliver_tx.take() {
2322        drop(tx);
2323    }
2324    let join = cap.deliver_join.take();
2325    release_scanout_inflight(cap, true);
2326    // The last strong reference: dropping it here, before a following start binds its own
2327    // sink, is what keeps the outgoing sink's unlink from stripping the successor's socket
2328    // path. The encode thread holds only a Weak.
2329    cap.recording_sink = None;
2330    (join, encode_join)
2331}
2332
2333/// Stop a capture's zero-copy session: its encode thread ends after the frame in flight, and
2334/// its render targets go. False where none ran.
2335fn end_zero_copy(cap: &mut wayland::frontend::WlCapture) -> bool {
2336    let Some(pool) = cap.gpu_pool.take() else {
2337        return false;
2338    };
2339    pool.shutdown();
2340    cap.zero_copy = None;
2341    cap.gpu_ring.clear();
2342    cap.host_still = None;
2343    release_scanout_inflight(cap, true);
2344    true
2345}
2346
2347/// Keep `frame` as the display's host content. Under a zero-copy session the frame it replaces
2348/// goes back to the host only once no frame in flight to the encode thread reads it.
2349fn retain_host_frame(
2350    host: &wayland::host::HostSession,
2351    cap: Option<&mut wayland::frontend::WlCapture>,
2352    display_id: u32,
2353    frame: wayland::host::HostFrame,
2354) {
2355    match cap {
2356        Some(cap) if cap.gpu_pool.is_some() => {
2357            if let Some(old) = host.swap_retained(display_id, frame) {
2358                cap.host_inflight.push((cap.host_retained_use, old));
2359            }
2360            cap.host_retained_use = 0;
2361            release_host_inflight(Some(host), cap, display_id, false);
2362        }
2363        _ => host.retain_frame(display_id, frame),
2364    }
2365}
2366
2367/// `HostSession::detach_retained` for a GPU frame, which only a zero-copy session takes: the
2368/// renderer copies it into a buffer of the capture's own, which re-encodes then read, and the
2369/// stream's buffer goes back once no frame in flight reads it.
2370fn detach_retained_gpu(
2371    renderer: Option<&mut GlesRenderer>,
2372    gbm: Option<&RawGbmDevice<std::fs::File>>,
2373    host: &wayland::host::HostSession,
2374    cap: &mut wayland::frontend::WlCapture,
2375    display_id: u32,
2376    size: (i32, i32),
2377) {
2378    if !host.is_portal() || cap.gpu_pool.is_none() {
2379        return;
2380    }
2381    // The copy an encode in flight may still read is not drawn over.
2382    if cap.host_inflight.iter().any(|(_, f)| f.is_detached()) {
2383        return;
2384    }
2385    let Some(src) = host.with_retained(display_id, |r| {
2386        r.filter(|f| !f.is_detached() && (f.width, f.height) == size)
2387            .and_then(|f| f.dmabuf.clone())
2388    }) else {
2389        return;
2390    };
2391    let (Some(renderer), Some(gbm)) = (renderer, gbm) else {
2392        return;
2393    };
2394    let fits = |bo: &BufferObject<()>| (bo.width() as i32, bo.height() as i32) == size;
2395    if !cap.host_still.as_ref().is_some_and(|(bo, _)| fits(bo)) {
2396        cap.host_still = alloc_render_target(
2397            gbm,
2398            size.0.max(0) as u32,
2399            size.1.max(0) as u32,
2400            GbmFormat::Argb8888,
2401        )
2402        .ok()
2403        .map(|bo| {
2404            let dmabuf = create_dmabuf_from_bo(&bo);
2405            (bo, dmabuf)
2406        });
2407    }
2408    let Some((_, still)) = cap.host_still.as_mut() else {
2409        return;
2410    };
2411    match compose_host_frame(renderer, None, &src, still, size) {
2412        Ok(sync) if wait_render_fence(&sync, display_id) => {}
2413        Ok(_) => return,
2414        Err(e) => {
2415            static WARNED: std::sync::atomic::AtomicBool =
2416                std::sync::atomic::AtomicBool::new(false);
2417            if !WARNED.swap(true, Ordering::Relaxed) {
2418                eprintln!("[HostCapture] a still frame could not be copied out of the stream: {e}");
2419            }
2420            return;
2421        }
2422    }
2423    let still = still.clone();
2424    if let Some(copy) = host.with_retained(display_id, |r| r.map(|f| f.detached_into(still))) {
2425        retain_host_frame(host, Some(cap), display_id, copy);
2426    }
2427}
2428
2429/// Hand back the pool reservations a render tick made and did not publish.
2430fn cancel_slots(
2431    node: &wayland::frontend::OutputNode,
2432    pool_slot: &mut Option<(usize, Vec<u8>)>,
2433    gpu_slot: &mut Option<(usize, Dmabuf)>,
2434) {
2435    let Some(cap) = node.capture.as_ref() else {
2436        return;
2437    };
2438    if let Some((id, buf)) = pool_slot.take()
2439        && let Some(pool) = cap.encode_pool.as_ref()
2440    {
2441        pool.cancel(id, buf);
2442    }
2443    if let Some((id, dmabuf)) = gpu_slot.take()
2444        && let Some(pool) = cap.gpu_pool.as_ref()
2445    {
2446        pool.cancel(id, dmabuf);
2447    }
2448}
2449
2450/// Hand back to the host the frames its compositor blitted that a zero-copy encode no longer
2451/// reads: each once the encode thread is done with every frame published from it, or all of
2452/// them (`all`) where the capture is going away.
2453fn release_host_inflight(
2454    host: Option<&wayland::host::HostSession>,
2455    cap: &mut wayland::frontend::WlCapture,
2456    display_id: u32,
2457    all: bool,
2458) {
2459    if cap.host_inflight.is_empty() {
2460        return;
2461    }
2462    let done = cap.encode_stats.recycled.load(Ordering::Acquire);
2463    let (free, held): (Vec<_>, Vec<_>) = std::mem::take(&mut cap.host_inflight)
2464        .into_iter()
2465        .partition(|(last_use, _)| all || *last_use <= done);
2466    cap.host_inflight = held;
2467    if let Some(host) = host {
2468        for (_, frame) in free {
2469            host.release_frame(display_id, frame);
2470        }
2471    }
2472}
2473
2474/// Let go of the client buffers encoded as they stood that no frame in flight reads anymore
2475/// (`WlCapture::scanout_inflight`), or all of them (`all`) where the capture or its zero-copy
2476/// session is going away: the last hold on one releases it to its client.
2477fn release_scanout_inflight(cap: &mut wayland::frontend::WlCapture, all: bool) {
2478    if cap.scanout_inflight.is_empty() {
2479        return;
2480    }
2481    let done = cap.encode_stats.recycled.load(Ordering::Acquire);
2482    cap.scanout_inflight
2483        .retain(|(last_use, _)| !all && *last_use > done);
2484}
2485
2486/// Say, at debug, that a display's frames moved between compositing and a client's buffer,
2487/// or are composited for a new reason: the choice is made every frame, so only its change is
2488/// worth a line.
2489fn note_direct(
2490    cap: &mut wayland::frontend::WlCapture,
2491    display_id: u32,
2492    direct: &Result<String, &'static str>,
2493) {
2494    let now = direct.as_ref().map(|_| ()).map_err(|why| *why);
2495    if cap.direct == Some(now) {
2496        return;
2497    }
2498    cap.direct = Some(now);
2499    match direct {
2500        Ok(what) => crate::log::debug!(
2501            "[Wayland] Output {display_id}: encoding the covering client's {what} as it stands."
2502        ),
2503        Err(why) => crate::log::debug!("[Wayland] Output {display_id}: compositing: {why}."),
2504    }
2505}
2506
2507/// The software cursor an output's frame draws, where it does: the theme that resolves a
2508/// named cursor, what the client set, where the pointer is on the output, and the time for
2509/// an animated cursor's frame.
2510struct SoftwareCursor<'a> {
2511    theme: &'a Cursor,
2512    icon: &'a Option<CursorImageStatus>,
2513    pos: Point<f64, Logical>,
2514    time: Duration,
2515}
2516
2517/// What an output's frame is composed of, for `composition_elements`.
2518struct FrameScene<'a> {
2519    space: &'a Space<smithay::desktop::Window>,
2520    wm: Option<&'a wayland::wm::WmState>,
2521    /// `None` where the cursor is not drawn into the frame or the pointer is elsewhere.
2522    cursor: Option<SoftwareCursor<'a>>,
2523    /// A drag's icon and where its top-left corner lies on the output, while a drag carries one
2524    /// over it.
2525    drag_icon: Option<(
2526        &'a smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
2527        Point<f64, Logical>,
2528    )>,
2529    overlay: &'a OverlayState,
2530    output: &'a Output,
2531    /// The output's origin in the layout.
2532    origin: Point<i32, Logical>,
2533    /// The frame's size in pixels.
2534    size: (i32, i32),
2535    scale: f64,
2536}
2537
2538/// The render elements of an output's frame, topmost first: the software cursor, a drag's icon,
2539/// the watermark, the window manager's overlays, the overlay and top wlr-layer layers, the windows
2540/// with their frames, and the bottom and background layers. One list for both renderers, read
2541/// by `scanout_candidate` before anything is drawn.
2542fn composition_elements<R>(
2543    renderer: &mut R,
2544    scene: &FrameScene<'_>,
2545) -> Vec<CompositionElements<R, WaylandSurfaceRenderElement<R>>>
2546where
2547    R: smithay::backend::renderer::Renderer + ImportAll + ImportMem,
2548    R::TextureId: Clone + Send + 'static,
2549{
2550    use smithay::wayland::shell::wlr_layer::Layer;
2551    let mut elements = Vec::new();
2552    let scale = scene.scale;
2553    if let Some(cursor) = scene.cursor.as_ref() {
2554        let theme_scale = scale.round() as u32;
2555        match cursor.icon {
2556            Some(CursorImageStatus::Named(icon)) => {
2557                let name = wayland::frontend::cursor_icon_to_str(icon);
2558                if let Some(image) = cursor
2559                    .theme
2560                    .get_image_by_name(name, theme_scale, cursor.time)
2561                    && let Some(elem) = scene
2562                        .overlay
2563                        .get_cursor_element(renderer, image, cursor.pos, scale)
2564                {
2565                    elements.push(CompositionElements::Cursor(elem));
2566                }
2567            }
2568            Some(CursorImageStatus::Surface(surface)) => {
2569                let hot = cursor_surface_hotspot(surface).to_f64();
2570                let phys_pos = (cursor.pos - hot).to_physical(Scale::from(scale));
2571                let elem_result = with_states(surface, |states| {
2572                    WaylandSurfaceRenderElement::from_surface(
2573                        renderer,
2574                        surface,
2575                        states,
2576                        phys_pos,
2577                        1.0,
2578                        Kind::Cursor,
2579                    )
2580                });
2581                if let Ok(Some(cursor_elem)) = elem_result {
2582                    elements.push(CompositionElements::Surface(cursor_elem));
2583                }
2584            }
2585            Some(CursorImageStatus::Hidden) => {}
2586            None => {
2587                let image = cursor.theme.get_image(theme_scale, cursor.time);
2588                if let Some(elem) = scene
2589                    .overlay
2590                    .get_cursor_element(renderer, image, cursor.pos, scale)
2591                {
2592                    elements.push(CompositionElements::Cursor(elem));
2593                }
2594            }
2595        }
2596    }
2597    if let Some((surface, at)) = scene.drag_icon {
2598        let at = at.to_physical(Scale::from(scale)).to_i32_round();
2599        elements.extend(
2600            smithay::backend::renderer::element::surface::render_elements_from_surface_tree(
2601                renderer,
2602                surface,
2603                at,
2604                scale,
2605                1.0,
2606                Kind::Unspecified,
2607            )
2608            .into_iter()
2609            .map(CompositionElements::Surface),
2610        );
2611    }
2612    if let Some(elem) = scene.overlay.get_watermark_element(renderer) {
2613        elements.push(CompositionElements::Cursor(elem));
2614    }
2615    let logical: Size<i32, Logical> = (
2616        (f64::from(scene.size.0) / scale).round() as i32,
2617        (f64::from(scene.size.1) / scale).round() as i32,
2618    )
2619        .into();
2620    elements.extend(
2621        wayland::wm::overlay_elements(scene.wm, renderer, scene.origin, logical, scale)
2622            .into_iter()
2623            .map(CompositionElements::Cursor),
2624    );
2625    // The top layer (a desktop's panel) lies over the windows, except under a window the policy
2626    // shows fullscreen on top: a video or a game covers the panel, as it does on every desktop,
2627    // and the panel is back once the window leaves fullscreen. X11 override-redirect windows
2628    // (menus, tooltips, a tray's icon strip, a game's own full-screen window) lie over the top
2629    // layer, as Xwayland compositors stack them, since they place themselves; under a fullscreen
2630    // window they keep their place in the stacking, so a tray's strip stays under the game with
2631    // the panel it sits on.
2632    let fullscreen_on_top = scene
2633        .space
2634        .elements_for_output(scene.output)
2635        .rfind(|window| !wayland::frontend::is_override_redirect(window))
2636        .and_then(|window| wayland::frontend::window_meta(window).map(|m| m.id))
2637        .and_then(|id| scene.wm.and_then(|wm| wm.states.get(&id)))
2638        .is_some_and(|states| states.fullscreen);
2639    {
2640        let layer_map = layer_map_for_output(scene.output);
2641        push_layer_elements(renderer, &mut elements, &layer_map, Layer::Overlay, scale);
2642    }
2643    let lifted = |window: &&smithay::desktop::Window| {
2644        !fullscreen_on_top && wayland::frontend::is_override_redirect(window)
2645    };
2646    for window in scene
2647        .space
2648        .elements_for_output(scene.output)
2649        .rev()
2650        .filter(lifted)
2651    {
2652        push_window_elements(
2653            scene.space,
2654            scene.wm,
2655            renderer,
2656            &mut elements,
2657            window,
2658            scene.origin,
2659            scale,
2660        );
2661    }
2662    if !fullscreen_on_top {
2663        let layer_map = layer_map_for_output(scene.output);
2664        push_layer_elements(renderer, &mut elements, &layer_map, Layer::Top, scale);
2665    }
2666    push_ghost_elements(renderer, &mut elements, scene.wm, scene.origin, scale);
2667    for window in scene
2668        .space
2669        .elements_for_output(scene.output)
2670        .rev()
2671        .filter(|window| !lifted(window))
2672    {
2673        push_window_elements(
2674            scene.space,
2675            scene.wm,
2676            renderer,
2677            &mut elements,
2678            window,
2679            scene.origin,
2680            scale,
2681        );
2682    }
2683    {
2684        let layer_map = layer_map_for_output(scene.output);
2685        if fullscreen_on_top {
2686            push_layer_elements(renderer, &mut elements, &layer_map, Layer::Top, scale);
2687        }
2688        push_layer_elements(renderer, &mut elements, &layer_map, Layer::Bottom, scale);
2689        push_layer_elements(
2690            renderer,
2691            &mut elements,
2692            &layer_map,
2693            Layer::Background,
2694            scale,
2695        );
2696    }
2697    elements
2698}
2699
2700/// The surface whose buffer is an output's frame as it stands, or why there is none.
2701///
2702/// Decided from the frame's element list every frame, as a display compositor picks direct
2703/// scanout: the topmost element on the output has to be a surface drawn over exactly the
2704/// output, unscaled, uncropped, untransformed, at full alpha, and opaque throughout, so its
2705/// buffer holds the frame pixel for pixel and hides everything under it. A cursor drawn into
2706/// the frame, a watermark, an overlay or frame of the window manager, a layer surface, a
2707/// popup, or a second window on the output is on top of it or beside it, and ends the direct
2708/// path the frame it appears. The client whose buffer qualifies is the usual full-screen
2709/// case: a game, a kiosk browser, a maximized window under a policy that frames none.
2710fn scanout_candidate<R>(
2711    elements: &[CompositionElements<R, WaylandSurfaceRenderElement<R>>],
2712    size: (i32, i32),
2713    scale: f64,
2714) -> Result<&WaylandSurfaceRenderElement<R>, &'static str>
2715where
2716    R: smithay::backend::renderer::Renderer + ImportAll + ImportMem,
2717    R::TextureId: Clone + 'static,
2718{
2719    let full = Rectangle::<i32, Physical>::from_size(size.into());
2720    let scale = Scale::from(scale);
2721    let Some(top) = elements.iter().find(|e| e.geometry(scale).overlaps(full)) else {
2722        return Err("nothing is on the output");
2723    };
2724    let surface = match top {
2725        CompositionElements::Space(SpaceRenderElements::Surface(s))
2726        | CompositionElements::Surface(s)
2727            if s.kind() != Kind::Cursor =>
2728        {
2729            s
2730        }
2731        _ => return Err("something is drawn over the window"),
2732    };
2733    if surface.alpha() < 1.0 {
2734        return Err("the window is translucent");
2735    }
2736    if surface.geometry(scale) != full {
2737        return Err("the window does not cover the output");
2738    }
2739    if surface.transform() != Transform::Normal {
2740        return Err("the buffer is transformed");
2741    }
2742    let buffer_full = Rectangle::<f64, smithay::utils::Buffer>::from_size(
2743        (f64::from(size.0), f64::from(size.1)).into(),
2744    );
2745    if surface.src() != buffer_full {
2746        return Err("the buffer is scaled or cropped");
2747    }
2748    if !full
2749        .subtract_rects(surface.opaque_regions(scale).iter().copied())
2750        .is_empty()
2751    {
2752        return Err("the window is not opaque throughout");
2753    }
2754    Ok(surface)
2755}
2756
2757/// A client's dmabuf the zero-copy session encodes as it stands, with its description for the
2758/// log, and a hold on the buffer, or why the session cannot take it: both hardware sessions
2759/// import one of the four packed 8-bit RGB formats at their own width and height, so only a
2760/// buffer of the capture's size qualifies, and of no more planes than the session's encoder
2761/// imports (`planes`: VA-API maps a compressed layout's metadata plane, NVENC one plane). What
2762/// the driver then makes of its modifier and pitch only the import says, and a refusal there
2763/// ends the direct path for the capture (`WlEncodeControls::scanout_refused`).
2764fn scanout_dmabuf<R: smithay::backend::renderer::Renderer + ImportAll>(
2765    surface: &WaylandSurfaceRenderElement<R>,
2766    size: (i32, i32),
2767    planes: usize,
2768) -> Result<(Dmabuf, SurfaceBuffer, String), &'static str> {
2769    let buffer = surface.buffer();
2770    let dmabuf = get_dmabuf(buffer).map_err(|_| "the window's buffer is in system memory")?;
2771    if (dmabuf.width() as i32, dmabuf.height() as i32) != size {
2772        return Err("the buffer is not the size of the capture");
2773    }
2774    if dmabuf.num_planes() > planes {
2775        return Err("the buffer has more planes than the encoder imports");
2776    }
2777    let format = dmabuf.format();
2778    if !matches!(
2779        format.code,
2780        Fourcc::Argb8888 | Fourcc::Xrgb8888 | Fourcc::Abgr8888 | Fourcc::Xbgr8888
2781    ) {
2782        return Err("the buffer's format is not one the encoder imports");
2783    }
2784    let what = format!(
2785        "{:?} dmabuf (modifier {:#x})",
2786        format.code,
2787        u64::from(format.modifier)
2788    );
2789    Ok((dmabuf.clone(), buffer.clone(), what))
2790}
2791
2792/// A client's shm buffer the readback path encodes where it lies, with its description for
2793/// the log, or why not: the rows have to be what the pool's are, B,G,R,A (`ARGB8888` or
2794/// `XRGB8888`) packed at the capture's width, since the encoders read them at that pitch.
2795fn scanout_shm<R: smithay::backend::renderer::Renderer + ImportAll>(
2796    surface: &WaylandSurfaceRenderElement<R>,
2797    size: (i32, i32),
2798) -> Result<(SurfaceBuffer, String), &'static str> {
2799    use smithay::reexports::wayland_server::protocol::wl_shm;
2800    let buffer = surface.buffer();
2801    let layout = with_buffer_contents(buffer, |_, len, data| {
2802        let fits = matches!(
2803            data.format,
2804            wl_shm::Format::Argb8888 | wl_shm::Format::Xrgb8888
2805        ) && (data.width, data.height) == size
2806            && data.stride == size.0.saturating_mul(4)
2807            && data.offset >= 0
2808            && (data.offset as usize).saturating_add(
2809                (data.height.max(0) as usize).saturating_mul(data.stride.max(0) as usize),
2810            ) <= len;
2811        fits.then(|| format!("{:?} shm buffer", data.format))
2812    })
2813    .map_err(|_| "the window's buffer is on the GPU")?;
2814    layout
2815        .map(|what| (buffer.clone(), what))
2816        .ok_or("the buffer's format or layout is not the pool's")
2817}
2818
2819/// The error `encode_shm_rows` returns where the client's pool no longer holds the frame.
2820const SHM_GONE: &str = "the client's shm buffer is gone";
2821
2822/// Run `encode` on the rows of a client's shm buffer a frame names (`WlFrame::shm`), `height`
2823/// rows of `width` pixels packed four bytes each: the pool's mapping is valid only inside this
2824/// call, so the encode runs inside it. `SHM_GONE` where the pool was resized or destroyed under
2825/// the frame.
2826fn encode_shm_rows<T>(
2827    buffer: &SurfaceBuffer,
2828    width: i32,
2829    height: i32,
2830    encode: impl FnOnce(&[u8]) -> Result<T, String>,
2831) -> Result<T, String> {
2832    with_buffer_contents(buffer, |ptr, len, data| {
2833        let stride = (width.max(0) as usize) * 4;
2834        let offset = data.offset.max(0) as usize;
2835        let needed = (height.max(0) as usize) * stride;
2836        if data.stride.max(0) as usize != stride || offset.saturating_add(needed) > len {
2837            return Err(SHM_GONE.to_string());
2838        }
2839        // SAFETY: smithay maps the pool for `len` bytes for the span of this closure, the
2840        // rows lie within them, and the SIGBUS of a pool shrunk under the read becomes the
2841        // error mapped below.
2842        let rows = unsafe { std::slice::from_raw_parts(ptr.add(offset), needed) };
2843        encode(rows)
2844    })
2845    .map_err(|_| SHM_GONE.to_string())?
2846}
2847
2848/// Drop a host-capture session whose compositor connection has ended, stopping every
2849/// capture it fed: their displays leave the alive set, so `is_capturing` turns false and the
2850/// consumer rebuilds them (the next start reconnects), and no input is written to a dead
2851/// connection. The same signal X11 gives when its capture thread ends.
2852fn reap_dead_host(state: &mut AppState) {
2853    if !state.host.as_ref().is_some_and(|h| !h.alive()) {
2854        return;
2855    }
2856    eprintln!("[HostCapture] host compositor connection lost; captures stop until restarted.");
2857    let ids: Vec<u32> = state
2858        .output_nodes
2859        .iter()
2860        .filter(|n| n.capture.is_some())
2861        .map(|n| n.id)
2862        .collect();
2863    for id in ids {
2864        stop_capture_on_display(state, id);
2865        // stop_capture_on_display cleared this display's outcome; overwrite it with the
2866        // host-death reason so capture_state reports why the pipeline went down (input is
2867        // black-holed to a dead connection until a fresh start rebuilds against a new one).
2868        set_wayland_capture_err(
2869            id,
2870            Some("host compositor connection lost; capture stopped".to_string()),
2871        );
2872    }
2873    state
2874        .pending_screenshots
2875        .fail_all("Host compositor connection lost");
2876    state.host = None;
2877    *crate::wayland::pwcapture::HOST_POINTER.lock().unwrap() = None;
2878    state.host_mode_refusals.clear();
2879    // Nothing will answer the requests still in flight: their geometry readers get
2880    // what the outputs show now.
2881    let pending: Vec<_> = state.host_layout_pending.drain().collect();
2882    for (id, p) in pending {
2883        answer_geometry_waiters(state, id, p.geometry_waiters);
2884    }
2885}
2886
2887/// Stop the capture bound to `display_id`, leaving the output (and every other display's
2888/// capture) running.
2889fn stop_capture_on_display(state: &mut AppState, display_id: u32) {
2890    let Some(idx) = state.node_idx_for_id(display_id) else {
2891        return;
2892    };
2893    if let Some(mut cap) = state.output_nodes[idx].capture.take() {
2894        crate::log::debug!("[Wayland] Capture loop stopped (display {display_id}).");
2895        let (join, encode_join) = teardown_capture(&mut cap);
2896        release_host_inflight(state.host.as_ref(), &mut cap, display_id, true);
2897        state.deliver_reaper.extend(join);
2898        state.encode_reaper.extend(encode_join);
2899    }
2900    wayland_alive().lock().unwrap().remove(&display_id);
2901    set_wayland_capture_err(display_id, None);
2902    set_wayland_active_codec(display_id, None);
2903    report::wayland_reports()
2904        .lock()
2905        .unwrap()
2906        .remove(&display_id);
2907    if let Some(p) = state.host_layout_pending.remove(&display_id) {
2908        answer_geometry_waiters(state, display_id, p.geometry_waiters);
2909    }
2910}
2911
2912/// The geometry `get_realized_geometry` reports for `display_id`: the live capture's size
2913/// and scale, else the output's current mode, else zeros for an unknown display.
2914fn realized_geometry(state: &AppState, display_id: u32) -> (i32, i32, f64) {
2915    state
2916        .node_idx_for_id(display_id)
2917        .map(|idx| {
2918            let node = &state.output_nodes[idx];
2919            match node.capture.as_ref() {
2920                Some(c) => (c.settings.width, c.settings.height, c.settings.scale),
2921                None => node
2922                    .output
2923                    .current_mode()
2924                    .map(|m| {
2925                        (
2926                            m.size.w,
2927                            m.size.h,
2928                            node.output.current_scale().fractional_scale(),
2929                        )
2930                    })
2931                    .unwrap_or((0, 0, 0.0)),
2932            }
2933        })
2934        .unwrap_or((0, 0, 0.0))
2935}
2936
2937/// Answer geometry readers parked behind a host layout request with what `display_id`
2938/// captures now.
2939fn answer_geometry_waiters(
2940    state: &AppState,
2941    display_id: u32,
2942    waiters: Vec<std::sync::mpsc::Sender<(i32, i32, f64)>>,
2943) {
2944    if waiters.is_empty() {
2945        return;
2946    }
2947    let info = realized_geometry(state, display_id);
2948    for w in waiters {
2949        let _ = w.send(info);
2950    }
2951}
2952
2953/// What the mode a host announced means for a capture configured at `want`, once the host
2954/// has ruled on the layout request: `Some(size)` when the capture must follow a different
2955/// mode the host runs, `None` when nothing changes — the host runs the requested size, or
2956/// its mode is unknown and the capture keeps gating on the size it asked for. The verdict
2957/// itself is not consulted: a host may answer `succeeded` for a mode it did not take
2958/// (Hyprland acknowledges a custom mode it rejected), so the announced size decides. It has
2959/// arrived by the time any verdict does, both riding the one host connection in order.
2960fn host_layout_resolution(want: (i32, i32), current: Option<(i32, i32)>) -> Option<(i32, i32)> {
2961    current.filter(|&c| c != want)
2962}
2963
2964/// Periods without a fresh host frame before the timer treats the host as quiet and, in
2965/// streaming mode, re-encodes the retained frame to hold the rate. A live host's frames come
2966/// about a period apart, so a tick landing a hair ahead of one must not publish a duplicate
2967/// the fresh frame then waits behind.
2968const HOST_QUIET_PERIODS: f64 = 1.5;
2969
2970/// Refused mode requests in a row for one display before the log says plainly what to do: a
2971/// host that keeps its own mode is asked again on every client resize, and each refusal
2972/// restarts the capture at the mode the host runs.
2973const HOST_MODE_REFUSALS_BEFORE_HINT: u32 = 3;
2974
2975/// Settle the host layout requests answered since the last tick, against the mode each host
2976/// announced. A host already running the requested size needs nothing: the capture was
2977/// configured for it and its frames flow. A host running anything else — a request it
2978/// refused, acknowledged without applying, or never answered by the deadline, and a host
2979/// with no layout management at all — has the capture re-sized to that mode through the same
2980/// in-place reconfigure a resize takes, so it never gates on a size the host will not
2981/// produce, and the mismatch becomes the capture's caveat. Geometry readers parked behind a
2982/// request answer once it is settled, with the size actually captured.
2983fn reconcile_host_layouts(state: &mut AppState) {
2984    if state.host_layout_pending.is_empty() {
2985        return;
2986    }
2987    let ids: Vec<u32> = state.host_layout_pending.keys().copied().collect();
2988    for id in ids {
2989        // An earlier iteration's restart may have reaped the host (and every request).
2990        let Some(pending) = state.host_layout_pending.get(&id) else {
2991            continue;
2992        };
2993        let host_mode = match state.host.as_ref() {
2994            Some(host) => match host.layout_outcome(pending.epoch) {
2995                // Unanswered: the host has not ruled, so its announced mode may still be
2996                // the one from before the request.
2997                None => continue,
2998                Some(_) => host_layout_resolution(pending.want, host.current_output_size(id)),
2999            },
3000            None => None,
3001        };
3002        let Some(pending) = state.host_layout_pending.remove(&id) else {
3003            continue;
3004        };
3005        if host_mode.is_none() {
3006            state.host_mode_refusals.remove(&id);
3007        }
3008        if let Some((rw, rh)) = host_mode {
3009            let (w, h) = pending.want;
3010            let restart = state
3011                .node_idx_for_id(id)
3012                .and_then(|idx| state.output_nodes[idx].capture.as_ref())
3013                .map(|cap| (cap.callback.clone(), cap.settings.clone()));
3014            if let Some((cb, mut settings)) = restart {
3015                settings.width = rw;
3016                settings.height = rh;
3017                // Video codecs even-mask their dimensions, so an odd host mode is followed as
3018                // closely as the encoder can; asking again would only loop.
3019                let followed = if settings.codec.is_video() {
3020                    (rw & !1, rh & !1)
3021                } else {
3022                    (rw, rh)
3023                };
3024                if followed != (w, h) {
3025                    eprintln!(
3026                        "[HostCapture] host runs {rw}x{rh} for display {id} ({w}x{h} not applied); capturing at that size."
3027                    );
3028                    let refusals = {
3029                        let n = state.host_mode_refusals.entry(id).or_insert(0);
3030                        *n += 1;
3031                        *n
3032                    };
3033                    if refusals == HOST_MODE_REFUSALS_BEFORE_HINT {
3034                        eprintln!(
3035                            "[HostCapture] display {id}: the host keeps its own mode; a manual resolution of {rw}x{rh} stops the capture restarting on every resize."
3036                        );
3037                    }
3038                    start_capture_on_display(state, id, cb, settings);
3039                    let mismatch = if refusals >= HOST_MODE_REFUSALS_BEFORE_HINT {
3040                        format!(
3041                            "host runs {rw}x{rh} ({w}x{h} not applied; set a manual resolution of {rw}x{rh})"
3042                        )
3043                    } else {
3044                        format!("host runs {rw}x{rh} ({w}x{h} not applied)")
3045                    };
3046                    let own = wayland_capture_err().lock().unwrap().get(&id).cloned();
3047                    set_wayland_capture_err(
3048                        id,
3049                        Some(match own {
3050                            Some(e) => format!("{mismatch}; {e}"),
3051                            None => mismatch,
3052                        }),
3053                    );
3054                }
3055            }
3056        }
3057        // Settled either way: the restart, if any, runs at the size the host has, so the
3058        // readers need not wait for the host to acknowledge it again.
3059        answer_geometry_waiters(state, id, pending.geometry_waiters);
3060    }
3061}
3062
3063/// Start (or in-place reconfigure) the capture bound to output `display_id`: reprogram the
3064/// output's mode/scale/refresh, size the render targets, fullscreen the display's windows at
3065/// the new logical size, resolve the encode path (zero-copy vs readback), and spawn the
3066/// delivery (and readback-mode encode) threads. The single-display behavior of the former
3067/// global StartCapture is preserved exactly for display 0.
3068/// Bring up the readback encode path (pixman readback → pool → encode thread) for a
3069/// capture whose zero-copy session is absent or just died. Mirrors the start-capture
3070/// bootstrap: u64::MAX content generations mark every pool slot stale so each one is
3071/// read back before its first publish, whatever the damage says.
3072fn bootstrap_readback_pool(
3073    cap: &mut wayland::frontend::WlCapture,
3074    display_id: u32,
3075    use_gpu: bool,
3076    try_gpu: bool,
3077    prior: Option<FrameEncoder>,
3078    predecessor: Option<std::thread::JoinHandle<Option<FrameEncoder>>>,
3079) {
3080    let Some(deliver_tx) = cap.deliver_tx.clone() else {
3081        return;
3082    };
3083    let settings = cap.settings.clone();
3084    let pool = Arc::new(WlFramePool::new(
3085        WL_POOL_SURFACES,
3086        (settings.width.max(0) as usize) * (settings.height.max(0) as usize) * 4,
3087    ));
3088    cap.pool_last_render = vec![0; WL_POOL_SURFACES];
3089    cap.render_seq = 0;
3090    cap.pool_content_gen = vec![u64::MAX; WL_POOL_SURFACES];
3091    cap.content_gen = 0;
3092    let c = &cap.encode_controls;
3093    c.bitrate_kbps
3094        .store(settings.video_bitrate_kbps, Ordering::Relaxed);
3095    c.vbv_mult_milli.store(
3096        (settings.video_vbv_multiplier * 1000.0).round() as i32,
3097        Ordering::Relaxed,
3098    );
3099    c.fps_bits
3100        .store(settings.target_fps.max(1.0).to_bits(), Ordering::Relaxed);
3101    c.link_kbps
3102        .store(settings.video_link_kbps, Ordering::Relaxed);
3103    let cfg = WlEncodeConfig {
3104        settings,
3105        display_id,
3106        use_gpu,
3107        try_gpu,
3108        prior,
3109        predecessor,
3110        zero_copy: None,
3111        host: false,
3112        recording_sink: cap.recording_sink.as_ref().map(Arc::downgrade),
3113        deliver_tx,
3114        controls: cap.encode_controls.clone(),
3115        stats: cap.encode_stats.clone(),
3116        report: cap.report.clone(),
3117    };
3118    let pool2 = pool.clone();
3119    cap.encode_join = Some(
3120        thread::Builder::new()
3121            .name(format!("wl-encode-{display_id}"))
3122            .spawn(move || wayland_encode_loop(&pool2, cfg))
3123            .expect("failed to spawn wl-encode thread"),
3124    );
3125    cap.encode_pool = Some(pool);
3126}
3127
3128/// `WL_POOL_SURFACES` zero-copy render targets the size of the capture, or `None` where the
3129/// GPU refuses one.
3130fn alloc_zero_copy_ring(
3131    gbm: &RawGbmDevice<std::fs::File>,
3132    settings: &CaptureSettings,
3133    display_id: u32,
3134) -> Option<Vec<(BufferObject<()>, Dmabuf)>> {
3135    let mut ring = Vec::with_capacity(WL_POOL_SURFACES);
3136    for _ in 0..WL_POOL_SURFACES {
3137        match alloc_render_target(
3138            gbm,
3139            settings.width.max(0) as u32,
3140            settings.height.max(0) as u32,
3141            GbmFormat::Argb8888,
3142        ) {
3143            Ok(bo) => {
3144                let dmabuf = create_dmabuf_from_bo(&bo);
3145                ring.push((bo, dmabuf));
3146            }
3147            Err(e) => {
3148                eprintln!(
3149                    "[Wayland] Display {display_id}: zero-copy render target {}x{} not allocated ({e:?}).",
3150                    settings.width, settings.height
3151                );
3152                return None;
3153            }
3154        }
3155    }
3156    Some(ring)
3157}
3158
3159/// Start a zero-copy capture's encode thread on `encoder`, a session the calloop opened on the
3160/// renderer's EGL display, on `ring`'s render targets.
3161fn bootstrap_zero_copy_pool(
3162    cap: &mut wayland::frontend::WlCapture,
3163    ring: Vec<(BufferObject<()>, Dmabuf)>,
3164    display_id: u32,
3165    encoder: FrameEncoder,
3166    egl_display: *const std::ffi::c_void,
3167    host: bool,
3168) {
3169    let Some(deliver_tx) = cap.deliver_tx.clone() else {
3170        return;
3171    };
3172    let settings = cap.settings.clone();
3173    let pool = Arc::new(WlFramePool::of(
3174        ring.iter().map(|(_, dmabuf)| dmabuf.clone()).collect(),
3175    ));
3176    cap.gpu_ring = ring;
3177    cap.pool_last_render = vec![0; WL_POOL_SURFACES];
3178    cap.render_seq = 0;
3179    let session = Arc::new(Mutex::new(Some(encoder)));
3180    cap.zero_copy = Some(session.clone());
3181    let c = &cap.encode_controls;
3182    c.bitrate_kbps
3183        .store(settings.video_bitrate_kbps, Ordering::Relaxed);
3184    c.vbv_mult_milli.store(
3185        (settings.video_vbv_multiplier * 1000.0).round() as i32,
3186        Ordering::Relaxed,
3187    );
3188    c.fps_bits
3189        .store(settings.target_fps.max(1.0).to_bits(), Ordering::Relaxed);
3190    c.link_kbps
3191        .store(settings.video_link_kbps, Ordering::Relaxed);
3192    let cfg = WlEncodeConfig {
3193        settings,
3194        display_id,
3195        use_gpu: true,
3196        try_gpu: true,
3197        prior: None,
3198        predecessor: None,
3199        zero_copy: Some(ZeroCopySession {
3200            encoder: session,
3201            egl_display: egl_display as usize,
3202        }),
3203        host,
3204        recording_sink: cap.recording_sink.as_ref().map(Arc::downgrade),
3205        deliver_tx,
3206        controls: cap.encode_controls.clone(),
3207        stats: cap.encode_stats.clone(),
3208        report: cap.report.clone(),
3209    };
3210    let pool2 = pool.clone();
3211    cap.encode_join = Some(
3212        thread::Builder::new()
3213            .name(format!("wl-encode-{display_id}"))
3214            .spawn(move || wayland_encode_loop(&pool2, cfg))
3215            .expect("failed to spawn wl-encode thread"),
3216    );
3217    cap.gpu_pool = Some(pool);
3218}
3219
3220/// Consecutive encode failures before a hardware path recovers (~0.5s at 60fps): a hiccup
3221/// outlasts it, anything longer starts to look like a dead session. Shared by every hardware
3222/// encode path (Wayland zero-copy, Wayland readback, X11) so recovery timing matches.
3223pub(crate) const HW_ERROR_RECOVERY_THRESHOLD: u32 = 30;
3224
3225fn start_capture_on_display(
3226    state: &mut AppState,
3227    display_id: u32,
3228    cb: Option<Arc<dyn crate::sink::FrameSink>>,
3229    mut settings: CaptureSettings,
3230) {
3231    // The cursor worker outlives individual captures, so the starting settings have to
3232    // reach it here — same point X11 applies the cap — or it keeps the previous capture's.
3233    let _ = state
3234        .cursor_tx
3235        .send(CursorJob::SetSizeCap(settings.cursor_size_cap));
3236
3237    // Fresh attempt: drop any prior outcome so a stale caveat cannot read as this start's.
3238    set_wayland_capture_err(display_id, None);
3239
3240    let Some(node_idx) = state.node_idx_for_id(display_id) else {
3241        eprintln!("[Wayland] StartCapture: no output with display id {display_id}.");
3242        set_wayland_capture_err(
3243            display_id,
3244            Some(format!("no output with display id {display_id}")),
3245        );
3246        return;
3247    };
3248    let stream_report = report::StreamReport::new("wayland");
3249    report::wayland_reports()
3250        .lock()
3251        .unwrap()
3252        .insert(display_id, stream_report.clone());
3253    let _report = report::enter(&stream_report);
3254    let mut node = state.output_nodes.remove(node_idx);
3255    // Geometry readers parked behind a layout request this start supersedes: they ride
3256    // on to this start's request, or answer at its end if no host is involved.
3257    let mut geometry_waiters = state
3258        .host_layout_pending
3259        .remove(&display_id)
3260        .map(|p| p.geometry_waiters)
3261        .unwrap_or_default();
3262
3263    if settings.encode_node_index < -1 {
3264        let picked = if state.auto_gpu_selected {
3265            render_node_index(&state.render_node_path)
3266        } else {
3267            first_render_node_index()
3268        };
3269        if let Some(idx) = picked {
3270            settings.encode_node_index = idx;
3271        }
3272    }
3273
3274    if settings.codec.is_video() {
3275        settings.width &= !1;
3276        settings.height &= !1;
3277    }
3278
3279    // Tear down this display's previous capture first; its hardware sessions are the
3280    // reuse candidates below (zero-copy inline, readback via the encode config).
3281    let mut prior_zero_copy: Option<FrameEncoder> = None;
3282    let mut prior_encode_join: Option<std::thread::JoinHandle<Option<FrameEncoder>>> = None;
3283    let mut prior_deliver_join: Option<std::thread::JoinHandle<()>> = None;
3284    if let Some(mut old) = node.capture.take() {
3285        (prior_deliver_join, prior_encode_join) = teardown_capture(&mut old);
3286        // The outgoing thread stops after the frame in flight, which is the most this waits.
3287        prior_zero_copy = old.zero_copy.take().and_then(|z| z.lock().unwrap().take());
3288        release_host_inflight(state.host.as_ref(), &mut old, display_id, true);
3289    }
3290
3291    // Bind only after the old capture is gone: its sink unlinks the socket path in
3292    // Drop, which would strip a fresh bind's filesystem name and leave every later
3293    // recorder connect with ENOENT.
3294    let recording_sink = crate::recording_sink::RecordingSink::try_bind(
3295        &settings.recording_socket,
3296        settings.target_fps,
3297    );
3298
3299    // Host-capture mode: connect on first use. The display's mode is requested from
3300    // the host further down, without waiting for an answer; a host that keeps its own
3301    // mode (refusal, or no layout management — KWin) has this capture re-sized to it
3302    // when the verdict arrives (`reconcile_host_layouts`), the same way a failed GBM
3303    // resize falls back to the live mode.
3304    let host_capture = !settings.wayland_host_display.is_empty();
3305    reap_dead_host(state);
3306    if host_capture && state.host.is_none() {
3307        // Capture buffers come from the same render node the encoder imports
3308        // from, resolved via its live fd (the path string is not retained in
3309        // auto mode); each capture thread opens its own device handle.
3310        let gbm_path = if state.use_gpu {
3311            state.gbm_device.as_ref().and_then(|dev| {
3312                use std::os::fd::{AsFd as _, AsRawFd as _};
3313                let fd = dev.as_fd().as_raw_fd();
3314                std::fs::read_link(format!("/proc/self/fd/{fd}")).ok()
3315            })
3316        } else {
3317            None
3318        };
3319        // The dmabuf formats the encoder's display imports are what a portal stream may
3320        // offer the compositor for zero-copy frames; a CPU session offers none.
3321        let dma_formats: Vec<(u32, Vec<u64>)> = match (&state.gles_renderer, state.use_gpu) {
3322            (Some(renderer), true) => {
3323                let formats = renderer.egl_context().display().dmabuf_texture_formats();
3324                [Fourcc::Xrgb8888, Fourcc::Argb8888]
3325                    .into_iter()
3326                    .map(|code| {
3327                        (
3328                            code as u32,
3329                            formats
3330                                .iter()
3331                                .filter(|f| f.code == code)
3332                                .map(|f| u64::from(f.modifier))
3333                                .collect(),
3334                        )
3335                    })
3336                    .collect()
3337            }
3338            _ => Vec::new(),
3339        };
3340        match crate::wayland::host::HostSession::connect(
3341            &settings.wayland_host_display,
3342            gbm_path,
3343            state.host_frame_tx.clone(),
3344            dma_formats,
3345            state.cursor_tx.clone(),
3346        ) {
3347            Ok(h) => {
3348                println!(
3349                    "[HostCapture] capturing host compositor '{}' ({} outputs).",
3350                    settings.wayland_host_display,
3351                    h.output_count()
3352                );
3353                state.host = Some(h);
3354                // The host's pointer stands in for the compositor's from here on, None until
3355                // its first cursor meta.
3356                *crate::wayland::pwcapture::HOST_POINTER.lock().unwrap() = Some(None);
3357            }
3358            Err(e) => {
3359                eprintln!(
3360                    "[HostCapture] connect '{}' failed: {e}",
3361                    settings.wayland_host_display
3362                );
3363                set_wayland_capture_err(
3364                    display_id,
3365                    Some(format!(
3366                        "host compositor '{}' connect failed ({e}); capturing locally",
3367                        settings.wayland_host_display
3368                    )),
3369                );
3370            }
3371        }
3372    }
3373    // A failed connect leaves this display compositing locally, so its frames follow the
3374    // local renderer again.
3375    let host_capture = host_capture && state.host.is_some();
3376    if host_capture && let Some(host) = &state.host {
3377        // The node's layout offset rides along so the host's heads mirror
3378        // selkies' union layout (input coordinates already assume it).
3379        host.set_layout(display_id, node.pos.0, node.pos.1);
3380    }
3381
3382    // A view captures a rectangle of a screen it does not own, so its size is the
3383    // rectangle's and the screen keeps the mode its owner gave it. Resizing here would
3384    // shrink the session to one display's worth of desk, which is the whole point of
3385    // cutting several out of one screen.
3386    if node.owner.is_none() {
3387        // Never panic the compositor thread: an output momentarily without a current
3388        // mode falls back to the requested geometry so the reconfigure below is a
3389        // no-op for size/refresh instead of unwrap-panicking.
3390        let target_refresh = (settings.target_fps * 1000.0).round() as i32;
3391        let (current_w, current_h, current_refresh) = match node.output.current_mode() {
3392            Some(m) => (m.size.w, m.size.h, m.refresh),
3393            None => (settings.width, settings.height, target_refresh),
3394        };
3395        let current_scale = node.output.current_scale().fractional_scale();
3396
3397        if current_w != settings.width
3398            || current_h != settings.height
3399            || (current_scale - settings.scale).abs() > 0.001
3400            || current_refresh != target_refresh
3401        {
3402            // Allocate the GPU backing for the new dimensions BEFORE committing
3403            // anything: if the driver refuses (VRAM exhaustion, dimensions it will
3404            // not back), the whole reconfigure is skipped and the previous mode +
3405            // buffers stay live. A failed resize must degrade to "no resize", never
3406            // panic the compositor thread.
3407            let mut new_offscreen = None;
3408            let mut gbm_resize_failed = false;
3409            if state.use_gpu
3410                && let Some(gbm) = state.gbm_device.as_mut()
3411            {
3412                match alloc_render_target(
3413                    gbm,
3414                    settings.width as u32,
3415                    settings.height as u32,
3416                    GbmFormat::Argb8888,
3417                ) {
3418                    Ok(bo) => {
3419                        let dmabuf = create_dmabuf_from_bo(&bo);
3420                        new_offscreen = Some((bo, dmabuf));
3421                    }
3422                    Err(e) => {
3423                        eprintln!(
3424                            "[Wayland] GBM buffer resize to {}x{} failed ({:?}); keeping previous output mode.",
3425                            settings.width, settings.height, e
3426                        );
3427                        gbm_resize_failed = true;
3428                    }
3429                }
3430            }
3431            if gbm_resize_failed {
3432                // The mode commit below is skipped wholesale, so the rest of this
3433                // StartCapture (encoder setup, stored settings) must see the
3434                // dimensions actually live.
3435                set_wayland_capture_err(
3436                    display_id,
3437                    Some(format!(
3438                        "GPU buffer resize to {}x{} refused; kept {current_w}x{current_h}",
3439                        settings.width, settings.height
3440                    )),
3441                );
3442                settings.width = current_w;
3443                settings.height = current_h;
3444                settings.scale = current_scale;
3445                settings.target_fps = f64::from(current_refresh) / 1000.0;
3446            } else {
3447                println!(
3448                    "[Wayland] Configuring Output {} ({}): {}x{} @ {:.2} FPS (Scale {:.2})",
3449                    display_id,
3450                    node.output.name(),
3451                    settings.width,
3452                    settings.height,
3453                    settings.target_fps,
3454                    settings.scale
3455                );
3456                let new_mode = OutputMode {
3457                    size: (settings.width, settings.height).into(),
3458                    refresh: target_refresh,
3459                };
3460                node.output.change_current_state(
3461                    Some(new_mode),
3462                    Some(Transform::Normal),
3463                    Some(OutputScale::Fractional(settings.scale)),
3464                    Some(Point::from(node.pos)),
3465                );
3466                node.output.set_preferred(new_mode);
3467                // Capture clients allocate to our announced size; a stale size means
3468                // every frame they submit from now on fails buffer validation.
3469                for cs in state
3470                    .copy_sessions
3471                    .iter()
3472                    .filter(|cs| cs.output.upgrade().as_ref() == Some(&node.output))
3473                {
3474                    if let Some(c) = wayland::frontend::output_capture_constraints(
3475                        &node.output,
3476                        state.gles_renderer.as_ref(),
3477                        &state.render_node_path,
3478                    ) {
3479                        cs.session.update_constraints(c);
3480                    }
3481                }
3482
3483                let pixel_count =
3484                    (settings.width.max(0) as usize) * (settings.height.max(0) as usize);
3485                node.frame_buffer = vec![0u8; pixel_count * 4];
3486                node.target_seeded = false;
3487
3488                if let Some(off) = new_offscreen.take() {
3489                    node.offscreen_buffer = Some(off);
3490                }
3491            }
3492        }
3493
3494        let out = node.output.clone();
3495        configure_windows_for_mode(
3496            state,
3497            display_id,
3498            &out,
3499            settings.width,
3500            settings.height,
3501            settings.scale,
3502        );
3503    } else if state.use_gpu {
3504        // A view keeps the rectangle its capture asks for, and its GPU render
3505        // target has to follow: it was allocated at the view's creation size,
3506        // and a zero-copy encoder handed a stale-size dmabuf fails every frame
3507        // (the EGLImage and VAAPI imports both refuse it) while readback
3508        // delivers the old rectangle's content. Same degrade-to-no-resize
3509        // semantics as the screen branch above.
3510        let have = node
3511            .offscreen_buffer
3512            .as_ref()
3513            .map(|(bo, _)| (bo.width() as i32, bo.height() as i32));
3514        if have != Some((settings.width, settings.height))
3515            && let Some(gbm) = state.gbm_device.as_mut()
3516        {
3517            match alloc_render_target(
3518                gbm,
3519                settings.width as u32,
3520                settings.height as u32,
3521                GbmFormat::Argb8888,
3522            ) {
3523                Ok(bo) => {
3524                    let dmabuf = create_dmabuf_from_bo(&bo);
3525                    node.offscreen_buffer = Some((bo, dmabuf));
3526                    node.damage_tracker = OutputDamageTracker::new(
3527                        (settings.width, settings.height),
3528                        settings.scale,
3529                        Transform::Normal,
3530                    );
3531                    node.content_tracker = None;
3532                    node.view_size = (settings.width, settings.height);
3533                    node.view_scale = settings.scale;
3534                    node.frame_buffer = vec![
3535                        0u8;
3536                        (settings.width.max(0) as usize)
3537                            * (settings.height.max(0) as usize)
3538                            * 4
3539                    ];
3540                    node.target_seeded = false;
3541                    crate::log::debug!(
3542                        "[Wayland] View {display_id} render target resized to {}x{}.",
3543                        settings.width,
3544                        settings.height
3545                    );
3546                }
3547                Err(e) => {
3548                    eprintln!(
3549                        "[Wayland] GBM view buffer resize to {}x{} failed ({:?}); keeping previous target.",
3550                        settings.width, settings.height, e
3551                    );
3552                    set_wayland_capture_err(
3553                        display_id,
3554                        Some(format!(
3555                            "GPU view buffer resize to {}x{} refused",
3556                            settings.width, settings.height
3557                        )),
3558                    );
3559                }
3560            }
3561        }
3562    }
3563
3564    let use_cpu_explicit = settings.use_cpu || settings.encode_node_index == -1;
3565    let gpu_intent = settings.codec.is_video() && !use_cpu_explicit;
3566
3567    let mut different_gpu = false;
3568    if gpu_intent {
3569        let encode_node_idx = settings.encode_node_index.max(0);
3570        if !state.render_node_path.is_empty()
3571            && !state
3572                .render_node_path
3573                .contains(&format!("renderD{}", 128 + encode_node_idx))
3574        {
3575            different_gpu = true;
3576        }
3577    }
3578
3579    let mut video_encoder: Option<FrameEncoder> = None;
3580    if gpu_intent && state.use_gpu && !different_gpu {
3581        let egl_display = state
3582            .gles_renderer
3583            .as_ref()
3584            .map(|r| r.egl_context().display().get_display_handle().handle)
3585            .unwrap_or(std::ptr::null());
3586        video_encoder = encoders::select_frame_encoder(
3587            &mut settings,
3588            FrameSource::Dmabuf { egl_display },
3589            prior_zero_copy.take(),
3590            "Wayland",
3591        );
3592        if video_encoder.is_none() {
3593            set_wayland_capture_err(
3594                display_id,
3595                Some(format!(
3596                    "no zero-copy {} encoder on the render node; using readback encode",
3597                    settings.codec.display()
3598                )),
3599            );
3600        }
3601    }
3602    drop(prior_zero_copy);
3603
3604    let rendered = if state.use_gpu {
3605        format!("rendered on {}", state.render_node_path)
3606    } else {
3607        "rendered in software (Pixman)".to_string()
3608    };
3609    match video_encoder.as_ref() {
3610        Some(enc) => {
3611            report::capture("dmabuf", true);
3612            println!(
3613                "[Wayland] Zero-copy capture: output {display_id} {}x{} {rendered}, encoded in place on {}.",
3614                settings.width,
3615                settings.height,
3616                enc.backend_name()
3617            );
3618        }
3619        None => {
3620            let why = if different_gpu {
3621                "the encode node is another GPU"
3622            } else if !settings.codec.is_video() {
3623                "JPEG encodes on the CPU"
3624            } else if use_cpu_explicit {
3625                "software encoding selected"
3626            } else if !state.use_gpu {
3627                "no GPU renderer"
3628            } else {
3629                "no zero-copy encoder on the render node"
3630            };
3631            report::capture("readback", false);
3632            report::zero_copy_available(gpu_intent && state.use_gpu && !different_gpu);
3633            report::capture_reason(why);
3634            println!(
3635                "[Wayland] Readback capture: output {display_id} {}x{} {rendered}, read back for the encode thread ({why}).",
3636                settings.width, settings.height
3637            );
3638        }
3639    }
3640
3641    // Point this display's host capture thread at the size the encoder was just
3642    // configured for and ask the host for that mode. The compositor keeps running (CU,
3643    // clipboard callbacks, input fallbacks) but its renderer is bypassed. The buffer
3644    // type follows the consumer settled on above: dmabufs only for a zero-copy encoder,
3645    // shm frames for every CPU path (use_cpu, JPEG, readback). The host's
3646    // answer is not waited for here — this thread carries input and every other
3647    // display — but polled by the render tick, which re-sizes this capture should the
3648    // host keep a different mode; frames gate until the sizes agree.
3649    if host_capture && let Some(host) = &state.host {
3650        let epoch = host.start_capture(
3651            display_id,
3652            settings.width,
3653            settings.height,
3654            video_encoder.is_some(),
3655            settings.capture_cursor,
3656            settings.target_fps,
3657        );
3658        state.host_layout_pending.insert(
3659            display_id,
3660            wayland::frontend::PendingHostLayout {
3661                epoch,
3662                want: (settings.width, settings.height),
3663                geometry_waiters: std::mem::take(&mut geometry_waiters),
3664            },
3665        );
3666    }
3667
3668    if recording_sink.is_some() && !settings.codec.is_video() {
3669        eprintln!(
3670            "[recording_sink] WARNING: recording_socket is set but the codec is JPEG. The \
3671             recording socket requires a video stream; set a video codec on the Python \
3672             CaptureSettings to produce a recordable output."
3673        );
3674    }
3675
3676    // Every display's capture composites its own watermark, uploaded at this output's
3677    // scale and placed against this output's frame dimensions.
3678    let watermark_output_scale = node.output.current_scale().fractional_scale();
3679    node.overlay_state
3680        .load_watermark(&settings.watermark_path, watermark_output_scale);
3681    if display_id == 0 {
3682        state.settings = settings.clone();
3683        if state.cursor_callback_set
3684            && let Some(icon) = state.current_cursor_icon.clone()
3685        {
3686            state.send_cursor_image(&icon);
3687        }
3688    }
3689    state.render_cursor_on_framebuffer = settings.capture_cursor;
3690
3691    let mut cap = wayland::frontend::WlCapture {
3692        settings: settings.clone(),
3693        callback: cb.clone(),
3694        zero_copy: None,
3695        gpu_pool: None,
3696        gpu_ring: Vec::new(),
3697        host_still: None,
3698        host_inflight: Vec::new(),
3699        scanout_inflight: Vec::new(),
3700        direct: None,
3701        published: 0,
3702        host_retained_use: 0,
3703        recording_sink,
3704        deliver_tx: None,
3705        deliver_join: None,
3706        deliver_discard: None,
3707        encode_pool: None,
3708        encode_join: None,
3709        encode_controls: Arc::new(WlEncodeControls::new()),
3710        encode_stats: Arc::new(WlEncodeStats::new()),
3711        report: stream_report.clone(),
3712        pool_last_render: Vec::new(),
3713        render_seq: 0,
3714        pool_content_gen: Vec::new(),
3715        content_gen: 0,
3716        frame_counter: 0,
3717        pending_force_idr: false,
3718        needs_full_render: true,
3719        pace: FramePace::windowed(),
3720        change_pending: false,
3721        callbacks_at: None,
3722        callbacks_owed: false,
3723    };
3724
3725    {
3726        let (tx, rx) = std::sync::mpsc::sync_channel::<Vec<EncodedStripe>>(1);
3727        let discard = Arc::new(AtomicBool::new(false));
3728        let thread_discard = discard.clone();
3729        let predecessor = prior_deliver_join;
3730        // With no consumer (internal recorder-owned capture) the delivery thread
3731        // only drains the channel: the recorder already consumed the frames at the
3732        // delivery-layer tap, upstream of this per-consumer handoff.
3733        let join = thread::spawn(move || {
3734            // The predecessor capture's deliver thread finishes first, off the
3735            // event loop: encoded stripes reach the consumer in capture order across
3736            // a reconfigure, and a stale pre-teardown stripe can never land
3737            // after this capture's first frame.
3738            if let Some(handle) = predecessor {
3739                let _ = handle.join();
3740            }
3741            match cb {
3742                Some(cb) => {
3743                    crate::boost_thread_priority(-10);
3744                    while let Ok(stripes) = rx.recv() {
3745                        if thread_discard.load(Ordering::Relaxed)
3746                            || SHUTDOWN.load(Ordering::Relaxed)
3747                        {
3748                            continue;
3749                        }
3750                        stream_report.tally(&stripes);
3751                        cb.frames(stripes);
3752                    }
3753                }
3754                None => while rx.recv().is_ok() {},
3755            }
3756        });
3757        cap.deliver_tx = Some(tx);
3758        cap.deliver_discard = Some(discard);
3759        cap.deliver_join = Some(join);
3760    }
3761
3762    // A zero-copy session whose render targets cannot be allocated leaves the capture to a
3763    // readback path that still tries the GPU, as one given up mid-stream does.
3764    let mut zero_copy_refused = false;
3765    if let Some(encoder) = video_encoder {
3766        cap.encode_stats.n_stripes.store(1, Ordering::Relaxed);
3767        *cap.encode_stats.desc.lock().unwrap() = encoder_desc(&settings, Some(&encoder), true);
3768        set_wayland_active_codec(display_id, Some(settings.codec));
3769        log_stream_settings("Wayland", &settings, 1, Some(&encoder));
3770        let egl_display = state
3771            .gles_renderer
3772            .as_ref()
3773            .map(|r| r.egl_context().display().get_display_handle().handle)
3774            .unwrap_or(std::ptr::null());
3775        match state
3776            .gbm_device
3777            .as_ref()
3778            .and_then(|gbm| alloc_zero_copy_ring(gbm, &settings, display_id))
3779        {
3780            Some(ring) => bootstrap_zero_copy_pool(
3781                &mut cap,
3782                ring,
3783                display_id,
3784                encoder,
3785                egl_display,
3786                host_capture,
3787            ),
3788            None => {
3789                drop(encoder);
3790                eprintln!(
3791                    "[Wayland] Display {display_id}: zero-copy capture not started; using readback encode."
3792                );
3793                zero_copy_refused = true;
3794                report::capture("readback", false);
3795                report::capture_reason("the zero-copy render targets could not be allocated");
3796                if host_capture && let Some(h) = state.host.as_ref() {
3797                    h.set_buffer_type(display_id, false);
3798                }
3799            }
3800        }
3801    }
3802    if cap.gpu_pool.is_none() {
3803        bootstrap_readback_pool(
3804            &mut cap,
3805            display_id,
3806            // Host frames land in the pool as BGRA whatever the local renderer is; only
3807            // a GLES readback of our own compositing produces RGBA.
3808            state.use_gpu && !host_capture,
3809            gpu_intent && (zero_copy_refused || !state.use_gpu || different_gpu),
3810            None,
3811            prior_encode_join.take(),
3812        );
3813    }
3814    // A zero-copy start has no successor encode thread to inherit the outgoing readback
3815    // session, so the outgoing thread is reaped instead.
3816    state.encode_reaper.extend(prior_encode_join);
3817    // Force the keyframe unconditionally: the damage tracker and offscreen buffer
3818    // stay warm across stop/start, so a restarted capture on a static screen
3819    // otherwise produces no damage, no first frame, and no IDR in either path.
3820    cap.request_idr();
3821
3822    node.capture = Some(cap);
3823    // The start reprogrammed this output, and until a client answers at the new size the
3824    // compositor paints its clear color over whatever the client does not cover — a
3825    // freshly created output, whose session window is still parked at a placeholder size,
3826    // is covered by none of it. Those frames are held rather than streamed as a blank
3827    // screen; the deadline releases an output no client ever draws on.
3828    node.content_hold_until = Some(Instant::now() + WL_CONTENT_HOLD);
3829    state.output_nodes.insert(node_idx, node);
3830    wayland_alive().lock().unwrap().insert(display_id);
3831    answer_geometry_waiters(state, display_id, geometry_waiters);
3832}
3833
3834/// One output's render + capture tick: composite the elements overlapping this output
3835/// (positions made output-local by subtracting its layout origin), track damage, feed the
3836/// display's own encode path, and answer a pending screenshot on the primary. Returns true
3837/// when the tick was skipped because this display's encode pool was still busy (the caller
3838/// then retries shortly instead of waiting a full frame interval).
3839/// Stamp the watermark element onto `target` in place — no clear, so the
3840/// captured content underneath stays — returning the draw's sync point for the
3841/// encoder to wait on.
3842fn draw_host_watermark(
3843    renderer: &mut GlesRenderer,
3844    overlay: &crate::encoders::overlay::OverlayState,
3845    target: &mut Dmabuf,
3846    size: (i32, i32),
3847) -> Result<SyncPoint, String> {
3848    let elem = overlay
3849        .get_watermark_element(renderer)
3850        .ok_or("watermark element unavailable")?;
3851    let mut fb = renderer.bind(target).map_err(|e| format!("bind: {e:?}"))?;
3852    let mut frame = renderer
3853        .render(&mut fb, (size.0, size.1).into(), Transform::Normal)
3854        .map_err(|e| format!("render: {e:?}"))?;
3855    let dst = elem.geometry(1.0.into());
3856    let local = Rectangle::from_size(dst.size);
3857    elem.draw(&mut frame, elem.src(), dst, &[local], &[], None)
3858        .map_err(|e| format!("draw: {e:?}"))?;
3859    frame.finish().map_err(|e| format!("finish: {e:?}"))
3860}
3861
3862/// Re-compose `src` (the retained host frame) into `target`, with the watermark where `overlay`
3863/// is given: the path a moving watermark needs, since re-drawing over the same retained buffer
3864/// would leave trails.
3865fn compose_host_frame(
3866    renderer: &mut GlesRenderer,
3867    overlay: Option<&crate::encoders::overlay::OverlayState>,
3868    src: &Dmabuf,
3869    target: &mut Dmabuf,
3870    size: (i32, i32),
3871) -> Result<SyncPoint, String> {
3872    let elem = match overlay {
3873        Some(overlay) => Some(
3874            overlay
3875                .get_watermark_element(renderer)
3876                .ok_or("watermark element unavailable")?,
3877        ),
3878        None => None,
3879    };
3880    let tex = renderer
3881        .import_dmabuf(src, None)
3882        .map_err(|e| format!("import: {e:?}"))?;
3883    let mut fb = renderer.bind(target).map_err(|e| format!("bind: {e:?}"))?;
3884    let full: Rectangle<i32, Physical> = Rectangle::from_size((size.0, size.1).into());
3885    let mut frame = renderer
3886        .render(&mut fb, (size.0, size.1).into(), Transform::Normal)
3887        .map_err(|e| format!("render: {e:?}"))?;
3888    frame
3889        .render_texture_from_to(
3890            &tex,
3891            Rectangle::from_size((f64::from(size.0), f64::from(size.1)).into()),
3892            full,
3893            &[full],
3894            // Opaque: the capture format's undefined alpha must not blend, it
3895            // would leave the target's previous content (and stamped
3896            // watermarks) underneath.
3897            &[full],
3898            Transform::Normal,
3899            1.0,
3900            None,
3901            &[],
3902        )
3903        .map_err(|e| format!("texture: {e:?}"))?;
3904    if let Some(elem) = elem {
3905        let dst = elem.geometry(1.0.into());
3906        let local = Rectangle::from_size(dst.size);
3907        elem.draw(&mut frame, elem.src(), dst, &[local], &[], None)
3908            .map_err(|e| format!("draw: {e:?}"))?;
3909    }
3910    frame.finish().map_err(|e| format!("finish: {e:?}"))
3911}
3912
3913fn warn_once_host_watermark(e: &str) {
3914    static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
3915    if !WARNED.swap(true, Ordering::Relaxed) {
3916        eprintln!("[HostCapture] watermark compositing failed: {e}");
3917    }
3918}
3919
3920/// Copy full rows from `src` (tight `width*4` stride) into an Argb8888/Xrgb8888 shm
3921/// buffer, honoring the destination's own offset and stride.
3922fn copy_rows_into_shm(
3923    buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer,
3924    src: &[u8],
3925    width: i32,
3926    height: i32,
3927) -> Result<(), String> {
3928    use smithay::wayland::shm::with_buffer_contents_mut;
3929    let src_stride = (width.max(0) as usize) * 4;
3930    if src.len() < src_stride * (height.max(0) as usize) {
3931        return Err("source smaller than advertised".into());
3932    }
3933    with_buffer_contents_mut(buffer, |ptr, len, spec| {
3934        if spec.width < width || spec.height < height {
3935            return Err("shm buffer smaller than the output".to_string());
3936        }
3937        let dst_stride = spec.stride as usize;
3938        let offset = spec.offset as usize;
3939        if dst_stride < src_stride || len < offset + dst_stride * (height as usize) {
3940            return Err("shm stride or length mismatch".to_string());
3941        }
3942        for y in 0..height as usize {
3943            unsafe {
3944                std::ptr::copy_nonoverlapping(
3945                    src.as_ptr().add(y * src_stride),
3946                    ptr.add(offset + y * dst_stride),
3947                    src_stride,
3948                );
3949            }
3950        }
3951        Ok(())
3952    })
3953    .map_err(|e| format!("{e:?}"))?
3954}
3955
3956/// The GPU target the last render composited into: `source`, a zero-copy capture's target
3957/// still on the calloop, or else the output's own.
3958fn composited_target(
3959    node: &wayland::frontend::OutputNode,
3960    source: Option<&Dmabuf>,
3961) -> Result<Dmabuf, String> {
3962    source
3963        .or(node.offscreen_buffer.as_ref().map(|(_, dmabuf)| dmabuf))
3964        .cloned()
3965        .ok_or_else(|| "no composited buffer".to_string())
3966}
3967
3968/// Complete parked ext-image-copy-capture frames for this output from the frame the
3969/// render pass just produced. A dmabuf target is filled by one GPU blit from the
3970/// composited buffer (`composited_target`); an shm target by one readback (GLES) or one
3971/// row copy (pixman). With no damage, a session that has already received content keeps
3972/// its frame parked, so a static screen costs capture clients nothing.
3973fn service_copy_frames(
3974    state: &mut AppState,
3975    node: &mut wayland::frontend::OutputNode,
3976    source: Option<&Dmabuf>,
3977    width: i32,
3978    height: i32,
3979    damage_rects: &[Rectangle<i32, Physical>],
3980) {
3981    use smithay::backend::renderer::{Blit, BufferType, ExportMem, TextureFilter, buffer_type};
3982    use smithay::utils::Buffer as BufferCoords;
3983
3984    if state.copy_sessions.is_empty() {
3985        return;
3986    }
3987    let time = state.clock.now();
3988    for i in 0..state.copy_sessions.len() {
3989        {
3990            let cs = &state.copy_sessions[i];
3991            if cs.output.upgrade().as_ref() != Some(&node.output)
3992                || cs.pending.is_none()
3993                || (damage_rects.is_empty() && cs.delivered_once)
3994            {
3995                continue;
3996            }
3997        }
3998        let first = !state.copy_sessions[i].delivered_once;
3999        let frame = state.copy_sessions[i].pending.take().unwrap();
4000        let buffer = frame.buffer();
4001        let full = Rectangle::<i32, Physical>::new((0, 0).into(), (width, height).into());
4002        let result: Result<(), String> = match buffer_type(&buffer) {
4003            Some(BufferType::Dma) => (|| {
4004                let renderer = state.gles_renderer.as_mut().ok_or("no GLES renderer")?;
4005                let mut client = smithay::wayland::dmabuf::get_dmabuf(&buffer)
4006                    .map_err(|e| e.to_string())?
4007                    .clone();
4008                let mut composited = composited_target(node, source)?;
4009                let src = renderer
4010                    .bind(&mut composited)
4011                    .map_err(|e| format!("{e:?}"))?;
4012                let mut dst = renderer.bind(&mut client).map_err(|e| format!("{e:?}"))?;
4013                // ready() promises readable contents, and implicit dmabuf sync cannot
4014                // be relied on cross-process on every driver: wait out the blit fence
4015                // (microseconds) before declaring the frame done.
4016                let sync = renderer
4017                    .blit(&src, &mut dst, full, full, TextureFilter::Linear)
4018                    .map_err(|e| format!("{e:?}"))?;
4019                if !wait_render_fence(&sync, node.id) {
4020                    return Err("blit fence not signaled".to_string());
4021                }
4022                Ok(())
4023            })(),
4024            Some(BufferType::Shm) => (|| {
4025                if let Some(renderer) = state.gles_renderer.as_mut() {
4026                    let mut composited = composited_target(node, source)?;
4027                    let fb = renderer
4028                        .bind(&mut composited)
4029                        .map_err(|e| format!("{e:?}"))?;
4030                    let mapping = renderer
4031                        .copy_framebuffer(
4032                            &fb,
4033                            Rectangle::new((0, 0).into(), (width, height).into()),
4034                            Fourcc::Argb8888,
4035                        )
4036                        .map_err(|e| format!("{e:?}"))?;
4037                    let data = renderer
4038                        .map_texture(&mapping)
4039                        .map_err(|e| format!("{e:?}"))?;
4040                    copy_rows_into_shm(&buffer, data, width, height)
4041                } else {
4042                    copy_rows_into_shm(&buffer, &node.frame_buffer, width, height)
4043                }
4044            })(),
4045            _ => Err("unsupported buffer type".into()),
4046        };
4047        match result {
4048            Ok(()) => {
4049                let damage: Option<Vec<Rectangle<i32, BufferCoords>>> = if first {
4050                    None
4051                } else {
4052                    Some(
4053                        damage_rects
4054                            .iter()
4055                            .map(|r| {
4056                                Rectangle::new(
4057                                    (r.loc.x, r.loc.y).into(),
4058                                    (r.size.w, r.size.h).into(),
4059                                )
4060                            })
4061                            .collect(),
4062                    )
4063                };
4064                frame.success(Transform::Normal, damage, time);
4065                state.copy_sessions[i].delivered_once = true;
4066            }
4067            Err(e) => {
4068                eprintln!("[Wayland] copy-capture frame failed: {e}");
4069                frame.fail(CaptureFailureReason::Unknown);
4070            }
4071        }
4072    }
4073}
4074
4075/// Answer the frame callbacks of a surface-backed cursor. Xwayland and libwayland-cursor
4076/// clients wait for the cursor surface's frame callback before attaching their next
4077/// sprite, so a cursor surface that never gets one stops updating after its first sprite.
4078fn send_cursor_frame(state: &AppState, output: &Output, time: impl Into<Duration>) {
4079    if let Some(CursorImageStatus::Surface(surface)) = &state.current_cursor_icon {
4080        send_frames_surface_tree(surface, output, time, Some(Duration::ZERO), |_, _| {
4081            Some(output.clone())
4082        });
4083    }
4084}
4085
4086/// Hotspot of a surface-backed cursor, in the surface's logical coordinates (zero when the
4087/// client set none). The sprite is placed at pointer - hotspot, like named and X11 cursors.
4088fn cursor_surface_hotspot(
4089    surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
4090) -> Point<i32, smithay::utils::Logical> {
4091    with_states(surface, |states| {
4092        states
4093            .data_map
4094            .get::<std::sync::Mutex<smithay::input::pointer::CursorImageAttributes>>()
4095            .and_then(|attrs| attrs.lock().ok().map(|guard| guard.hotspot))
4096            .unwrap_or_default()
4097    })
4098}
4099
4100/// Longest a render tick waits for the GPU to finish a frame before giving that frame up.
4101const RENDER_FENCE_TIMEOUT: Duration = Duration::from_millis(500);
4102
4103/// Render ticks in a row that draw nothing before one closes an empty frame on its target.
4104/// smithay binds a dmabuf target through a texture with a framebuffer made for that bind, and
4105/// the NVIDIA driver keeps the state of each one never drawn to until a frame retires it (a
4106/// fence or a flush does not): about 15 KB a tick while the screen stands still.
4107const UNDRAWN_TICKS_PER_FRAME: u32 = 60;
4108
4109/// Reads `target` back as RGBA into `dst`. The read runs in a frame of the target: a dmabuf
4110/// bound as a texture gets a framebuffer of its own on each bind, which only a frame makes
4111/// current, so a bare read on a tick that drew nothing would read another framebuffer.
4112fn read_back_rgba(
4113    renderer: &mut GlesRenderer,
4114    target: &mut smithay::backend::renderer::gles::GlesTarget<'_>,
4115    width: i32,
4116    height: i32,
4117    dst: &mut [u8],
4118) {
4119    if let Err(e) = read_back_rgba_result(renderer, target, width, height, dst, false) {
4120        eprintln!("[Wayland] Readback failed: {e}");
4121    }
4122}
4123
4124/// `glBufferStorageEXT`, which GLES has only through `GL_EXT_buffer_storage`.
4125type BufferStorageFn = unsafe extern "system" fn(u32, isize, *const std::ffi::c_void, u32);
4126
4127const GL_MAP_READ_BIT: u32 = 0x0001;
4128const GL_MAP_PERSISTENT_BIT_EXT: u32 = 0x0040;
4129const GL_MAP_COHERENT_BIT_EXT: u32 = 0x0080;
4130const GL_CLIENT_STORAGE_BIT_EXT: u32 = 0x0200;
4131/// Pixel-pack buffers kept at once, as many screenshots as the process admits.
4132const READBACK_SLOTS_MAX: usize = computer_use::MAX_SCREENSHOT_REQUESTS;
4133/// How long an unused pixel-pack buffer is kept for the next screenshot.
4134const READBACK_SLOT_IDLE: Duration = Duration::from_secs(10);
4135
4136/// The persistently mapped pixel-pack buffers GLES screenshots are read back into, so the
4137/// compositor only issues the transfer and the caller copies the pixels out once the GPU has
4138/// written them. Without `GL_EXT_buffer_storage`, or with `PIXELFLUX_SCREENSHOT_SYNC` set, the
4139/// compositor reads back synchronously into the caller's buffer instead. Declared ahead of the
4140/// renderer in [`AppState`](wayland::frontend::AppState), so its drop forgets every mapping, waiting
4141/// out a copy under way, before the GL context goes away.
4142#[derive(Default)]
4143pub(crate) struct ScreenshotReadbacks {
4144    slots: Vec<(Arc<computer_use::ReadbackSlot>, Instant)>,
4145    /// The entry point, once looked up: `None` before the first GLES screenshot.
4146    storage: Option<Option<BufferStorageFn>>,
4147}
4148
4149impl ScreenshotReadbacks {
4150    fn lookup(gl: &smithay::backend::renderer::gles::ffi::Gles2) -> Option<BufferStorageFn> {
4151        if std::env::var_os("PIXELFLUX_SCREENSHOT_SYNC").is_some_and(|v| !v.is_empty() && v != "0")
4152        {
4153            return None;
4154        }
4155        // SAFETY: GL_EXTENSIONS is a NUL-terminated string the context owns.
4156        let extensions = unsafe {
4157            let names = gl.GetString(smithay::backend::renderer::gles::ffi::EXTENSIONS);
4158            if names.is_null() {
4159                return None;
4160            }
4161            std::ffi::CStr::from_ptr(names as *const std::ffi::c_char)
4162                .to_string_lossy()
4163                .into_owned()
4164        };
4165        if !extensions
4166            .split(' ')
4167            .any(|name| name == "GL_EXT_buffer_storage")
4168        {
4169            return None;
4170        }
4171        // SAFETY: a non-null address of the extension's entry point has its signature.
4172        unsafe {
4173            // A GL context is current, so its EGL display, made by `egl_display`, has
4174            // libEGL open.
4175            #[allow(clippy::disallowed_methods)]
4176            let address = smithay::backend::egl::get_proc_address("glBufferStorageEXT");
4177            (!address.is_null())
4178                .then(|| std::mem::transmute::<*const std::ffi::c_void, BufferStorageFn>(address))
4179        }
4180    }
4181
4182    /// A free slot of `len` bytes, made when none is; `None` sends the screenshot down the
4183    /// synchronous readback.
4184    fn acquire(
4185        &mut self,
4186        gl: &smithay::backend::renderer::gles::ffi::Gles2,
4187        len: usize,
4188        now: Instant,
4189    ) -> Option<Arc<computer_use::ReadbackSlot>> {
4190        let storage = (*self.storage.get_or_insert_with(|| Self::lookup(gl)))?;
4191        if let Some((slot, used)) = self
4192            .slots
4193            .iter_mut()
4194            .find(|(slot, _)| !slot.in_use() && slot.size == len)
4195        {
4196            *used = now;
4197            return Some(Arc::clone(slot));
4198        }
4199        if self.slots.len() >= READBACK_SLOTS_MAX {
4200            let free = self.slots.iter().position(|(slot, _)| !slot.in_use())?;
4201            Self::delete(gl, &self.slots.swap_remove(free).0);
4202        }
4203        let slot = Arc::new(Self::create(gl, storage, len)?);
4204        self.slots.push((Arc::clone(&slot), now));
4205        Some(slot)
4206    }
4207
4208    fn create(
4209        gl: &smithay::backend::renderer::gles::ffi::Gles2,
4210        storage: BufferStorageFn,
4211        len: usize,
4212    ) -> Option<computer_use::ReadbackSlot> {
4213        use smithay::backend::renderer::gles::ffi;
4214        let size = isize::try_from(len).ok()?;
4215        let access = GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT_EXT | GL_MAP_COHERENT_BIT_EXT;
4216        // SAFETY: plain GL calls on the current context; the mapping is checked before use.
4217        unsafe {
4218            let mut id = 0;
4219            gl.GenBuffers(1, &mut id);
4220            gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, id);
4221            storage(
4222                ffi::PIXEL_PACK_BUFFER,
4223                size,
4224                std::ptr::null(),
4225                access | GL_CLIENT_STORAGE_BIT_EXT,
4226            );
4227            let ptr = gl.MapBufferRange(ffi::PIXEL_PACK_BUFFER, 0, size, access);
4228            gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, 0);
4229            if ptr.is_null() || gl.GetError() != ffi::NO_ERROR {
4230                gl.DeleteBuffers(1, &id);
4231                return None;
4232            }
4233            Some(computer_use::ReadbackSlot::new(id, ptr as *const u8, len))
4234        }
4235    }
4236
4237    fn delete(
4238        gl: &smithay::backend::renderer::gles::ffi::Gles2,
4239        slot: &computer_use::ReadbackSlot,
4240    ) {
4241        use smithay::backend::renderer::gles::ffi;
4242        slot.unmap();
4243        // SAFETY: the slot is free and forgotten, so nothing reads the mapping any more.
4244        unsafe {
4245            gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, slot.id);
4246            gl.UnmapBuffer(ffi::PIXEL_PACK_BUFFER);
4247            gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, 0);
4248            gl.DeleteBuffers(1, &slot.id);
4249        }
4250    }
4251
4252    /// Whether a free slot has gone unused long enough to be deleted.
4253    fn has_idle(&self, now: Instant) -> bool {
4254        self.slots
4255            .iter()
4256            .any(|(slot, used)| !slot.in_use() && now.duration_since(*used) >= READBACK_SLOT_IDLE)
4257    }
4258
4259    fn reap_idle(&mut self, gl: &smithay::backend::renderer::gles::ffi::Gles2, now: Instant) {
4260        self.slots.retain(|(slot, used)| {
4261            let idle = !slot.in_use() && now.duration_since(*used) >= READBACK_SLOT_IDLE;
4262            if idle {
4263                Self::delete(gl, slot);
4264            }
4265            !idle
4266        });
4267    }
4268}
4269
4270impl Drop for ScreenshotReadbacks {
4271    fn drop(&mut self) {
4272        for (slot, _) in &self.slots {
4273            slot.unmap();
4274        }
4275    }
4276}
4277
4278/// Start reading the bound target back into a pixel-pack buffer and return the readback the
4279/// caller waits for, or `None` where the buffers are unavailable.
4280fn read_back_rgba_async(
4281    renderer: &mut GlesRenderer,
4282    readbacks: &mut ScreenshotReadbacks,
4283    target: &mut smithay::backend::renderer::gles::GlesTarget<'_>,
4284    width: i32,
4285    height: i32,
4286) -> Option<Result<computer_use::GpuReadback, String>> {
4287    use smithay::backend::renderer::gles::ffi;
4288    let len = screenshot_len(width, height)?;
4289    let now = Instant::now();
4290    let slot = renderer
4291        .with_context(|gl| readbacks.acquire(gl, len, now))
4292        .ok()??;
4293    Some((|| {
4294        let mut frame = renderer
4295            .render(target, (width, height).into(), Transform::Normal)
4296            .map_err(|e| format!("Screenshot readback failed: {e:?}"))?;
4297        let error = frame
4298            .with_context(|gl| unsafe {
4299                gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, slot.id);
4300                gl.ReadPixels(
4301                    0,
4302                    0,
4303                    width,
4304                    height,
4305                    ffi::RGBA,
4306                    ffi::UNSIGNED_BYTE,
4307                    std::ptr::null_mut(),
4308                );
4309                gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, 0);
4310                gl.GetError()
4311            })
4312            .map_err(|e| format!("Screenshot readback failed: {e:?}"))?;
4313        if error != ffi::NO_ERROR {
4314            return Err(format!("Screenshot readback GL error: 0x{error:x}"));
4315        }
4316        // The frame's fence follows the transfer, so the caller waits for exactly that.
4317        let sync = frame
4318            .finish()
4319            .map_err(|e| format!("Screenshot readback failed: {e:?}"))?;
4320        Ok(computer_use::GpuReadback { slot, sync, len })
4321    })())
4322}
4323
4324/// Screenshot reads also check GL errors before any pixels can be handed to the caller.
4325fn read_back_rgba_result(
4326    renderer: &mut GlesRenderer,
4327    target: &mut smithay::backend::renderer::gles::GlesTarget<'_>,
4328    width: i32,
4329    height: i32,
4330    dst: &mut [u8],
4331    check_error: bool,
4332) -> Result<(), String> {
4333    if check_error
4334        && (width <= 0 || height <= 0 || dst.len() < width as usize * height as usize * 4)
4335    {
4336        return Err("Screenshot readback buffer is too small".to_string());
4337    }
4338    let error = renderer
4339        .render(target, (width, height).into(), Transform::Normal)
4340        .and_then(|mut frame| {
4341            frame.with_context(|gl| unsafe {
4342                gl.ReadPixels(
4343                    0,
4344                    0,
4345                    width,
4346                    height,
4347                    smithay::backend::renderer::gles::ffi::RGBA,
4348                    smithay::backend::renderer::gles::ffi::UNSIGNED_BYTE,
4349                    dst.as_mut_ptr() as *mut std::ffi::c_void,
4350                );
4351                if check_error { gl.GetError() } else { 0 }
4352            })
4353        })
4354        .map_err(|e| format!("Screenshot readback failed: {e:?}"))?;
4355    if error != 0 {
4356        return Err(format!("Screenshot readback GL error: 0x{error:x}"));
4357    }
4358    Ok(())
4359}
4360
4361/// Where a screenshot's pixels are once its output has rendered.
4362enum ShotPixels {
4363    /// The render's own CPU pixels: the pool slot while one is held, else the frame buffer.
4364    Rendered,
4365    /// Already in the buffer the caller receives.
4366    Owned(computer_use::ScreenshotBuffer),
4367    /// On their way from the GPU, for the caller to wait for and copy out.
4368    Gpu(computer_use::GpuReadback),
4369}
4370
4371/// The byte length of one `width` x `height` RGBA frame, `None` for an empty or overflowing one.
4372fn screenshot_len(width: i32, height: i32) -> Option<usize> {
4373    usize::try_from(width)
4374        .ok()
4375        .zip(usize::try_from(height).ok())
4376        .filter(|(w, h)| *w > 0 && *h > 0)
4377        .and_then(|(w, h)| w.checked_mul(h))
4378        .and_then(|n| n.checked_mul(4))
4379}
4380
4381/// Copy exactly one complete frame into a caller's buffer, excluding any scratch-buffer
4382/// capacity from an older size.
4383fn screenshot_pixels(
4384    pixels: &[u8],
4385    width: i32,
4386    height: i32,
4387) -> Result<computer_use::ScreenshotBuffer, String> {
4388    let expected = screenshot_len(width, height)
4389        .ok_or_else(|| "Screenshot dimensions are invalid".to_string())?;
4390    let pixels = pixels
4391        .get(..expected)
4392        .ok_or_else(|| "Screenshot readback produced incomplete pixels".to_string())?;
4393    Ok(computer_use::ScreenshotBuffer::copy_of(pixels))
4394}
4395
4396/// Reject truncated host mappings instead of retaining pixels from an earlier frame.
4397fn validate_host_screenshot(
4398    frame: &wayland::host::HostCpuFrame,
4399    width: i32,
4400    height: i32,
4401) -> Result<computer_use::ScreenshotPixelFormat, String> {
4402    if width <= 0 || height <= 0 {
4403        return Err("Screenshot host dimensions are invalid".to_string());
4404    }
4405    let src_bpp = match frame.format {
4406        f if f == wl_shm::Format::Bgr888 as u32 || f == wl_shm::Format::Rgb888 as u32 => 3,
4407        f if f == wl_shm::Format::Xrgb8888 as u32
4408            || f == wl_shm::Format::Argb8888 as u32
4409            || f == wl_shm::Format::Xbgr8888 as u32
4410            || f == wl_shm::Format::Abgr8888 as u32 =>
4411        {
4412            4
4413        }
4414        _ => return Err("Screenshot host pixel format is unsupported".to_string()),
4415    };
4416    let row = (width as usize)
4417        .checked_mul(src_bpp)
4418        .ok_or_else(|| "Screenshot host dimensions overflow".to_string())?;
4419    let required = (height as usize - 1)
4420        .checked_mul(frame.stride)
4421        .and_then(|n| n.checked_add(row))
4422        .and_then(|n| n.checked_add(frame.offset));
4423    if frame.stride < row || required.is_none_or(|n| n > (*frame.map).as_ref().len()) {
4424        return Err("Screenshot host frame is incomplete".to_string());
4425    }
4426    Ok(
4427        if frame.format == wl_shm::Format::Xrgb8888 as u32
4428            || frame.format == wl_shm::Format::Xbgr8888 as u32
4429        {
4430            computer_use::ScreenshotPixelFormat::Bgrx
4431        } else {
4432            computer_use::ScreenshotPixelFormat::Bgra
4433        },
4434    )
4435}
4436
4437#[cfg(test)]
4438mod screenshot_frame_tests {
4439    use super::{screenshot_pixels, validate_host_screenshot};
4440    use crate::computer_use::ScreenshotPixelFormat;
4441    use crate::wayland::host::HostCpuFrame;
4442    use smithay::reexports::wayland_server::protocol::wl_shm;
4443    use std::sync::Arc;
4444
4445    #[test]
4446    fn screenshot_readbacks_forget_their_mappings_before_the_context_goes() {
4447        let mapping = [0u8; 8];
4448        // SAFETY: `mapping` outlives the copies below.
4449        let slot = Arc::new(unsafe {
4450            crate::computer_use::ReadbackSlot::new(3, mapping.as_ptr(), mapping.len())
4451        });
4452        let held = Arc::clone(&slot);
4453        let then = std::time::Instant::now();
4454        let mut readbacks = super::ScreenshotReadbacks::default();
4455        readbacks.slots.push((slot, then));
4456        // A slot a screenshot holds is never idle; a free one is, once unused long enough.
4457        assert!(!readbacks.has_idle(then + super::READBACK_SLOT_IDLE));
4458        drop(readbacks);
4459        assert_eq!(
4460            held.copy_out(8).err().as_deref(),
4461            Some("Screenshot compositor stopped"),
4462            "a screenshot still holding a slot cannot read a mapping its context let go"
4463        );
4464        let mut idle = super::ScreenshotReadbacks::default();
4465        // SAFETY: as above.
4466        let free = Arc::new(unsafe {
4467            crate::computer_use::ReadbackSlot::new(4, mapping.as_ptr(), mapping.len())
4468        });
4469        idle.slots.push((free, then));
4470        assert!(!idle.has_idle(then));
4471        assert!(idle.has_idle(then + super::READBACK_SLOT_IDLE));
4472    }
4473
4474    #[test]
4475    fn screenshot_owns_only_the_current_frame_pixels() {
4476        let mut source = vec![1, 2, 3, 4, 5, 6, 7, 8, 99, 99, 99, 99];
4477        let pixels = screenshot_pixels(&source, 2, 1).unwrap();
4478        source.fill(0);
4479        assert_eq!(&*pixels, [1, 2, 3, 4, 5, 6, 7, 8]);
4480    }
4481
4482    #[test]
4483    fn screenshot_rejects_invalid_dimensions_and_short_pixels() {
4484        for (width, height) in [(0, 1), (1, 0), (-1, 2), (2, -1), (2, 2)] {
4485            assert!(screenshot_pixels(&[0; 12], width, height).is_err());
4486        }
4487    }
4488
4489    fn host_frame(
4490        bytes: usize,
4491        stride: usize,
4492        offset: usize,
4493        format: wl_shm::Format,
4494    ) -> HostCpuFrame {
4495        HostCpuFrame {
4496            map: Arc::new(vec![0u8; bytes]),
4497            offset,
4498            stride,
4499            format: format as u32,
4500        }
4501    }
4502
4503    #[test]
4504    fn screenshot_validates_host_rows_offset_and_truncation() {
4505        for (format, row) in [(wl_shm::Format::Argb8888, 8), (wl_shm::Format::Bgr888, 6)] {
4506            let stride = row + 4;
4507            let required = 3 + stride + row;
4508            assert!(
4509                validate_host_screenshot(&host_frame(required, stride, 3, format), 2, 2).is_ok()
4510            );
4511            assert!(
4512                validate_host_screenshot(&host_frame(required - 1, stride, 3, format), 2, 2)
4513                    .is_err()
4514            );
4515            assert!(
4516                validate_host_screenshot(&host_frame(required, row - 1, 3, format), 2, 2).is_err()
4517            );
4518        }
4519    }
4520
4521    #[test]
4522    fn screenshot_retains_host_alpha_semantics_after_bgra_conversion() {
4523        for (source, expected) in [
4524            (wl_shm::Format::Xrgb8888, ScreenshotPixelFormat::Bgrx),
4525            (wl_shm::Format::Xbgr8888, ScreenshotPixelFormat::Bgrx),
4526            (wl_shm::Format::Argb8888, ScreenshotPixelFormat::Bgra),
4527            (wl_shm::Format::Abgr8888, ScreenshotPixelFormat::Bgra),
4528            (wl_shm::Format::Rgb888, ScreenshotPixelFormat::Bgra),
4529            (wl_shm::Format::Bgr888, ScreenshotPixelFormat::Bgra),
4530        ] {
4531            assert_eq!(
4532                validate_host_screenshot(&host_frame(16, 8, 0, source), 2, 2).unwrap(),
4533                expected
4534            );
4535        }
4536    }
4537
4538    #[test]
4539    fn screenshot_rejects_unsupported_host_formats_and_dimensions() {
4540        let mut frame = host_frame(16, 8, 0, wl_shm::Format::Argb8888);
4541        assert!(validate_host_screenshot(&frame, 0, 2).is_err());
4542        assert!(validate_host_screenshot(&frame, 2, 0).is_err());
4543        frame.offset = usize::MAX;
4544        assert!(validate_host_screenshot(&frame, 2, 2).is_err());
4545        frame.offset = 0;
4546        frame.format = u32::MAX;
4547        assert!(validate_host_screenshot(&frame, 2, 2).is_err());
4548    }
4549}
4550
4551/// Wait, bounded, for a render's fence to signal before the tick reuses or releases the
4552/// buffers it sampled.
4553///
4554/// A client buffer a render read can be released and destroyed by the client's next commit
4555/// (a nested session swaps its swapchain across a mode change); without implicit dmabuf
4556/// fencing the driver then frees it under an unfinished read and faults the channel, after
4557/// which no fence of the context signals. The bound turns that into a logged, dropped frame
4558/// rather than a compositor thread parked forever. The wait is one `eglClientWaitSync` to the
4559/// deadline, flushing whatever commands still queue ahead of the fence; a fence-less sync
4560/// point is reached at once.
4561fn wait_render_fence(sync: &SyncPoint, display_id: u32) -> bool {
4562    let reached = match sync.get::<EGLFence>() {
4563        Some(fence) => fence
4564            .client_wait(Some(RENDER_FENCE_TIMEOUT), true)
4565            .unwrap_or(false),
4566        None => sync.wait().is_ok(),
4567    };
4568    if !reached {
4569        eprintln!(
4570            "[Wayland] Display {display_id}: render fence not signaled within {:?}; frame dropped.",
4571            RENDER_FENCE_TIMEOUT
4572        );
4573    }
4574    reached
4575}
4576
4577/// Render every output that is due for `trigger`, and say whether a readback pool was still
4578/// busy. The nodes are taken out of the state so each per-output render can borrow the shared
4579/// renderer/space alongside its own damage tracker and buffers.
4580fn render_pass(state: &mut AppState, trigger: TickTrigger) -> bool {
4581    let drivers: Vec<bool> = state
4582        .output_nodes
4583        .iter()
4584        .map(|n| drives_screen(&state.output_nodes, n))
4585        .collect();
4586    let mut nodes = std::mem::take(&mut state.output_nodes);
4587    let mut any_pool_busy = false;
4588    for (node, drives) in nodes.iter_mut().zip(drivers) {
4589        if render_node_tick(state, node, trigger, drives) {
4590            any_pool_busy = true;
4591        }
4592    }
4593    state.output_nodes = nodes;
4594    any_pool_busy
4595}
4596
4597/// Render each capture a change waits on (`WlCapture::change_pending`: a client's commit,
4598/// input, a host frame) once its period is open: a change takes its period's frame as it
4599/// lands, the X11 captures' schedule, rather than waiting for the frame timer. A capture whose
4600/// period a frame already took keeps waiting, and the timer wakes as the next one opens.
4601fn serve_changes(state: &mut AppState) {
4602    let now = Instant::now();
4603    let open: Vec<u32> = state
4604        .output_nodes
4605        .iter()
4606        .filter_map(|n| {
4607            let cap = n.capture.as_ref()?;
4608            (cap.change_pending && cap.pace.due(TickTrigger::Damage, capture_period(cap), now))
4609                .then_some(n.id)
4610        })
4611        .collect();
4612    if open.is_empty() {
4613        return;
4614    }
4615    state.space.refresh();
4616    for id in open {
4617        render_display(state, id, TickTrigger::Damage);
4618    }
4619}
4620
4621/// Answer the frame callbacks of each captured display's clients once a window, on its boundary:
4622/// the vblank a display gives them. A change renders as it lands (`serve_changes`), and a
4623/// client drawing on frame callbacks takes its clock from them, which the boundaries keep even.
4624/// The frame timer wakes at every boundary while a display captures, and calls this each time.
4625fn answer_frame_callbacks(state: &mut AppState) {
4626    let now = Instant::now();
4627    let time = state.clock.now();
4628    for i in 0..state.output_nodes.len() {
4629        let node = &state.output_nodes[i];
4630        let Some(cap) = node.capture.as_ref() else {
4631            continue;
4632        };
4633        if state
4634            .host
4635            .as_ref()
4636            .is_some_and(|h| h.has_output_for(node.id))
4637        {
4638            continue;
4639        }
4640        let Some(opened) = cap.pace.window_opened(capture_period(cap), now) else {
4641            continue;
4642        };
4643        if cap.callbacks_at.is_some_and(|at| at >= opened) {
4644            continue;
4645        }
4646        // A client whose last commit is not rendered yet is answered once it is: asked to draw
4647        // again meanwhile, it would draw frames a busy capture never shows.
4648        let owed = cap.change_pending;
4649        let output = node.output.clone();
4650        if !owed && drives_screen(&state.output_nodes, node) {
4651            for window in state.space.elements_for_output(&output) {
4652                window.send_frame(&output, time, Some(Duration::ZERO), |_, _| {
4653                    Some(output.clone())
4654                });
4655            }
4656            for layer in layer_map_for_output(&output).layers() {
4657                layer.send_frame(&output, time, Some(Duration::ZERO), |_, _| {
4658                    Some(output.clone())
4659                });
4660            }
4661        }
4662        if let Some(cap) = state.output_nodes[i].capture.as_mut() {
4663            cap.callbacks_at = Some(opened);
4664            cap.callbacks_owed = owed;
4665        }
4666    }
4667}
4668
4669/// One display's render tick, for a trigger that concerns that display alone.
4670fn render_display(state: &mut AppState, display_id: u32, trigger: TickTrigger) {
4671    let drives = state
4672        .output_nodes
4673        .iter()
4674        .find(|n| n.id == display_id)
4675        .is_some_and(|n| drives_screen(&state.output_nodes, n));
4676    let mut nodes = std::mem::take(&mut state.output_nodes);
4677    if let Some(node) = nodes.iter_mut().find(|n| n.id == display_id) {
4678        render_node_tick(state, node, trigger, drives);
4679    }
4680    state.output_nodes = nodes;
4681}
4682
4683/// Views share one output, so its clients are driven once a frame, by the fastest display
4684/// capturing that screen (ties go to the lowest number) -- a client asked to draw once per
4685/// view would render as many times a frame as there are displays over it, one asked by nobody
4686/// (the screen itself is often captured by views alone) would freeze on the frame it first
4687/// painted, and one driven by a slower sibling would stream stale frames on the faster display.
4688fn drives_screen(
4689    nodes: &[wayland::frontend::OutputNode],
4690    node: &wayland::frontend::OutputNode,
4691) -> bool {
4692    let screen = node.owner.unwrap_or(node.id);
4693    let fps = |n: &wayland::frontend::OutputNode| {
4694        n.capture
4695            .as_ref()
4696            .map(|c| c.settings.target_fps)
4697            .unwrap_or(0.0)
4698    };
4699    let own_fps = fps(node);
4700    !nodes.iter().any(|n| {
4701        n.owner.unwrap_or(n.id) == screen
4702            && n.capture.is_some()
4703            && (fps(n) > own_fps || (fps(n) == own_fps && n.id < node.id))
4704    })
4705}
4706
4707/// One display's frame: decide whether this tick produces one, reserve a pool buffer, take
4708/// the host's frame or compose the output's own, publish it to the encode thread, and answer
4709/// the clients' frame callbacks and presentation feedback. True where the encoder still holds
4710/// every pool buffer and the tick is skipped.
4711///
4712/// The output's own frame is its element list (`composition_elements`), rendered by the GL
4713/// renderer into a zero-copy target or read back, or by pixman straight into a pool buffer.
4714/// Where that list is one opaque surface over exactly the output and nothing else
4715/// (`scanout_candidate`), the frame is the client's buffer and nothing is drawn: a zero-copy
4716/// session is handed the dmabuf itself and the pixman path's encode thread reads the shm
4717/// buffer's rows where they lie, each held for the client until the encode is done
4718/// (`WlCapture::scanout_inflight`); the damage trackers record the frame either way, so a
4719/// later composite repaints what changed meanwhile. Wayland otherwise costs a render pass X11
4720/// never pays, since X11 clients draw into the one framebuffer the capture reads, and this is
4721/// the one case where the compositor can skip its own.
4722fn render_node_tick(
4723    state: &mut AppState,
4724    node: &mut wayland::frontend::OutputNode,
4725    trigger: TickTrigger,
4726    drives_screen: bool,
4727) -> bool {
4728    state.pending_screenshots.prune(Instant::now());
4729    let take_screenshot = state.pending_screenshots.contains(node.id);
4730    // Copy-capture clients take the whole screen, which only the screen itself composites.
4731    let copy_frame_wanted = node.owner.is_none() && state.copy_frame_pending_for(&node.output);
4732    if node.capture.is_none() && !take_screenshot && !copy_frame_wanted {
4733        return false;
4734    }
4735
4736    // Per-display frame pacing under the one shared timer, and under input.
4737    if let Some(cap) = node.capture.as_ref()
4738        && !take_screenshot
4739        && !cap.pace.due(trigger, capture_period(cap), Instant::now())
4740    {
4741        return false;
4742    }
4743
4744    let output = node.output.clone();
4745    let origin: Point<i32, smithay::utils::Logical> = node.pos.into();
4746    let output_scale_val = output.current_scale().fractional_scale();
4747    let (width, height) = match node.capture.as_ref() {
4748        Some(c) => (c.settings.width, c.settings.height),
4749        None if node.owner.is_some() => node.view_size,
4750        None => output
4751            .current_mode()
4752            .map(|m| (m.size.w, m.size.h))
4753            .unwrap_or((0, 0)),
4754    };
4755    if width <= 0 || height <= 0 {
4756        return false;
4757    }
4758    if node.frame_buffer.len() < (width as usize) * (height as usize) * 4 {
4759        node.frame_buffer = vec![0u8; (width as usize) * (height as usize) * 4];
4760    }
4761    // A view's tracker is static, so a display that changed resolution, or whose
4762    // screen changed scale, needs a new one; a screen's follows its output on its own.
4763    if node.owner.is_some()
4764        && (node.view_size != (width, height) || (node.view_scale - output_scale_val).abs() > 1e-6)
4765    {
4766        node.damage_tracker =
4767            OutputDamageTracker::new((width, height), output_scale_val, Transform::Normal);
4768        node.content_tracker = None;
4769        node.view_size = (width, height);
4770        node.view_scale = output_scale_val;
4771    }
4772    let logical_w = (f64::from(width) / output_scale_val).round();
4773    let logical_h = (f64::from(height) / output_scale_val).round();
4774
4775    // A reconfigured or freshly created output composites its clear color wherever a client
4776    // has not yet answered the new size; publishing is held until one covers the output so
4777    // that gray never reaches the stream. Compositing continues, so the frame callbacks the
4778    // waited-on clients redraw on keep flowing and the hold cannot deadlock on itself. Host
4779    // capture streams the host compositor's own frames and has no such gap.
4780    let hold_frame = match node.content_hold_until {
4781        Some(deadline)
4782            if state.host.is_none()
4783                && Instant::now() < deadline
4784                && !wayland::frontend::output_content_covers(
4785                    &state.space,
4786                    node.owner.unwrap_or(node.id),
4787                    logical_w,
4788                    logical_h,
4789                ) =>
4790        {
4791            true
4792        }
4793        Some(_) => {
4794            node.content_hold_until = None;
4795            false
4796        }
4797        None => false,
4798    };
4799
4800    // A recorder connecting counts as an IDR request, kept armed across skipped ticks.
4801    if let Some(cap) = node.capture.as_mut()
4802        && cap
4803            .recording_sink
4804            .as_ref()
4805            .map(|s| s.should_force_idr())
4806            .unwrap_or(false)
4807    {
4808        cap.request_idr();
4809    }
4810    let requested_idr = node
4811        .capture
4812        .as_ref()
4813        .map(|c| c.pending_force_idr)
4814        .unwrap_or(false);
4815    // A client keyframe request lands on the hardware path's atomic (RequestIdr sets
4816    // it whenever an encode pool exists); host mode consults it — without consuming —
4817    // to decide whether a static screen must re-encode its retained frame.
4818    let hw_idr_pending = node
4819        .capture
4820        .as_ref()
4821        .map(|c| c.encode_controls.force_idr.load(Ordering::Relaxed))
4822        .unwrap_or(false);
4823    let want_idr_for_host = requested_idr || hw_idr_pending;
4824
4825    // A dead encode thread (a panic, a zero-copy session given up) cannot drain its pool:
4826    // every publish would park the slot and each tick would silently skip while
4827    // is_capturing still reports true. A readback path is rebuilt in place, reusing a
4828    // cleanly handed-back readback session if any.
4829    if let Some(cap) = node.capture.as_mut()
4830        && cap.encode_join.as_ref().is_some_and(|j| j.is_finished())
4831    {
4832        let host_owned = state
4833            .host
4834            .as_ref()
4835            .is_some_and(|h| h.has_output_for(node.id));
4836        let prior = cap.encode_join.take().and_then(|j| j.join().ok().flatten());
4837        if let Some(pool) = cap.encode_pool.take() {
4838            pool.shutdown();
4839        }
4840        if end_zero_copy(cap) {
4841            let _report = report::enter(&cap.report);
4842            report::capture("readback", false);
4843            report::capture_reason("the zero-copy encoder failed repeatedly and was given up");
4844            release_host_inflight(state.host.as_ref(), cap, node.id, true);
4845            // The host has to switch to buffers the readback path can read back on the CPU.
4846            if host_owned && let Some(h) = state.host.as_ref() {
4847                h.set_buffer_type(node.id, false);
4848            }
4849        } else {
4850            eprintln!("[Wayland] encode thread died; rebuilding the readback path.");
4851        }
4852        let s = &cap.settings;
4853        let try_gpu = s.codec.is_video() && !(s.use_cpu || s.encode_node_index == -1);
4854        // Host frames reach the pool as BGRA; only our own GLES readback produces RGBA.
4855        bootstrap_readback_pool(
4856            cap,
4857            node.id,
4858            state.use_gpu && !host_owned,
4859            try_gpu,
4860            prior,
4861            None,
4862        );
4863        cap.request_idr();
4864    }
4865
4866    let mut pool_slot: Option<(usize, Vec<u8>)> = None;
4867    if !hold_frame
4868        && let Some(cap) = node.capture.as_ref()
4869        && let Some(ref pool) = cap.encode_pool
4870    {
4871        pool_slot = pool.try_begin();
4872        if pool_slot.is_none() {
4873            return true;
4874        }
4875    }
4876    // A zero-copy session composites into a target the encode thread does not hold, held
4877    // frames too, so each target's age counts every render.
4878    let mut gpu_slot: Option<(usize, Dmabuf)> = None;
4879    if let Some(cap) = node.capture.as_ref()
4880        && let Some(ref pool) = cap.gpu_pool
4881    {
4882        gpu_slot = pool.try_begin();
4883        if gpu_slot.is_none() {
4884            return true;
4885        }
4886    }
4887
4888    let loc_enum = node
4889        .capture
4890        .as_ref()
4891        .map(|c| c.settings.watermark_location_enum)
4892        .unwrap_or(state.settings.watermark_location_enum);
4893    node.overlay_state.update_position(width, height, loc_enum);
4894
4895    // Host-capture mode: the host compositor already blitted this display's frame
4896    // into one of our buffers (screencopy); adopt it in place of compositing. Its tick
4897    // is recorded below, once it is known a frame will be published.
4898    let host_mode = state
4899        .host
4900        .as_ref()
4901        .map(|h| h.has_output_for(node.id))
4902        .unwrap_or(false);
4903    if !host_mode && let Some(cap) = node.capture.as_mut() {
4904        let period = capture_period(cap);
4905        let paced = FramePace::input_paced(state.input_interval, period);
4906        cap.pace.ticked(trigger, period, Instant::now(), paced);
4907        cap.change_pending = false;
4908    }
4909
4910    // The cursor is composited only on the output the pointer is on, at that output's
4911    // scale; its position is output-local.
4912    let pointer_local: Option<Point<f64, smithay::utils::Logical>> =
4913        state.pointer_shown_at().and_then(|pos| {
4914            let rect = Rectangle::<f64, smithay::utils::Logical>::new(
4915                origin.to_f64(),
4916                (logical_w, logical_h).into(),
4917            );
4918            if rect.contains(pos) {
4919                Some(pos - origin.to_f64())
4920            } else {
4921                None
4922            }
4923        });
4924
4925    // The picture's capture time for the stats: compositing is this path's capture.
4926    let composite_ns = wayland::host::now_ns();
4927    let mut render_success = false;
4928    let mut screenshot_result = None;
4929    let mut render_sync = None;
4930    let mut damage_rects: Vec<Rectangle<i32, Physical>> = Vec::new();
4931    let needs_full = node
4932        .capture
4933        .as_ref()
4934        .map(|c| c.needs_full_render)
4935        .unwrap_or(!node.target_seeded);
4936
4937    if state.host.is_some() && !host_mode {
4938        // No host output backs this display (start_capture already warned):
4939        // produce nothing rather than the compositor's own empty content.
4940        cancel_slots(node, &mut pool_slot, &mut gpu_slot);
4941        return false;
4942    }
4943    // A dmabuf handed to the GPU encoder in place of the slot's own: in host mode the new or
4944    // retained host frame, else a covering client's buffer (`scanout_candidate`); and whether
4945    // it is the retained host frame itself rather than a composite of it.
4946    let mut source_dmabuf: Option<Dmabuf> = None;
4947    let mut host_enc_retained = false;
4948    let mut new_stamp: Option<i64> = None;
4949    // Host software frames arrive BGRA, so anything reading this display's frame buffer
4950    // back has to know it is not the GLES readback's RGBA.
4951    let mut screenshot_format = if state.use_gpu {
4952        computer_use::ScreenshotPixelFormat::Rgba
4953    } else {
4954        computer_use::ScreenshotPixelFormat::Bgra
4955    };
4956    if host_mode {
4957        const RETAINED_OK: u8 = 0;
4958        const RETAINED_NONE: u8 = 1;
4959        // The host produced the buffer type this display's consumer cannot take; each
4960        // direction has its own recovery below.
4961        const RETAINED_CPU_FRAME: u8 = 2;
4962        const RETAINED_GPU_FRAME: u8 = 3;
4963        let host_idx = node.id;
4964        let gpu_encoder = node
4965            .capture
4966            .as_ref()
4967            .map(|c| c.zero_copy.is_some())
4968            .unwrap_or(false);
4969        // The session steps out of `state` while frames are adopted so the
4970        // renderer can composite the watermark / serve screenshot readbacks.
4971        let host = state.host.take().unwrap();
4972        if let Some(cap) = node.capture.as_mut() {
4973            release_host_inflight(Some(&host), cap, host_idx, false);
4974        }
4975        // A frame of another size than this capture's is not encoded (the CPU path would blit
4976        // old-pitch rows into a new-size buffer, and HW encoders cannot take a mismatched
4977        // dmabuf), but it is the host's newest picture, so it is retained: a host that keeps
4978        // its own mode brings the capture back to its size, and a quiet one may send nothing
4979        // else for that capture to show.
4980        let expect = node
4981            .capture
4982            .as_ref()
4983            .map(|c| (c.settings.width, c.settings.height));
4984        let new_frame = match (host.try_take_frame(host_idx), expect) {
4985            (Some(f), Some((w, h))) if f.width != w || f.height != h => {
4986                retain_host_frame(&host, node.capture.as_mut(), host_idx, f);
4987                None
4988            }
4989            (f, _) => f,
4990        };
4991        let have_new = new_frame.is_some();
4992        new_stamp = new_frame.as_ref().map(|f| f.stamp_ns);
4993        // Streaming mode wants a constant-rate stream (the client's decoder pipeline
4994        // is built for it), so re-encode the retained frame every tick like the
4995        // compositor path does. Outside streaming mode, stay damage-driven, waking
4996        // only for a pending IDR (a viewer opening its keyframe gate), a screenshot
4997        // request, or a bouncing watermark that must keep moving.
4998        let streaming = node
4999            .capture
5000            .as_ref()
5001            .map(|c| c.settings.video_streaming_mode)
5002            .unwrap_or(false);
5003        let wm_active = node.overlay_state.is_active();
5004        let wm_animated = wm_active && node.overlay_state.is_animated();
5005        // Without a fresh frame, retained content is published for a reason of its own: a
5006        // keyframe request, a screenshot, a moving watermark, or streaming mode's constant
5007        // rate once the host has gone quiet. A live host's next frame is imminent and
5008        // renders through its own wake, so the timer is held off rather than publishing a
5009        // duplicate that frame would then queue behind.
5010        let now = Instant::now();
5011        let period = node.capture.as_ref().map(capture_period);
5012        let host_quiet = node.capture.as_ref().is_none_or(|cap| {
5013            cap.pace
5014                .since_last_tick(now)
5015                .is_none_or(|since| since >= capture_period(cap).mul_f64(HOST_QUIET_PERIODS))
5016        });
5017        if !have_new && host_quiet {
5018            host.detach_retained(host_idx);
5019            if let Some(cap) = node.capture.as_mut() {
5020                detach_retained_gpu(
5021                    state.gles_renderer.as_mut(),
5022                    state.gbm_device.as_ref(),
5023                    &host,
5024                    cap,
5025                    host_idx,
5026                    (width, height),
5027                );
5028            }
5029        }
5030        if !have_new
5031            && !want_idr_for_host
5032            && !take_screenshot
5033            && !wm_animated
5034            && !(streaming && host_quiet)
5035        {
5036            cancel_slots(node, &mut pool_slot, &mut gpu_slot);
5037            if let Some(cap) = node.capture.as_mut()
5038                && let Some(period) = period
5039            {
5040                let held = if streaming {
5041                    cap.pace
5042                        .last_tick
5043                        .map_or(now, |last| last + period.mul_f64(HOST_QUIET_PERIODS))
5044                } else {
5045                    now + period
5046                };
5047                cap.pace.defer(held.max(now + Duration::from_millis(1)));
5048            }
5049            state.host = Some(host);
5050            return false;
5051        }
5052        if let Some(cap) = node.capture.as_mut()
5053            && let Some(period) = period
5054        {
5055            cap.pace.ticked(trigger, period, now, false);
5056            cap.change_pending = false;
5057        }
5058        if let Some(f) = new_frame {
5059            retain_host_frame(&host, node.capture.as_mut(), host_idx, f);
5060        }
5061        // Consume the (new or prior) retained frame's content into the pool slot /
5062        // GPU dmabuf. The frame stays retained for the next IDR. The CPU path
5063        // blends the watermark in place; the GPU path composites it below. Damage
5064        // is the fresh blit's; a re-encode of retained content has none of its own.
5065        let mut wm_drawn = false;
5066        let outcome = host.with_retained(host_idx, |r| {
5067            let Some(f) = r else { return RETAINED_NONE };
5068            // A frame of another size waits for a capture of its size, and a screenshot asked
5069            // for meanwhile stays queued for a frame of this one, or until its deadline.
5070            if (f.width, f.height) != (width, height) {
5071                return RETAINED_NONE;
5072            }
5073            damage_rects = if have_new {
5074                f.damage.clone()
5075            } else {
5076                Vec::new()
5077            };
5078            if let Some(cpu) = f.cpu.as_ref() {
5079                if gpu_encoder {
5080                    return RETAINED_CPU_FRAME;
5081                }
5082                if take_screenshot {
5083                    screenshot_result =
5084                        Some(validate_host_screenshot(cpu, width, height).map(|format| {
5085                            screenshot_format = format;
5086                            ShotPixels::Rendered
5087                        }));
5088                }
5089                if let Some((_, ref mut buf)) = pool_slot {
5090                    cpu.write_bgra(f.width, f.height, buf);
5091                    if wm_active {
5092                        node.overlay_state.blend_bgra(
5093                            buf,
5094                            (f.width as usize) * 4,
5095                            f.width,
5096                            f.height,
5097                        );
5098                        wm_drawn = true;
5099                    }
5100                }
5101                cpu.write_bgra(f.width, f.height, &mut node.frame_buffer);
5102                if wm_active {
5103                    node.overlay_state.blend_bgra(
5104                        &mut node.frame_buffer,
5105                        (f.width as usize) * 4,
5106                        f.width,
5107                        f.height,
5108                    );
5109                }
5110            } else if let Some(dmabuf) = f.dmabuf.as_ref() {
5111                if !gpu_encoder {
5112                    return RETAINED_GPU_FRAME;
5113                }
5114                source_dmabuf = Some(dmabuf.clone());
5115                host_enc_retained = true;
5116            }
5117            RETAINED_OK
5118        });
5119        // A fresh blit may still be in flight on the GPU when the compositor announces it.
5120        if have_new && let Some(src) = source_dmabuf.as_ref() {
5121            wayland::host::wait_gpu_done(src);
5122        }
5123        // GPU path: composite the watermark and serve screenshot readbacks with
5124        // the renderer. A bouncing watermark re-composes retained content into
5125        // this display's offscreen target every tick (drawing in place would
5126        // trail); anchored watermarks are stamped once onto each fresh blit and
5127        // ride along with retained re-encodes.
5128        if let Some(src) = source_dmabuf.clone() {
5129            if take_screenshot && matches!(src.format().code, Fourcc::Xrgb8888 | Fourcc::Xbgr8888) {
5130                screenshot_format = computer_use::ScreenshotPixelFormat::Rgbx;
5131            }
5132            if wm_active && let Some(renderer) = state.gles_renderer.as_mut() {
5133                if wm_animated {
5134                    let target = match gpu_slot.as_mut() {
5135                        Some((_, target)) => Some(target),
5136                        None => node.offscreen_buffer.as_mut().map(|(_, target)| target),
5137                    };
5138                    if let Some(target) = target {
5139                        match compose_host_frame(
5140                            renderer,
5141                            Some(&node.overlay_state),
5142                            &src,
5143                            target,
5144                            (width, height),
5145                        ) {
5146                            Ok(sync) => {
5147                                render_sync = Some(sync);
5148                                source_dmabuf = Some(target.clone());
5149                                host_enc_retained = false;
5150                                wm_drawn = true;
5151                            }
5152                            Err(e) => warn_once_host_watermark(&e),
5153                        }
5154                    }
5155                } else if have_new {
5156                    let mut target = src.clone();
5157                    match draw_host_watermark(
5158                        renderer,
5159                        &node.overlay_state,
5160                        &mut target,
5161                        (width, height),
5162                    ) {
5163                        Ok(sync) => {
5164                            render_sync = Some(sync);
5165                            wm_drawn = true;
5166                        }
5167                        Err(e) => warn_once_host_watermark(&e),
5168                    }
5169                }
5170            }
5171            if take_screenshot && let Some(renderer) = state.gles_renderer.as_mut() {
5172                let mut shot = source_dmabuf.clone().unwrap_or(src);
5173                screenshot_result = Some((|| {
5174                    let fb = renderer
5175                        .bind(&mut shot)
5176                        .map_err(|e| format!("Screenshot bind failed: {e:?}"))?;
5177                    let rect = Rectangle::new((0, 0).into(), (width, height).into());
5178                    let mapping = renderer
5179                        .copy_framebuffer(&fb, rect, Fourcc::Abgr8888)
5180                        .map_err(|e| format!("Screenshot copy failed: {e:?}"))?;
5181                    let data = renderer
5182                        .map_texture(&mapping)
5183                        .map_err(|e| format!("Screenshot map failed: {e:?}"))?;
5184                    if Some(data.len()) != screenshot_len(width, height) {
5185                        return Err("Screenshot readback produced incomplete pixels".to_string());
5186                    }
5187                    Ok(ShotPixels::Owned(computer_use::ScreenshotBuffer::copy_of(
5188                        data,
5189                    )))
5190                })());
5191            }
5192        }
5193        if wm_drawn && let Some(rect) = node.overlay_state.damage_rect(width, height) {
5194            damage_rects.push(rect);
5195        }
5196        state.host = Some(host);
5197        if outcome != RETAINED_OK {
5198            cancel_slots(node, &mut pool_slot, &mut gpu_slot);
5199            // A frame type this display's consumer cannot take is recovered from rather
5200            // than warned about: either side of the mismatch would otherwise stream
5201            // nothing for the life of the capture.
5202            if outcome == RETAINED_GPU_FRAME {
5203                // The host is blitting into dmabufs a CPU encoder cannot read; ask it
5204                // for shm frames. Frames resume once the capture thread reallocates,
5205                // and the request is idempotent, so repeating it per tick costs nothing.
5206                if let Some(h) = state.host.as_ref() {
5207                    h.set_buffer_type(node.id, false);
5208                }
5209            } else if outcome == RETAINED_CPU_FRAME
5210                && let Some(cap) = node.capture.as_mut()
5211            {
5212                // The host hands out software frames only (no zwp_linux_dmabuf v3),
5213                // so the zero-copy session has nothing to import: demote it to the
5214                // readback path, which encodes those frames as they arrive.
5215                eprintln!(
5216                    "[HostCapture] host delivers software frames; demoting the zero-copy encoder to readback encode."
5217                );
5218                let _report = report::enter(&cap.report);
5219                report::capture("readback", false);
5220                report::zero_copy_available(false);
5221                report::capture_reason("the host compositor delivers software frames only");
5222                end_zero_copy(cap);
5223                let predecessor = cap.encode_join.take();
5224                release_host_inflight(state.host.as_ref(), cap, node.id, true);
5225                let s = &cap.settings;
5226                let try_gpu = s.codec.is_video() && !(s.use_cpu || s.encode_node_index == -1);
5227                bootstrap_readback_pool(cap, node.id, false, try_gpu, None, predecessor);
5228                cap.request_idr();
5229                // The consumer is now a CPU one, so the host stops preferring GPU slots.
5230                if let Some(h) = state.host.as_ref() {
5231                    h.set_buffer_type(node.id, false);
5232                }
5233            }
5234            return false;
5235        }
5236        render_success = true;
5237    }
5238
5239    if !host_mode && let Some(cap) = node.capture.as_mut() {
5240        release_scanout_inflight(cap, false);
5241    }
5242    // Whether a covering client's buffer may stand in for the composite this tick: a
5243    // screenshot and a copy-capture client read a composited target, a held frame is
5244    // composited to be dropped, and a session that refused one client buffer composites for
5245    // good. A zero-copy session takes a dmabuf and the pixman path an shm buffer; the readback
5246    // path on the GL renderer composites, since its pool is RGBA and a client's shm buffer is
5247    // not.
5248    let direct_wanted: Result<(), &'static str> = if take_screenshot {
5249        Err("a screenshot reads the composite")
5250    } else if copy_frame_wanted {
5251        Err("a copy-capture client reads the composite")
5252    } else if hold_frame {
5253        Err("the frames are held")
5254    } else if node
5255        .capture
5256        .as_ref()
5257        .is_some_and(|c| c.encode_controls.scanout_refused.load(Ordering::Relaxed))
5258    {
5259        Err("the encoder refused a client's buffer")
5260    } else {
5261        Ok(())
5262    };
5263    let scanout_planes = node.capture.as_ref().map_or(1, |c| {
5264        c.encode_controls.scanout_planes.load(Ordering::Relaxed) as usize
5265    });
5266    // The hold on a client buffer encoded as it stands this tick, kept until the encode is
5267    // done with it: a dmabuf a zero-copy session imports, or an shm buffer whose rows the
5268    // readback encode reads in place.
5269    let mut scanout_hold: Option<SurfaceBuffer> = None;
5270    let mut shm_hold: Option<SurfaceBuffer> = None;
5271    let cursor_time = Duration::from_millis(u64::from(state.clock.now().as_millis()));
5272    let drag_icon = state
5273        .dnd_icon
5274        .as_ref()
5275        .zip(pointer_local)
5276        .map(|((surface, offset), pointer)| (surface, pointer + offset.to_f64()));
5277    let software_cursor = |pos| SoftwareCursor {
5278        theme: &state.cursor_helper,
5279        icon: &state.current_cursor_icon,
5280        pos,
5281        time: cursor_time,
5282    };
5283
5284    if !host_mode && state.use_gpu {
5285        if let Some(renderer) = state.gles_renderer.as_mut() {
5286            let mut cap = node.capture.as_mut();
5287            // A zero-copy target's age counts the renders since it last took one.
5288            let slot_id = gpu_slot.as_ref().map(|(id, _)| *id);
5289            let slot_age = slot_id.map(|id| {
5290                cap.as_ref().map_or(0, |c| {
5291                    if c.pool_last_render[id] == 0 {
5292                        0
5293                    } else {
5294                        (c.render_seq + 1 - c.pool_last_render[id]) as usize
5295                    }
5296                })
5297            });
5298            let scene = FrameScene {
5299                space: &state.space,
5300                wm: state.wm.as_ref(),
5301                cursor: pointer_local
5302                    .filter(|_| state.render_cursor_on_framebuffer)
5303                    .map(software_cursor),
5304                drag_icon,
5305                overlay: &node.overlay_state,
5306                output: &output,
5307                origin,
5308                size: (width, height),
5309                scale: output_scale_val,
5310            };
5311            let elements = composition_elements(renderer, &scene);
5312            // Only a zero-copy session imports a client's buffer; the readback path composites.
5313            let direct = match (direct_wanted, slot_id) {
5314                (Ok(()), Some(_)) => {
5315                    scanout_candidate(&elements, (width, height), output_scale_val)
5316                        .and_then(|s| scanout_dmabuf(s, (width, height), scanout_planes))
5317                }
5318                (Err(why), _) => Err(why),
5319                (Ok(()), None) => Err("the readback path composites"),
5320            };
5321            if let Some(c) = cap.as_deref_mut() {
5322                let said = direct
5323                    .as_ref()
5324                    .map(|(_, _, what)| what.clone())
5325                    .map_err(|why| *why);
5326                note_direct(c, node.id, &said);
5327            }
5328            match direct {
5329                Ok((client, hold, _)) => {
5330                    let full = Rectangle::<i32, Physical>::from_size((width, height).into());
5331                    // The client's own damage is the frame's, read through the same tracker
5332                    // the composite reads it from; the output's tracker records the frame as
5333                    // well, so a slot composited later is repainted for everything since the
5334                    // frame it last took.
5335                    damage_rects = node
5336                        .content_tracker
5337                        .get_or_insert_with(|| {
5338                            OutputDamageTracker::from_mode_source(
5339                                node.damage_tracker.mode().clone(),
5340                            )
5341                        })
5342                        .damage_output(1, &elements)
5343                        .map_or_else(
5344                            |_| vec![full],
5345                            |(damage, _)| damage.cloned().unwrap_or_default(),
5346                        );
5347                    let _ = node.damage_tracker.damage_output(1, &elements);
5348                    render_success = true;
5349                    source_dmabuf = Some(client);
5350                    scanout_hold = Some(hold);
5351                    if let (Some(id), Some(c)) = (slot_id, cap.as_deref_mut()) {
5352                        c.render_seq += 1;
5353                        // The slot took no render, so its next one draws it whole.
5354                        c.pool_last_render[id] = 0;
5355                    }
5356                }
5357                Err(_) => {
5358                    let target = match gpu_slot.as_mut() {
5359                        Some((_, dmabuf)) => Some(dmabuf),
5360                        // A screen resized ahead of the capture restart that follows has a
5361                        // target for the frame the restart will ask for, not for this one.
5362                        None => node
5363                            .offscreen_buffer
5364                            .as_mut()
5365                            .filter(|(bo, _)| {
5366                                (bo.width() as i32, bo.height() as i32) == (width, height)
5367                            })
5368                            .map(|(_, dmabuf)| dmabuf),
5369                    };
5370                    if let Some(dmabuf) = target {
5371                        let render_age = if node.overlay_state.is_animated() || needs_full {
5372                            0
5373                        } else {
5374                            slot_age.unwrap_or(1)
5375                        };
5376                        match renderer.bind(dmabuf) {
5377                            Ok(mut frame) => {
5378                                // What changed since the last frame, for the encoder,
5379                                // whatever the target's age.
5380                                let changed = slot_id
5381                                    .and_then(|_| {
5382                                        node.content_tracker
5383                                            .get_or_insert_with(|| {
5384                                                OutputDamageTracker::from_mode_source(
5385                                                    node.damage_tracker.mode().clone(),
5386                                                )
5387                                            })
5388                                            .damage_output(1, &elements)
5389                                            .ok()
5390                                    })
5391                                    .map(|(damage, _)| damage.cloned().unwrap_or_default());
5392                                match node.damage_tracker.render_output(
5393                                    renderer,
5394                                    &mut frame,
5395                                    render_age,
5396                                    &elements,
5397                                    [0.1, 0.1, 0.1, 1.0],
5398                                ) {
5399                                    Ok(result) => {
5400                                        render_success = true;
5401                                        if let Some(damage) = result.damage {
5402                                            damage_rects = damage.clone();
5403                                            node.undrawn_ticks = 0;
5404                                        } else {
5405                                            node.undrawn_ticks += 1;
5406                                            if node.undrawn_ticks >= UNDRAWN_TICKS_PER_FRAME {
5407                                                node.undrawn_ticks = 0;
5408                                                let _ = renderer
5409                                                    .render(
5410                                                        &mut frame,
5411                                                        (width, height).into(),
5412                                                        Transform::Normal,
5413                                                    )
5414                                                    .and_then(|f| f.finish());
5415                                            }
5416                                        }
5417                                        if wait_render_fence(&result.sync, node.id) {
5418                                            if let Some(c) = cap.as_deref_mut() {
5419                                                c.needs_full_render = false;
5420                                            }
5421                                        } else {
5422                                            // The tracker counts the unfinished frame as
5423                                            // drawn, so the target is published to nobody
5424                                            // and redrawn whole next tick.
5425                                            render_success = false;
5426                                            damage_rects.clear();
5427                                            if let Some(c) = cap.as_deref_mut() {
5428                                                c.needs_full_render = true;
5429                                            }
5430                                        }
5431                                        if render_success
5432                                            && let Some(changed) =
5433                                                changed.filter(|_| render_age > 1)
5434                                        {
5435                                            damage_rects = changed;
5436                                        }
5437                                    }
5438                                    Err(e) => eprintln!("Render error: {:?}", e),
5439                                }
5440                                if let (Some(id), Some(c)) = (slot_id, cap.as_deref_mut()) {
5441                                    c.render_seq += 1;
5442                                    c.pool_last_render[id] =
5443                                        if render_success { c.render_seq } else { 0 };
5444                                }
5445                                if let Some(c) = cap {
5446                                    if !damage_rects.is_empty() {
5447                                        c.content_gen += 1;
5448                                    }
5449                                    if let Some((id, ref mut buf)) = pool_slot {
5450                                        // No-damage ticks skip the readback, so a pooled
5451                                        // buffer can lag the offscreen target whenever the
5452                                        // encoder held the other slot across a tick; one
5453                                        // catch-up readback keeps every published buffer
5454                                        // current.
5455                                        if render_success && take_screenshot {
5456                                            screenshot_result = read_back_rgba_async(
5457                                                renderer,
5458                                                &mut state.screenshot_readbacks,
5459                                                &mut frame,
5460                                                width,
5461                                                height,
5462                                            )
5463                                            .map(|readback| readback.map(ShotPixels::Gpu));
5464                                        }
5465                                        if render_success
5466                                            && take_screenshot
5467                                            && screenshot_result.is_none()
5468                                        {
5469                                            let result = read_back_rgba_result(
5470                                                renderer, &mut frame, width, height, buf, true,
5471                                            );
5472                                            if result.is_ok() {
5473                                                c.pool_content_gen[id] = c.content_gen;
5474                                            } else {
5475                                                render_success = false;
5476                                                c.needs_full_render = true;
5477                                            }
5478                                            screenshot_result =
5479                                                Some(result.map(|()| ShotPixels::Rendered));
5480                                        } else if render_success
5481                                            && c.pool_content_gen[id] != c.content_gen
5482                                        {
5483                                            read_back_rgba(
5484                                                renderer, &mut frame, width, height, buf,
5485                                            );
5486                                            c.pool_content_gen[id] = c.content_gen;
5487                                        }
5488                                    }
5489                                }
5490                                if render_success && pool_slot.is_none() && take_screenshot {
5491                                    screenshot_result = read_back_rgba_async(
5492                                        renderer,
5493                                        &mut state.screenshot_readbacks,
5494                                        &mut frame,
5495                                        width,
5496                                        height,
5497                                    )
5498                                    .map(|readback| readback.map(ShotPixels::Gpu));
5499                                }
5500                                if render_success
5501                                    && pool_slot.is_none()
5502                                    && take_screenshot
5503                                    && screenshot_result.is_none()
5504                                {
5505                                    let mut pixels = computer_use::ScreenshotBuffer::with_len(
5506                                        screenshot_len(width, height).unwrap_or(0),
5507                                    );
5508                                    screenshot_result = Some(
5509                                        read_back_rgba_result(
5510                                            renderer,
5511                                            &mut frame,
5512                                            width,
5513                                            height,
5514                                            &mut pixels,
5515                                            true,
5516                                        )
5517                                        .map(|()| ShotPixels::Owned(pixels)),
5518                                    );
5519                                }
5520                            }
5521                            Err(e) => eprintln!("Failed to bind buffer: {:?}", e),
5522                        }
5523                    }
5524                }
5525            }
5526        }
5527    } else if !host_mode && let Some(renderer) = state.pixman_renderer.as_mut() {
5528        let mut cap = node.capture.as_mut();
5529        let buf_age = match pool_slot {
5530            Some((id, _)) => cap
5531                .as_ref()
5532                .map(|c| {
5533                    if c.pool_last_render[id] == 0 {
5534                        0
5535                    } else {
5536                        (c.render_seq + 1 - c.pool_last_render[id]) as usize
5537                    }
5538                })
5539                .unwrap_or(0),
5540            None => 0,
5541        };
5542        let scene = FrameScene {
5543            space: &state.space,
5544            wm: state.wm.as_ref(),
5545            cursor: pointer_local
5546                .filter(|_| state.render_cursor_on_framebuffer)
5547                .map(software_cursor),
5548            drag_icon,
5549            overlay: &node.overlay_state,
5550            output: &output,
5551            origin,
5552            size: (width, height),
5553            scale: output_scale_val,
5554        };
5555        let elements = composition_elements(renderer, &scene);
5556        let render_age = if node.overlay_state.is_animated() || needs_full {
5557            0
5558        } else {
5559            buf_age
5560        };
5561        let changed = node
5562            .content_tracker
5563            .get_or_insert_with(|| {
5564                OutputDamageTracker::from_mode_source(node.damage_tracker.mode().clone())
5565            })
5566            .damage_output(1, &elements)
5567            .ok()
5568            .map(|(damage, _)| damage.cloned().unwrap_or_default());
5569        // A client's shm buffer stands in for the composite only where a pool buffer would
5570        // take it, since the encode thread reads the rows the frame names.
5571        let direct = match (direct_wanted, pool_slot.as_ref()) {
5572            (Ok(()), Some(_)) => scanout_candidate(&elements, (width, height), output_scale_val)
5573                .and_then(|s| scanout_shm(s, (width, height))),
5574            (Err(why), _) => Err(why),
5575            (Ok(()), None) => Err("no pool buffer takes the frame"),
5576        };
5577        if let Some(c) = cap.as_deref_mut() {
5578            let said = direct
5579                .as_ref()
5580                .map(|(_, what)| what.clone())
5581                .map_err(|why| *why);
5582            note_direct(c, node.id, &said);
5583        }
5584        match direct {
5585            Ok((client, _)) => {
5586                let full = Rectangle::<i32, Physical>::from_size((width, height).into());
5587                // The client's own damage is the frame's; the output's tracker records the
5588                // frame too, so a slot composited later is repainted for everything since
5589                // the frame it last took.
5590                damage_rects = changed.unwrap_or_else(|| vec![full]);
5591                let _ = node.damage_tracker.damage_output(1, &elements);
5592                render_success = true;
5593                shm_hold = Some(client);
5594                if let Some(c) = cap.as_deref_mut() {
5595                    c.render_seq += 1;
5596                    if let Some((id, _)) = pool_slot {
5597                        // The slot took no render, so its next one draws it whole.
5598                        c.pool_last_render[id] = 0;
5599                    }
5600                }
5601            }
5602            Err(_) => {
5603                let ptr = match pool_slot {
5604                    Some((_, ref mut buf)) => buf.as_mut_ptr() as *mut u32,
5605                    None => node.frame_buffer.as_mut_ptr() as *mut u32,
5606                };
5607                let mut image = unsafe {
5608                    pixman::Image::from_raw_mut(
5609                        pixman::FormatCode::A8R8G8B8,
5610                        width as usize,
5611                        height as usize,
5612                        ptr,
5613                        (width as usize) * 4,
5614                        false,
5615                    )
5616                    .expect("Failed to create pixman image")
5617                };
5618                match renderer.bind(&mut image) {
5619                    Ok(mut frame) => {
5620                        match node.damage_tracker.render_output(
5621                            renderer,
5622                            &mut frame,
5623                            render_age,
5624                            &elements,
5625                            [0.1, 0.1, 0.1, 1.0],
5626                        ) {
5627                            Ok(result) => {
5628                                render_success = true;
5629                                if let Some(c) = cap.as_deref_mut() {
5630                                    c.needs_full_render = false;
5631                                }
5632                                if let Some(damage) = result.damage {
5633                                    damage_rects = damage.clone();
5634                                }
5635                                if let Some(changed) = changed.filter(|_| render_age > 1) {
5636                                    damage_rects = changed;
5637                                }
5638                            }
5639                            Err(e) => eprintln!("Render error: {:?}", e),
5640                        }
5641                        if let Some(c) = cap {
5642                            c.render_seq += 1;
5643                            if render_success && let Some((id, _)) = pool_slot {
5644                                c.pool_last_render[id] = c.render_seq;
5645                            }
5646                        }
5647                    }
5648                    Err(e) => eprintln!("Failed to bind pixman image: {:?}", e),
5649                }
5650            }
5651        }
5652    }
5653
5654    if take_screenshot && render_success {
5655        if !host_mode && !state.use_gpu {
5656            screenshot_result = Some(Ok(ShotPixels::Rendered));
5657        }
5658        if !host_mode
5659            && matches!(&screenshot_result, Some(Ok(ShotPixels::Rendered)))
5660            && let Some((_, buf)) = pool_slot.as_ref()
5661        {
5662            screenshot_result = Some(screenshot_pixels(buf, width, height).map(ShotPixels::Owned));
5663        }
5664    }
5665    if render_success {
5666        // A zero-copy target took the frame, so the output's own one missed it.
5667        node.target_seeded = gpu_slot.is_none();
5668    }
5669    // A captured display answers its clients' frame callbacks on its window boundaries
5670    // (`answer_frame_callbacks`), or at the render a boundary waited for; host capture and an
5671    // uncaptured output as they render.
5672    let answer_now = node.capture.as_ref().is_none_or(|c| c.callbacks_owed) || host_mode;
5673    if render_success && drives_screen {
5674        let time = state.clock.now();
5675        // The composited frame is what the capture consumes, so this render is also the
5676        // presentation moment for wp_presentation feedback.
5677        let mut feedback = OutputPresentationFeedback::new(&output);
5678        for window in state.space.elements_for_output(&output) {
5679            if answer_now {
5680                window.send_frame(&output, time, Some(Duration::ZERO), |_, _| {
5681                    Some(output.clone())
5682                });
5683            }
5684            window.take_presentation_feedback(
5685                &mut feedback,
5686                |_, _| Some(output.clone()),
5687                |_, _| wp_presentation_feedback::Kind::empty(),
5688            );
5689        }
5690        // Panels, backgrounds, and other layer-shell surfaces are composited from the
5691        // layer map rather than the space, so they need the callback separately: one
5692        // that never arrives leaves a client which draws on frame callbacks showing
5693        // whatever it painted first, for as long as the session lasts.
5694        for layer in layer_map_for_output(&output).layers() {
5695            if answer_now {
5696                layer.send_frame(&output, time, Some(Duration::ZERO), |_, _| {
5697                    Some(output.clone())
5698                });
5699            }
5700            layer.take_presentation_feedback(
5701                &mut feedback,
5702                |_, _| Some(output.clone()),
5703                |_, _| wp_presentation_feedback::Kind::empty(),
5704            );
5705        }
5706        if answer_now && let Some(cap) = node.capture.as_mut() {
5707            cap.callbacks_owed = false;
5708        }
5709        send_cursor_frame(state, &output, time);
5710        let refresh = match node.capture.as_ref() {
5711            Some(c) => Refresh::Fixed(Duration::from_secs_f64(
5712                1.0 / c.settings.target_fps.max(1.0),
5713            )),
5714            None => Refresh::Unknown,
5715        };
5716        node.frame_seq += 1;
5717        feedback.presented(
5718            time,
5719            refresh,
5720            node.frame_seq,
5721            wp_presentation_feedback::Kind::Vsync,
5722        );
5723    }
5724    // Host mode composites nothing locally, so capture clients' frames stay parked.
5725    if render_success && !host_mode && copy_frame_wanted {
5726        service_copy_frames(
5727            state,
5728            node,
5729            gpu_slot.as_ref().map(|(_, dmabuf)| dmabuf),
5730            width,
5731            height,
5732            &damage_rects,
5733        );
5734    }
5735    if render_success
5736        && !hold_frame
5737        && let Some(cap) = node.capture.as_mut()
5738    {
5739        if let Some(pool) = cap.encode_pool.as_ref() {
5740            if let Some((id, buf)) = pool_slot.take() {
5741                cap.published += 1;
5742                if let Some(hold) = shm_hold.as_ref() {
5743                    cap.scanout_inflight.push((cap.published, hold.clone()));
5744                }
5745                pool.publish(WlFrame {
5746                    id,
5747                    buf,
5748                    source: None,
5749                    scanout: false,
5750                    shm: shm_hold.take(),
5751                    frame_id: cap.frame_counter,
5752                    damage: std::mem::take(&mut damage_rects),
5753                    is_animated: node.overlay_state.is_animated(),
5754                    captured_ns: new_stamp.unwrap_or(composite_ns),
5755                });
5756                cap.frame_counter = cap.frame_counter.wrapping_add(1);
5757            }
5758        } else if let Some(pool) = cap.gpu_pool.clone()
5759            && let Some((id, dmabuf)) = gpu_slot.take()
5760        {
5761            // A host watermark composited on the GPU is finished before the encode reads it.
5762            if render_sync
5763                .take()
5764                .is_some_and(|sync| !wait_render_fence(&sync, node.id))
5765            {
5766                pool.cancel(id, dmabuf);
5767            } else {
5768                cap.published += 1;
5769                if host_enc_retained {
5770                    cap.host_retained_use = cap.published;
5771                }
5772                let scanout = scanout_hold.is_some();
5773                if let Some(hold) = scanout_hold.take() {
5774                    cap.scanout_inflight.push((cap.published, hold));
5775                }
5776                pool.publish(WlFrame {
5777                    id,
5778                    buf: dmabuf,
5779                    source: source_dmabuf.take(),
5780                    scanout,
5781                    shm: None,
5782                    frame_id: cap.frame_counter,
5783                    damage: std::mem::take(&mut damage_rects),
5784                    is_animated: node.overlay_state.is_animated(),
5785                    captured_ns: new_stamp.unwrap_or(composite_ns),
5786                });
5787                cap.frame_counter = cap.frame_counter.wrapping_add(1);
5788            }
5789        }
5790    }
5791    cancel_slots(node, &mut pool_slot, &mut gpu_slot);
5792    if take_screenshot
5793        && let Some(request) = state.pending_screenshots.take(node.id, Instant::now())
5794    {
5795        let result = screenshot_result
5796            .unwrap_or_else(|| Err("Screenshot render produced no pixels".to_string()))
5797            .and_then(|pixels| {
5798                if !render_success {
5799                    return Err("Screenshot render failed".to_string());
5800                }
5801                let pixels = match pixels {
5802                    ShotPixels::Owned(pixels) => computer_use::ScreenshotPixels::Ready(pixels),
5803                    ShotPixels::Rendered => computer_use::ScreenshotPixels::Ready(
5804                        screenshot_pixels(&node.frame_buffer, width, height)?,
5805                    ),
5806                    ShotPixels::Gpu(readback) => computer_use::ScreenshotPixels::Gpu(readback),
5807                };
5808                Ok(computer_use::ScreenshotFrame {
5809                    pixels,
5810                    width: width as u32,
5811                    height: height as u32,
5812                    format: screenshot_format,
5813                })
5814            });
5815        let result = if request.canceled.load(Ordering::Acquire) {
5816            Err("Screenshot canceled".to_string())
5817        } else if Instant::now() >= request.deadline {
5818            Err("Screenshot timed out".to_string())
5819        } else {
5820            result
5821        };
5822        let _ = request.resp.send(result);
5823    }
5824    false
5825}
5826
5827/// True when the rectangles `(x, y, w, h)` overlap: strict interior intersection, so
5828/// touching edges do not count and empty (non-positive-dimension) rectangles never
5829/// overlap anything. Arithmetic is widened so extreme coordinates cannot wrap.
5830fn rects_overlap(a: (i32, i32, i32, i32), b: (i32, i32, i32, i32)) -> bool {
5831    let (ax, ay, aw, ah) = (
5832        i64::from(a.0),
5833        i64::from(a.1),
5834        i64::from(a.2),
5835        i64::from(a.3),
5836    );
5837    let (bx, by, bw, bh) = (
5838        i64::from(b.0),
5839        i64::from(b.1),
5840        i64::from(b.2),
5841        i64::from(b.3),
5842    );
5843    aw > 0
5844        && ah > 0
5845        && bw > 0
5846        && bh > 0
5847        && ax < bx + bw
5848        && bx < ax + aw
5849        && ay < by + bh
5850        && by < ay + ah
5851}
5852
5853/// An overlapping output as `(id, flavor, rect)`, where flavor names which rectangle
5854/// pair matched and rect is `(x, y, width, height)`.
5855type OutputOverlap = (u32, &'static str, (i32, i32, i32, i32));
5856
5857/// The first live screen (excluding `skip_id`; views stand behind their owner's
5858/// rectangle) whose rectangle overlaps a candidate placement. Both rectangle flavors are checked — logical
5859/// (Space layout, scale-divided) and physical (mode pixels at the same origin) — because
5860/// input injection and cursor compositing key off the physical rects while window layout
5861/// keys off the logical ones, and neither may overlap.
5862fn find_output_overlap(
5863    nodes: &[wayland::frontend::OutputNode],
5864    skip_id: Option<u32>,
5865    logical: (i32, i32, i32, i32),
5866    physical: (i32, i32, i32, i32),
5867) -> Option<OutputOverlap> {
5868    for n in nodes {
5869        if Some(n.id) == skip_id {
5870            continue;
5871        }
5872        if n.owner.is_some() {
5873            continue;
5874        }
5875        if let Some(geo) = n.logical_geometry() {
5876            let other = (geo.loc.x, geo.loc.y, geo.size.w, geo.size.h);
5877            if rects_overlap(logical, other) {
5878                return Some((n.id, "logical", other));
5879            }
5880        }
5881        if let Some(mode) = n.output.current_mode() {
5882            let other = (n.pos.0, n.pos.1, mode.size.w, mode.size.h);
5883            if rects_overlap(physical, other) {
5884                return Some((n.id, "physical", other));
5885            }
5886        }
5887    }
5888    None
5889}
5890
5891/// Create an additional output mapped into the layout at `(x, y)`. Fails (false) on a
5892/// duplicate id, non-positive geometry/scale, a rectangle overlapping a live output, or a
5893/// GPU render-target allocation failure. Only Create/Reposition placements are validated:
5894/// a capture reconfigure (StartCapture on an existing output) resizes UNVALIDATED, so
5895/// keeping a multi-step relayout overlap-free at every step is the caller's ordering
5896/// responsibility.
5897fn create_output_on(
5898    state: &mut AppState,
5899    id: u32,
5900    width: i32,
5901    height: i32,
5902    x: i32,
5903    y: i32,
5904    scale: f64,
5905) -> bool {
5906    if state.node_idx_for_id(id).is_some() || width <= 0 || height <= 0 || scale <= 0.0 {
5907        return false;
5908    }
5909    // Host-capture mode: displays map onto host outputs by rank, so an output beyond
5910    // the host's count would exist but never receive a frame. Refuse it instead.
5911    if let Some(host) = state.host.as_ref() {
5912        let capacity = host.output_count();
5913        if state.output_nodes.len() >= capacity {
5914            eprintln!(
5915                "[Wayland] CreateOutput {id}: rejected, the host compositor has {capacity} output(s) and all are backing displays."
5916            );
5917            return false;
5918        }
5919    }
5920    let logical_size = (
5921        (f64::from(width) / scale).round() as i32,
5922        (f64::from(height) / scale).round() as i32,
5923    );
5924    if let Some((oid, flavor, other)) = find_output_overlap(
5925        &state.output_nodes,
5926        None,
5927        (x, y, logical_size.0, logical_size.1),
5928        (x, y, width, height),
5929    ) {
5930        eprintln!(
5931            "[Wayland] CreateOutput {id}: rejected, {flavor} rect {}x{}+{x}+{y} overlaps output {oid} at {}x{}+{}+{}.",
5932            if flavor == "logical" {
5933                logical_size.0
5934            } else {
5935                width
5936            },
5937            if flavor == "logical" {
5938                logical_size.1
5939            } else {
5940                height
5941            },
5942            other.2,
5943            other.3,
5944            other.0,
5945            other.1,
5946        );
5947        return false;
5948    }
5949    let output = Output::new(
5950        format!("HEADLESS-{}", id + 1),
5951        PhysicalProperties {
5952            size: (width, height).into(),
5953            subpixel: Subpixel::Unknown,
5954            make: "Pixelflux".into(),
5955            model: "Virtual".into(),
5956            serial_number: format!("{:03}", id + 1),
5957        },
5958    );
5959    let mode = OutputMode {
5960        size: (width, height).into(),
5961        refresh: 60_000,
5962    };
5963    output.change_current_state(
5964        Some(mode),
5965        Some(Transform::Normal),
5966        Some(OutputScale::Fractional(scale)),
5967        Some((x, y).into()),
5968    );
5969    output.set_preferred(mode);
5970    let mut offscreen = None;
5971    if state.use_gpu {
5972        let Some(gbm) = state.gbm_device.as_mut() else {
5973            return false;
5974        };
5975        match alloc_render_target(gbm, width as u32, height as u32, GbmFormat::Argb8888) {
5976            Ok(bo) => {
5977                let dmabuf = create_dmabuf_from_bo(&bo);
5978                offscreen = Some((bo, dmabuf));
5979            }
5980            Err(e) => {
5981                eprintln!(
5982                    "[Wayland] CreateOutput {id}: GBM allocation {width}x{height} failed ({e:?})."
5983                );
5984                return false;
5985            }
5986        }
5987    }
5988    state.space.map_output(&output, (x, y));
5989    let global = output.create_global::<AppState>(&state.dh);
5990    let damage_tracker = OutputDamageTracker::from_output(&output);
5991    if let Some(host) = state.host.as_ref() {
5992        host.set_layout(id, x, y);
5993    }
5994    println!("[Wayland] Output {id} created: {width}x{height} @ ({x}, {y}) scale {scale:.2}.");
5995    state.output_nodes.push(wayland::frontend::OutputNode {
5996        id,
5997        output,
5998        global: Some(global),
5999        owner: None,
6000        view_size: (width, height),
6001        view_scale: 0.0,
6002        pos: (x, y),
6003        damage_tracker,
6004        content_tracker: None,
6005        frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
6006        offscreen_buffer: offscreen,
6007        undrawn_ticks: 0,
6008        overlay_state: OverlayState::default(),
6009        capture: None,
6010        frame_seq: 0,
6011        target_seeded: false,
6012        content_hold_until: None,
6013    });
6014    // A nested session opens one host toplevel per screen and the extras wait
6015    // parked until a display exists for them: hand the newest waiting window to
6016    // the new output. Windows stacked on an output rather than parked — anything
6017    // placed before this compositor started parking them — remain candidates.
6018    let mut counts: Vec<(u32, usize)> = Vec::new();
6019    for w in state.space.elements() {
6020        let oid = wayland::frontend::window_output_id(w);
6021        match counts.iter_mut().find(|(o, _)| *o == oid) {
6022            Some((_, c)) => *c += 1,
6023            None => counts.push((oid, 1)),
6024        }
6025    }
6026    let newest = |pred: &dyn Fn(&smithay::desktop::Window) -> bool| {
6027        state
6028            .space
6029            .elements()
6030            .filter(|w| pred(w))
6031            .max_by_key(|w| wayland::frontend::window_meta(w).map(|m| m.id).unwrap_or(0))
6032            .cloned()
6033    };
6034    let parked = |w: &smithay::desktop::Window| {
6035        wayland::frontend::window_meta(w)
6036            .map(|m| m.parked.load(std::sync::atomic::Ordering::Relaxed))
6037            .unwrap_or(false)
6038    };
6039    // A recreated output takes back the window it held before -- parked windows
6040    // carry the id of the output they came from, and the tag outlives it -- so
6041    // windows never swap displays however a bulk relayout orders its creates.
6042    let adopt = newest(&|w| parked(w) && wayland::frontend::window_output_id(w) == id)
6043        .or_else(|| newest(&parked))
6044        .or_else(|| {
6045            newest(&|w| {
6046                let oid = wayland::frontend::window_output_id(w);
6047                counts.iter().any(|(o, c)| *o == oid && *c >= 2)
6048            })
6049        });
6050    if let Some(window) = adopt
6051        && !wayland::wm::active(state)
6052    {
6053        state.place_window_on_output(&window, id);
6054        crate::log::debug!(
6055            "[Wayland] Output {id}: adopted waiting window {}.",
6056            wayland::frontend::window_meta(&window)
6057                .map(|m| m.id)
6058                .unwrap_or(0)
6059        );
6060    }
6061    wayland::workspace::outputs_changed(state);
6062    wayland::wm::outputs_changed(state);
6063    wayland::xwm::publish_workarea(state);
6064    true
6065}
6066
6067/// Move an existing output (the primary included) to layout offset `(x, y)`. The output's
6068/// advertised position, its Space mapping, and the windows placed on it (mapped at the
6069/// output's origin — window positions are output-relative under forced fullscreen) all
6070/// follow, so absolute input injection and cursor compositing — both keyed off `node.pos` —
6071/// resolve against the new layout immediately. A destination overlapping another live
6072/// output is refused (false). As with `CreateOutput`, only the placement itself is
6073/// validated: a capture reconfigure (StartCapture on an existing output) resizes
6074/// UNVALIDATED, so keeping a multi-step relayout overlap-free at every step is the
6075/// caller's ordering responsibility.
6076fn reposition_output_on(state: &mut AppState, id: u32, x: i32, y: i32) -> bool {
6077    let Some(idx) = state.node_idx_for_id(id) else {
6078        return false;
6079    };
6080    let output = state.output_nodes[idx].output.clone();
6081    if state.output_nodes[idx].pos == (x, y) {
6082        return true;
6083    }
6084    // A view moves within the screen it is cut from: no output to remap and no
6085    // windows to carry, since the screen and everything on it stay where they are.
6086    if let Some(owner) = state.output_nodes[idx].owner {
6087        let base = state
6088            .node_idx_for_id(owner)
6089            .map(|o| state.output_nodes[o].pos)
6090            .unwrap_or((0, 0));
6091        let mode = output
6092            .current_mode()
6093            .map(|m| (m.size.w, m.size.h))
6094            .unwrap_or((0, 0));
6095        let size = state.output_nodes[idx]
6096            .capture
6097            .as_ref()
6098            .map(|c| (c.settings.width, c.settings.height))
6099            .unwrap_or(state.output_nodes[idx].view_size);
6100        if x < base.0 || y < base.1 || x - base.0 + size.0 > mode.0 || y - base.1 + size.1 > mode.1
6101        {
6102            eprintln!(
6103                "[Wayland] RepositionOutput {id}: rejected, {}x{}+{x}+{y} leaves output \
6104                 {owner}'s {}x{}.",
6105                size.0, size.1, mode.0, mode.1
6106            );
6107            return false;
6108        }
6109        state.output_nodes[idx].pos = (x, y);
6110        if let Some(cap) = state.output_nodes[idx].capture.as_mut() {
6111            cap.needs_full_render = true;
6112        }
6113        println!("[Wayland] View {id} moved to ({x}, {y}).");
6114        return true;
6115    }
6116    let logical_size = state.output_nodes[idx]
6117        .logical_geometry()
6118        .map(|g| (g.size.w, g.size.h))
6119        .unwrap_or((0, 0));
6120    let physical_size = output
6121        .current_mode()
6122        .map(|m| (m.size.w, m.size.h))
6123        .unwrap_or((0, 0));
6124    if let Some((oid, flavor, other)) = find_output_overlap(
6125        &state.output_nodes,
6126        Some(id),
6127        (x, y, logical_size.0, logical_size.1),
6128        (x, y, physical_size.0, physical_size.1),
6129    ) {
6130        eprintln!(
6131            "[Wayland] RepositionOutput {id}: rejected, {flavor} rect {}x{}+{x}+{y} overlaps output {oid} at {}x{}+{}+{}.",
6132            if flavor == "logical" {
6133                logical_size.0
6134            } else {
6135                physical_size.0
6136            },
6137            if flavor == "logical" {
6138                logical_size.1
6139            } else {
6140                physical_size.1
6141            },
6142            other.2,
6143            other.3,
6144            other.0,
6145            other.1,
6146        );
6147        return false;
6148    }
6149    let (old_x, old_y) = state.output_nodes[idx].pos;
6150    state.output_nodes[idx].pos = (x, y);
6151    // A view is placed in layout coordinates over the screen it is cut from,
6152    // so the screen's move carries every view along: one left behind would
6153    // fall outside its screen and capture nothing.
6154    for view in state
6155        .output_nodes
6156        .iter_mut()
6157        .filter(|n| n.owner == Some(id))
6158    {
6159        view.pos = (view.pos.0 + x - old_x, view.pos.1 + y - old_y);
6160        if let Some(cap) = view.capture.as_mut() {
6161            cap.needs_full_render = true;
6162        }
6163    }
6164    if let Some(host) = state.host.as_ref() {
6165        host.set_layout(id, x, y);
6166    }
6167    output.change_current_state(None, None, None, Some((x, y).into()));
6168    state.space.map_output(&output, (x, y));
6169    let windows: Vec<smithay::desktop::Window> = state
6170        .space
6171        .elements()
6172        .filter(|w| wayland::frontend::window_output_id(w) == id)
6173        .cloned()
6174        .collect();
6175    if !wayland::wm::active(state) {
6176        for window in &windows {
6177            state.space.map_element(window.clone(), (x, y), false);
6178        }
6179    }
6180    state.refocus_pointer();
6181    if let Some(cap) = state.output_nodes[idx].capture.as_mut() {
6182        cap.needs_full_render = true;
6183    }
6184    println!("[Wayland] Output {id} repositioned to ({x}, {y}).");
6185    wayland::wm::outputs_changed(state);
6186    wayland::xwm::publish_workarea(state);
6187    true
6188}
6189
6190/// Destroy a secondary output: end its capture, relocate its windows onto the primary
6191/// output, unmap it from the space, and retract its global. The primary (id 0) is refused.
6192fn destroy_output_on(state: &mut AppState, id: u32) -> bool {
6193    if id == 0 {
6194        return false;
6195    }
6196    let Some(_) = state.node_idx_for_id(id) else {
6197        return false;
6198    };
6199    state
6200        .pending_screenshots
6201        .fail_display(id, "Screenshot output was removed");
6202    stop_capture_on_display(state, id);
6203    if let Some(host) = state.host.as_ref() {
6204        host.idle_output(id);
6205    }
6206    wayland_owners().lock().unwrap().remove(&id);
6207    // Relocate while the node is still registered so output leave/enter both resolve.
6208    let windows: Vec<smithay::desktop::Window> = state
6209        .space
6210        .elements()
6211        .filter(|w| wayland::frontend::window_output_id(w) == id)
6212        .cloned()
6213        .collect();
6214    for window in &windows {
6215        if wayland::wm::active(state) {
6216            if let Some(meta) = wayland::frontend::window_meta(window) {
6217                meta.output.store(0, Ordering::Relaxed);
6218            }
6219            continue;
6220        }
6221        // The primary keeps whichever screen it already shows: a nested session's
6222        // second screen parks again rather than covering the first, tagged with
6223        // the output it came from so recreating that output takes it back.
6224        if state.would_cover_screen(window, 0) {
6225            state.park_window(window, id);
6226        } else {
6227            state.place_window_on_output(window, 0);
6228        }
6229    }
6230    for w in &state.pending_windows {
6231        if let Some(meta) = wayland::frontend::window_meta(w)
6232            && meta.output.load(Ordering::Relaxed) == id
6233        {
6234            meta.output.store(0, Ordering::Relaxed);
6235        }
6236    }
6237    // A view holds no screen of its own, so only the node that published the
6238    // output unmaps it; destroying that node takes its views with it, captures
6239    // and encoders included.
6240    for view in state.view_ids_of(id) {
6241        state
6242            .pending_screenshots
6243            .fail_display(view, "Screenshot output was removed");
6244        stop_capture_on_display(state, view);
6245        wayland_owners().lock().unwrap().remove(&view);
6246        if let Some(vidx) = state.node_idx_for_id(view) {
6247            state.output_nodes.remove(vidx);
6248        }
6249    }
6250    let idx = state.node_idx_for_id(id).unwrap();
6251    let node = state.output_nodes.remove(idx);
6252    if let Some(global) = node.global {
6253        state.space.unmap_output(&node.output);
6254        state.dh.remove_global::<AppState>(global);
6255    }
6256    println!(
6257        "[Wayland] Output {id} destroyed; {} window(s) relocated to primary.",
6258        windows.len()
6259    );
6260    wayland::workspace::outputs_changed(state);
6261    wayland::wm::outputs_changed(state);
6262    wayland::xwm::publish_workarea(state);
6263    true
6264}
6265/// Resize a screen's output, keeping its position, so the displays cut out of it can be
6266/// laid out over a different desk.
6267///
6268/// Refused for a view (it has no output of its own) and for a size that would leave one of
6269/// the screen's views outside it -- the caller grows the screen before moving views onto
6270/// the new area and shrinks it after taking them off, so no view is ever orphaned.
6271/// Send every non-parked window on `display_id`'s output a fullscreen configure
6272/// for the new mode, with the scale each surface should render at. Without this
6273/// a live session keeps drawing at the old size under the resized screen.
6274/// The windows a mode change on an output reaches: under the window policy the ones the
6275/// policy placed on it, else the ones tagged for the display. A parked screen's windows are
6276/// left out either way: they are tagged for a display but composited on none, holding
6277/// PARKED_LOGICAL_SIZE until `create_output` gives them a screen, and resizing them would
6278/// double the session's coordinate space onto a screen nobody watches, which is where a window
6279/// that centers itself then lands.
6280fn windows_on_display(
6281    state: &AppState,
6282    display_id: u32,
6283    output: &Output,
6284) -> Vec<smithay::desktop::Window> {
6285    let on_output: Vec<smithay::desktop::Window> = if wayland::wm::active(state) {
6286        state.space.elements_for_output(output).cloned().collect()
6287    } else {
6288        state
6289            .space
6290            .elements()
6291            .filter(|window| wayland::frontend::window_output_id(window) == display_id)
6292            .cloned()
6293            .collect()
6294    };
6295    on_output
6296        .into_iter()
6297        .filter(|window| {
6298            !wayland::frontend::window_meta(window)
6299                .is_some_and(|meta| meta.parked.load(Ordering::Relaxed))
6300        })
6301        .collect()
6302}
6303
6304/// Tell every window on an output the output's scale: the surface's preferred buffer scale
6305/// and fractional scale, so a client draws at the new density rather than being stretched to
6306/// it. The size a client keeps is the policy's business where one runs, and the fullscreen
6307/// configure's otherwise.
6308fn announce_scale_to_windows(state: &AppState, display_id: u32, output: &Output, scale: f64) {
6309    let scale = scale.max(0.1);
6310    for window in windows_on_display(state, display_id, output) {
6311        if let Some(surface) = window.wl_surface() {
6312            output.enter(&surface);
6313            with_states(&surface, |states| {
6314                smithay::wayland::compositor::send_surface_state(
6315                    &surface,
6316                    states,
6317                    scale.ceil() as i32,
6318                    Transform::Normal,
6319                );
6320                smithay::wayland::fractional_scale::with_fractional_scale(states, |fs| {
6321                    fs.set_preferred_scale(scale);
6322                });
6323            });
6324        }
6325    }
6326}
6327
6328fn configure_windows_for_mode(
6329    state: &AppState,
6330    display_id: u32,
6331    output: &Output,
6332    width: i32,
6333    height: i32,
6334    scale: f64,
6335) {
6336    let scale = scale.max(0.1);
6337    let logical_width = (f64::from(width) / scale).round() as i32;
6338    let logical_height = (f64::from(height) / scale).round() as i32;
6339    announce_scale_to_windows(state, display_id, output, scale);
6340    for window in windows_on_display(state, display_id, output) {
6341        if let Some(toplevel) = window.toplevel() {
6342            toplevel.with_pending_state(|state| {
6343                use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State;
6344                state.states.set(State::Fullscreen);
6345                state.states.set(State::Activated);
6346                state.size = Some((logical_width, logical_height).into());
6347            });
6348            toplevel.send_configure();
6349        }
6350    }
6351}
6352
6353fn resize_output_on(state: &mut AppState, id: u32, width: i32, height: i32, scale: f64) -> bool {
6354    if width <= 0 || height <= 0 || scale <= 0.0 {
6355        return false;
6356    }
6357    let Some(idx) = state.node_idx_for_id(id) else {
6358        return false;
6359    };
6360    if state.output_nodes[idx].owner.is_some() {
6361        eprintln!("[Wayland] ResizeOutput {id}: rejected, {id} is a view of another output.");
6362        return false;
6363    }
6364    let base = state.output_nodes[idx].pos;
6365    // Only a view that is showing something can be orphaned by the resize; one
6366    // without a capture is placed by whoever starts it, against the new size.
6367    for view in state
6368        .output_nodes
6369        .iter()
6370        .filter(|n| n.owner == Some(id) && n.capture.is_some())
6371    {
6372        let (vw, vh) = view
6373            .capture
6374            .as_ref()
6375            .map(|c| (c.settings.width, c.settings.height))
6376            .unwrap_or(view.view_size);
6377        if view.pos.0 - base.0 + vw > width || view.pos.1 - base.1 + vh > height {
6378            eprintln!(
6379                "[Wayland] ResizeOutput {id}: rejected, view {} would fall outside {width}x{height}.",
6380                view.id
6381            );
6382            return false;
6383        }
6384    }
6385    let output = state.output_nodes[idx].output.clone();
6386    let refresh = output.current_mode().map(|m| m.refresh).unwrap_or(60_000);
6387    if output.current_mode().map(|m| (m.size.w, m.size.h)) == Some((width, height))
6388        && (output.current_scale().fractional_scale() - scale).abs() <= 0.001
6389    {
6390        return true;
6391    }
6392    // The GPU target is allocated before anything is committed: a driver that refuses the
6393    // new dimensions leaves the previous mode and buffers live rather than a half-resized
6394    // output.
6395    let mut new_offscreen = None;
6396    if state.use_gpu {
6397        let Some(gbm) = state.gbm_device.as_mut() else {
6398            return false;
6399        };
6400        match alloc_render_target(gbm, width as u32, height as u32, GbmFormat::Argb8888) {
6401            Ok(bo) => {
6402                let dmabuf = create_dmabuf_from_bo(&bo);
6403                new_offscreen = Some((bo, dmabuf));
6404            }
6405            Err(e) => {
6406                eprintln!(
6407                    "[Wayland] ResizeOutput {id}: GBM allocation {width}x{height} failed ({e:?})."
6408                );
6409                return false;
6410            }
6411        }
6412    }
6413    let mode = OutputMode {
6414        size: (width, height).into(),
6415        refresh,
6416    };
6417    output.change_current_state(
6418        Some(mode),
6419        Some(Transform::Normal),
6420        Some(OutputScale::Fractional(scale)),
6421        Some(Point::from(base)),
6422    );
6423    output.set_preferred(mode);
6424    state.space.map_output(&output, base);
6425    // Capture clients allocate to the announced size; a stale one fails validation on
6426    // every frame they submit from here on.
6427    for cs in state
6428        .copy_sessions
6429        .iter()
6430        .filter(|cs| cs.output.upgrade().as_ref() == Some(&output))
6431    {
6432        if let Some(c) = wayland::frontend::output_capture_constraints(
6433            &output,
6434            state.gles_renderer.as_ref(),
6435            &state.render_node_path,
6436        ) {
6437            cs.session.update_constraints(c);
6438        }
6439    }
6440    let node = &mut state.output_nodes[idx];
6441    node.frame_buffer = vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4];
6442    node.view_size = (width, height);
6443    node.target_seeded = false;
6444    if let Some(off) = new_offscreen.take() {
6445        node.offscreen_buffer = Some(off);
6446    }
6447    for n in state
6448        .output_nodes
6449        .iter_mut()
6450        .filter(|n| n.id == id || n.owner == Some(id))
6451    {
6452        if let Some(cap) = n.capture.as_mut() {
6453            cap.needs_full_render = true;
6454        }
6455    }
6456    if wayland::wm::active(state) {
6457        announce_scale_to_windows(state, id, &output, scale);
6458    } else {
6459        configure_windows_for_mode(state, id, &output, width, height, scale);
6460    }
6461    // The layer surfaces (a desktop's wallpaper and panel) are laid out for the mode they were
6462    // mapped on; the arrangement sends each its new size, and the frame that follows is
6463    // rendered without waiting for a client to commit something.
6464    layer_map_for_output(&output).arrange();
6465    wayland::wm::mark_dirty(state);
6466    println!("[Wayland] Output {id} resized to {width}x{height} scale {scale:.2}.");
6467    wayland::wm::outputs_changed(state);
6468    wayland::xwm::publish_workarea(state);
6469    true
6470}
6471
6472/// The view every screen carries from the start, covering it: the display a
6473/// consumer gets without asking for a layout at all.
6474const DEFAULT_VIEW_ID: u32 = 1;
6475
6476/// Add a view: a display over a rectangle of `owner`'s output.
6477///
6478/// A view renders and encodes on its own -- its own damage tracker, frame buffer, and
6479/// capture -- but publishes no `wl_output`, so the session sees one screen however many
6480/// displays the client is shown. That is what a window drag needs: the pointer grab it
6481/// runs under stays inside one surface across the whole desk, where separate screens
6482/// stop it at the edge of the one it began on.
6483fn create_view_on(
6484    state: &mut AppState,
6485    id: u32,
6486    owner: u32,
6487    x: i32,
6488    y: i32,
6489    width: i32,
6490    height: i32,
6491) -> bool {
6492    if state.node_idx_for_id(id).is_some() || width <= 0 || height <= 0 || x < 0 || y < 0 {
6493        return false;
6494    }
6495    let Some(oidx) = state.node_idx_for_id(owner) else {
6496        return false;
6497    };
6498    if state.output_nodes[oidx].owner.is_some() {
6499        eprintln!("[Wayland] CreateView {id}: rejected, output {owner} is itself a view.");
6500        return false;
6501    }
6502    let output = state.output_nodes[oidx].output.clone();
6503    let scale = output.current_scale().fractional_scale();
6504    let Some(mode) = output.current_mode() else {
6505        return false;
6506    };
6507    if x + width > mode.size.w || y + height > mode.size.h {
6508        eprintln!(
6509            "[Wayland] CreateView {id}: rejected, {width}x{height}+{x}+{y} leaves output \
6510             {owner}'s {}x{}.",
6511            mode.size.w, mode.size.h
6512        );
6513        return false;
6514    }
6515    let mut offscreen = None;
6516    if state.use_gpu {
6517        let Some(gbm) = state.gbm_device.as_mut() else {
6518            return false;
6519        };
6520        match alloc_render_target(gbm, width as u32, height as u32, GbmFormat::Argb8888) {
6521            Ok(bo) => {
6522                let dmabuf = create_dmabuf_from_bo(&bo);
6523                offscreen = Some((bo, dmabuf));
6524            }
6525            Err(e) => {
6526                eprintln!(
6527                    "[Wayland] CreateView {id}: GBM allocation {width}x{height} failed ({e:?})."
6528                );
6529                return false;
6530            }
6531        }
6532    }
6533    let pos = state.output_nodes[oidx].pos;
6534    let origin = (pos.0 + x, pos.1 + y);
6535    // Static rather than tied to the output: the tracker follows the view's rectangle,
6536    // not the whole screen the view is cut from.
6537    let damage_tracker = OutputDamageTracker::new((width, height), scale, Transform::Normal);
6538    println!("[Wayland] View {id} created over output {owner}: {width}x{height} @ ({x}, {y}).");
6539    state.output_nodes.push(wayland::frontend::OutputNode {
6540        id,
6541        output,
6542        global: None,
6543        owner: Some(owner),
6544        view_size: (width, height),
6545        view_scale: 0.0,
6546        pos: origin,
6547        damage_tracker,
6548        content_tracker: None,
6549        frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
6550        offscreen_buffer: offscreen,
6551        undrawn_ticks: 0,
6552        overlay_state: OverlayState::default(),
6553        capture: None,
6554        frame_seq: 0,
6555        target_seeded: false,
6556        content_hold_until: None,
6557    });
6558    true
6559}
6560
6561/// Startup inputs for the compositor thread: its two calloop channels, the sender it hands
6562/// to computer-use, the initial geometry, and the GPU / cursor policy.
6563struct WaylandThreadConfig {
6564    command_rx: smithay::reexports::calloop::channel::Channel<ThreadCommand>,
6565    wake_rx: smithay::reexports::calloop::channel::Channel<()>,
6566    wake_tx: smithay::reexports::calloop::channel::Sender<()>,
6567    command_tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
6568    initial_width: i32,
6569    initial_height: i32,
6570    explicit_dri_node: String,
6571    auto_gpu_selected: bool,
6572    cursor_size: i32,
6573    pointer_share: Arc<crate::wayland::frontend::PointerShare>,
6574}
6575
6576/// The library smithay opens for its EGL calls, by the name it opens it by.
6577const EGL_LIBRARY: &str = "libEGL.so.1";
6578
6579/// Whether [`EGL_LIBRARY`] opens. It is opened once and kept open, so smithay's own open of it
6580/// finds it loaded.
6581fn egl_library() -> Result<(), String> {
6582    static LIBRARY: OnceLock<Result<libloading::Library, String>> = OnceLock::new();
6583    LIBRARY
6584        // SAFETY: libEGL's initializers are the ones smithay runs when it opens the library.
6585        .get_or_init(|| {
6586            unsafe { libloading::Library::new(EGL_LIBRARY) }.map_err(|e| {
6587                match std::error::Error::source(&e) {
6588                    Some(why) => format!("{e}: {why}"),
6589                    None => e.to_string(),
6590                }
6591            })
6592        })
6593        .as_ref()
6594        .map(|_| ())
6595        .map_err(|e| format!("{EGL_LIBRARY} is not available: {e}"))
6596}
6597
6598/// An EGL display on `native`, or an error where libEGL is not installed.
6599///
6600/// smithay opens libEGL the first time any of its EGL calls runs and panics where the library
6601/// is missing. Every EGL display is made here, after [`egl_library`] has opened it, so a host
6602/// without libEGL gets an error its GPU path declines on, as for any other EGL failure.
6603/// `clippy.toml` holds the crate to this function and keeps smithay's EGL entry points out.
6604pub(crate) fn egl_display<N>(native: N) -> Result<EGLDisplay, String>
6605where
6606    N: smithay::backend::egl::native::EGLNativeDisplay + 'static,
6607{
6608    egl_library()?;
6609    // SAFETY: the EGL displays made outside smithay (the GPU probe's, on EGL devices and X11)
6610    // are on other native displays than the GBM devices smithay's are, so terminating one of
6611    // them never ends a display smithay tracks.
6612    #[allow(clippy::disallowed_methods)]
6613    unsafe { EGLDisplay::new(native) }.map_err(|e| format!("{e}"))
6614}
6615
6616/// Bring the hardware renderer up on a DRM render node: the GBM device, the EGL display and
6617/// context it backs, the GLES renderer, and the allocator the render targets come from.
6618///
6619/// The compositor and [`probe_wayland_gpu`] share this so the probe's answer is the
6620/// compositor's own. Errors name the step that failed, which is all a caller can act on.
6621fn gpu_render_init(
6622    device_path: &std::path::Path,
6623) -> Result<(RawGbmDevice<File>, GlesRenderer), String> {
6624    let file = File::options()
6625        .read(true)
6626        .write(true)
6627        .open(device_path)
6628        .map_err(|e| format!("Failed to open render device: {}", e))?;
6629    let file_for_alloc = file
6630        .try_clone()
6631        .map_err(|e| format!("Failed to clone file for GBM Allocator: {}", e))?;
6632    let gbm_allocator =
6633        RawGbmDevice::new(file_for_alloc).map_err(|_| "Failed to create Raw GBM Device")?;
6634    let gbm = GbmDevice::new(file).map_err(|_| "Failed to create GBM device")?;
6635    let egl = egl_display(gbm).map_err(|e| format!("Failed to create EGL display: {e}"))?;
6636    let context = EGLContext::new(&egl).map_err(|_| "Failed to create EGL context")?;
6637    let renderer =
6638        unsafe { GlesRenderer::new(context) }.map_err(|_| "Failed to init GlesRenderer")?;
6639    Ok((gbm_allocator, renderer))
6640}
6641
6642/// What the compositor renders with on a GPU, all of it brought up before any client hears of
6643/// the GPU (`gpu_bring_up`).
6644struct GpuBringUp {
6645    gbm: RawGbmDevice<File>,
6646    renderer: GlesRenderer,
6647    formats: Vec<smithay::backend::allocator::Format>,
6648    node: DrmNode,
6649    target: (BufferObject<()>, Dmabuf),
6650}
6651
6652/// The GL renderer on `device_path`, the dmabuf formats it imports, and its offscreen render
6653/// target at `width` by `height`. It touches no Wayland global: the caller advertises the GPU
6654/// only once this succeeded, since a dmabuf global left behind by a failed start (a render node
6655/// whose EGL comes up but whose allocator refuses the target, as on a virtual GPU) offered the
6656/// GL renderer's formats to a compositor rendering in Pixman, which refused every buffer, and a
6657/// nested kwin_wayland died of the protocol error.
6658fn gpu_bring_up(
6659    device_path: &std::path::Path,
6660    width: u32,
6661    height: u32,
6662) -> Result<GpuBringUp, String> {
6663    let (gbm, renderer) = gpu_render_init(device_path)?;
6664    let formats = Bind::<Dmabuf>::supported_formats(&renderer)
6665        .ok_or("Failed to query formats")?
6666        .into_iter()
6667        .collect::<Vec<_>>();
6668    let node = DrmNode::from_path(device_path).map_err(|_| "Failed to create DrmNode")?;
6669    let bo = alloc_render_target(&gbm, width, height, GbmFormat::Argb8888)
6670        .map_err(|_| "Failed to allocate GBM buffer")?;
6671    let dmabuf = create_dmabuf_from_bo(&bo);
6672    Ok(GpuBringUp {
6673        gbm,
6674        renderer,
6675        formats,
6676        node,
6677        target: (bo, dmabuf),
6678    })
6679}
6680
6681/// `GL_RENDERER` of a live renderer — the driver that actually answered, which is the only
6682/// thing that separates a GPU from Mesa's software fallback on the same node. Empty when the
6683/// context cannot be made current or the string is unavailable.
6684fn gl_renderer_name(renderer: &mut GlesRenderer) -> String {
6685    renderer
6686        .with_context(|gl| unsafe {
6687            let ptr = gl.GetString(smithay::backend::renderer::gles::ffi::RENDERER);
6688            if ptr.is_null() {
6689                String::new()
6690            } else {
6691                std::ffi::CStr::from_ptr(ptr as *const std::ffi::c_char)
6692                    .to_string_lossy()
6693                    .into_owned()
6694            }
6695        })
6696        .unwrap_or_default()
6697}
6698
6699/// The name the GL driver gives the GPU behind render node `node_index`, asked once per node
6700/// and remembered: what a VA-API session, which names no device, is reported to run on.
6701/// Empty where no GL context comes up on the node.
6702fn node_gpu_name(node_index: i32) -> String {
6703    static NAMES: OnceLock<Mutex<std::collections::HashMap<i32, String>>> = OnceLock::new();
6704    let mut names = NAMES.get_or_init(Default::default).lock().unwrap();
6705    names
6706        .entry(node_index)
6707        .or_insert_with(|| {
6708            let path = format!("/dev/dri/renderD{}", 128 + node_index);
6709            gpu_render_init(std::path::Path::new(&path))
6710                .map(|(_gbm, mut renderer)| gl_renderer_name(&mut renderer))
6711                .unwrap_or_default()
6712        })
6713        .clone()
6714}
6715
6716/// A GPU is exposed to this container or machine.
6717///
6718/// Only device nodes count. `/sys/class/drm` is the host's and lists cards a container may
6719/// have no access to, while `/dev` is the container's own; the NVIDIA character devices
6720/// answer for a driver stack that was given no DRM node at all.
6721fn gpu_exposed() -> bool {
6722    std::path::Path::new("/dev/nvidiactl").exists()
6723        || std::fs::read_dir("/dev/dri")
6724            .into_iter()
6725            .flatten()
6726            .flatten()
6727            .any(|e| {
6728                let name = e.file_name();
6729                let name = name.to_string_lossy();
6730                name.starts_with("renderD") || name.starts_with("card")
6731            })
6732}
6733
6734/// The main execution loop of the Wayland backend.
6735///
6736/// This function is the central nervous system of the backend. It runs on its own thread and owns
6737/// the entire lifecycle of the headless Wayland compositor:
6738///
6739/// 1. **Initialization**: builds the `calloop` event loop and the Wayland display, raises
6740///    libwayland's per-client buffer limit when the newer setter is available (resolved at runtime
6741///    so the module still loads against older libwayland), and brings up the rendering pipeline —
6742///    GBM/EGL hardware acceleration on the resolved DRM render node, falling back to software
6743///    rendering (Pixman) when no node is usable.
6744/// 2. **State management**: constructs and holds the `AppState` — the Wayland globals (compositor,
6745///    seat, SHM, shell, dmabuf, selections, and the rest) plus the output registry: the primary
6746///    virtual `HEADLESS-1` output at layout (0, 0), extended at runtime by CreateOutput with
6747///    additional outputs at their layout offsets, each `OutputNode` owning its damage tracker,
6748///    render targets, and (at most one) capture pipeline.
6749/// 3. **Event dispatch**:
6750///    - **Command channel**: control messages from the Python thread — per-display start/stop,
6751///      output lifecycle (create/destroy/list/move-window), input injection routed across the
6752///      output layout, keymap and clipboard operations, live rate / tunable changes, and the
6753///      computer-use queries.
6754///    - **Wayland socket**: accepts client connections and drives the compositor protocol.
6755/// 4. **StartCapture reconfigure** (per display): reprograms that output's mode / scale / refresh,
6756///    resizes its framebuffer and offscreen GBM buffer, and fullscreens the toplevels placed on
6757///    it. The encode device is resolved here: an operator's explicit `encode_node_index`
6758///    (-1 software, >= 0 a device) always wins, and only the unset `-2` sentinel is filled: from
6759///    the auto-picked render node, else the first one present. H.264 output masks the dimensions
6760///    even, because 4:2:0 needs even width and height.
6761/// 5. **Encode-path choice + render loop**: only a same-GPU GLES session encodes zero-copy on this
6762///    calloop thread, because the dmabuf and its EGL context are calloop-affine; every readback
6763///    flavor (the CPU path's H.264 and JPEG, Pixman, or a cross-GPU hardware encoder) builds its
6764///    encoders on that display's dedicated encode thread instead. A shared timer (paced at the
6765///    fastest active capture) renders each capturing output — its windows, popups, and layers made
6766///    output-local, the cursor only on the pointer's output — applies the shared paint-over /
6767///    recovery-IDR policy per display, and delivers each display's encoded stripes through its own
6768///    frame callback. The zero-copy encode waits the GL render fence first, so a hardware encoder
6769///    reading the dmabuf through CUDA/VA never maps a half-rasterized (torn) frame.
6770/// 6. **Thread lifecycle**: the Python frame callback runs on a dedicated delivery thread so its
6771///    GIL never stalls calloop input / control dispatch, and in readback mode the encoders run on
6772///    the `wl-encode` thread. On a restart or stop the encode thread is torn down before the
6773///    delivery thread — it feeds the delivery sender and must be gone first — and the retained
6774///    callbacks are dropped and gated by a process-shutdown flag so nothing fires into a finalizing
6775///    interpreter.
6776fn run_wayland_thread(cfg: WaylandThreadConfig) {
6777    let WaylandThreadConfig {
6778        command_rx,
6779        wake_rx,
6780        wake_tx,
6781        command_tx,
6782        initial_width,
6783        initial_height,
6784        explicit_dri_node,
6785        auto_gpu_selected,
6786        cursor_size,
6787        pointer_share,
6788    } = cfg;
6789    let width: i32 = if initial_width > 0 {
6790        initial_width
6791    } else {
6792        1024
6793    };
6794    let height: i32 = if initial_height > 0 {
6795        initial_height
6796    } else {
6797        768
6798    };
6799
6800    let mut event_loop = match EventLoop::<AppState>::try_new() {
6801        Ok(l) => l,
6802        Err(e) => {
6803            eprintln!("[Wayland] compositor thread aborting: event loop init failed: {e}");
6804            return;
6805        }
6806    };
6807    let display: Display<AppState> = match Display::new() {
6808        Ok(d) => d,
6809        Err(e) => {
6810            eprintln!("[Wayland] compositor thread aborting: display init failed: {e}");
6811            return;
6812        }
6813    };
6814    let dh: DisplayHandle = display.handle();
6815    unsafe {
6816        if let Ok(lib) = libloading::Library::new("libwayland-server.so.0") {
6817            if let Ok(set_max) = lib.get::<unsafe extern "C" fn(*mut std::ffi::c_void, usize)>(
6818                b"wl_display_set_default_max_buffer_size\0",
6819            ) {
6820                set_max(
6821                    dh.backend_handle().display_ptr() as *mut std::ffi::c_void,
6822                    10 * 1024 * 1024,
6823                );
6824            }
6825            std::mem::forget(lib);
6826        }
6827    }
6828
6829    let dri_node = explicit_dri_node;
6830
6831    let mut use_gpu = !dri_node.is_empty();
6832    let render_node_path = dri_node.clone();
6833
6834    let mut gles_renderer = None;
6835    let mut pixman_renderer = None;
6836    let mut offscreen_buffer: Option<(BufferObject<()>, Dmabuf)> = None;
6837    let mut dmabuf_global = None;
6838    let mut gbm_device_raw = None;
6839    let mut dmabuf_state = DmabufState::new();
6840
6841    let mut gpu_success = false;
6842    if use_gpu {
6843        println!("[Wayland] Renderer: GL on {dri_node}.");
6844        let init_res = gpu_bring_up(std::path::Path::new(&dri_node), width as u32, height as u32)
6845            .map(|mut up| {
6846                if let Err(e) = up.renderer.bind_wl_display(&dh) {
6847                    crate::log::debug!(
6848                        "[Wayland] EGL did not bind to the Wayland display (optional): {:?}",
6849                        e
6850                    );
6851                }
6852                let feedback =
6853                    DmabufFeedbackBuilder::new(up.node.dev_id(), up.formats.clone()).build();
6854                dmabuf_global = Some(if let Ok(default_feedback) = feedback {
6855                    dmabuf_state
6856                        .create_global_with_default_feedback::<AppState>(&dh, &default_feedback)
6857                } else {
6858                    dmabuf_state.create_global::<AppState>(&dh, up.formats)
6859                });
6860                offscreen_buffer = Some(up.target);
6861                gbm_device_raw = Some(up.gbm);
6862                gles_renderer = Some(up.renderer);
6863            });
6864
6865        match init_res {
6866            Ok(()) => {
6867                gpu_success = true;
6868                report::set_renderer(report::Renderer {
6869                    kind: "gl",
6870                    node: dri_node.clone(),
6871                    gpu: gles_renderer
6872                        .as_mut()
6873                        .map(gl_renderer_name)
6874                        .unwrap_or_default(),
6875                    reason: String::new(),
6876                });
6877            }
6878            Err(e) => {
6879                eprintln!(
6880                    "[Wayland] GPU renderer failed to initialize ({e}); rendering in software (Pixman)."
6881                );
6882                report::set_renderer(report::Renderer {
6883                    kind: "pixman",
6884                    node: String::new(),
6885                    gpu: String::new(),
6886                    reason: format!("the GPU renderer did not initialize on {dri_node}: {e}"),
6887                });
6888                use_gpu = false;
6889            }
6890        }
6891    }
6892
6893    if !gpu_success {
6894        if dri_node.is_empty() {
6895            println!("[Wayland] Renderer: software (Pixman), no render node.");
6896            report::set_renderer(report::Renderer {
6897                kind: "pixman",
6898                node: String::new(),
6899                gpu: String::new(),
6900                reason: "no render node".to_string(),
6901            });
6902        }
6903        pixman_renderer = Some(PixmanRenderer::new().expect("Failed to init PixmanRenderer"));
6904        use_gpu = false;
6905    }
6906
6907    let compositor_state = CompositorState::new_v6::<AppState>(&dh);
6908    let image_capture_source_state = ImageCaptureSourceState::new();
6909    let output_capture_source_state = OutputCaptureSourceState::new::<AppState>(&dh);
6910    let image_copy_capture_state = ImageCopyCaptureState::new::<AppState>(&dh);
6911    let fractional_scale_state = FractionalScaleManagerState::new::<AppState>(&dh);
6912    let shm_state = ShmState::new::<AppState>(&dh, vec![]);
6913    let output_state = OutputManagerState::new_with_xdg_output::<AppState>(&dh);
6914    let mut seat_state = SeatState::new();
6915    let shell_state = XdgShellState::new::<AppState>(&dh);
6916    let space = Space::default();
6917    let layer_shell_state = WlrLayerShellState::new::<AppState>(&dh);
6918    let keyboard_shortcuts_inhibit_state =
6919        smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitState::new::<AppState>(
6920            &dh,
6921        );
6922    let data_device_state = DataDeviceState::new::<AppState>(&dh);
6923    let data_control_state = DataControlState::new::<AppState, _>(&dh, None, |_| true);
6924    let ext_data_control_state = ExtDataControlState::new::<AppState, _>(&dh, None, |_| true);
6925    let cursor_shape_state = CursorShapeManagerState::new::<AppState>(&dh);
6926    let _vk_global = dh.create_global::<AppState, ZwpVirtualKeyboardManagerV1, _>(1, ());
6927    let pointer_warp_state = PointerWarpManager::new::<AppState>(&dh);
6928    let relative_pointer_state = RelativePointerManagerState::new::<AppState>(&dh);
6929    let pointer_constraints_state = PointerConstraintsState::new::<AppState>(&dh);
6930
6931    let foreign_toplevel_list = ForeignToplevelListState::new::<AppState>(&dh);
6932    let xdg_decoration_state = XdgDecorationState::new::<AppState>(&dh);
6933    let single_pixel_buffer = SinglePixelBufferState::new::<AppState>(&dh);
6934    let viewporter_state = ViewporterState::new::<AppState>(&dh);
6935    let presentation_state = PresentationState::new::<AppState>(&dh, 1);
6936    let xdg_activation_state = XdgActivationState::new::<AppState>(&dh);
6937    let primary_selection_state = PrimarySelectionState::new::<AppState>(&dh);
6938    let popups = PopupManager::default();
6939
6940    let mut seat = seat_state.new_wl_seat(&dh, "seat0");
6941    // Clients repeat a held key after 600 ms, the delay desktops default to: a remote key's
6942    // release can land a few hundred milliseconds late, and a shorter delay types it again.
6943    seat.add_keyboard(XkbConfig::default(), 600, 25)
6944        .expect("Failed to init keyboard");
6945    seat.add_pointer();
6946
6947    let (host_frame_tx, host_frame_rx) = smithay::reexports::calloop::channel::channel::<usize>();
6948
6949    let mut state = AppState {
6950        compositor_state,
6951        fractional_scale_state,
6952        viewporter_state,
6953        presentation_state,
6954        shm_state,
6955        single_pixel_buffer,
6956        dmabuf_state,
6957        dmabuf_global,
6958        ext_data_control_state,
6959        cursor_shape_state,
6960        image_capture_source_state,
6961        output_capture_source_state,
6962        image_copy_capture_state,
6963        copy_sessions: Vec::new(),
6964        output_state,
6965        seat_state,
6966        shell_state,
6967        layer_shell_state,
6968        space,
6969        data_device_state,
6970        data_control_state,
6971        dh: dh.clone(),
6972        seat,
6973        pointer_warp_state,
6974        relative_pointer_state,
6975        pointer_constraints_state,
6976        output_nodes: Vec::new(),
6977        pending_windows: Vec::new(),
6978        foreign_toplevel_list,
6979        keyboard_shortcuts_inhibit_state,
6980        xdg_decoration_state,
6981        xdg_activation_state,
6982        primary_selection_state,
6983        popups,
6984        gles_renderer,
6985        pixman_renderer,
6986        gbm_device: gbm_device_raw,
6987        settings: CaptureSettings {
6988            width,
6989            height,
6990            ..CaptureSettings::default()
6991        },
6992        cursor_callback_set: false,
6993        cursor_tx: wayland::cursor::spawn_cursor_worker(
6994            cursor_size,
6995            CaptureSettings::default().cursor_size_cap,
6996        ),
6997        clipboard_callback: None,
6998        pending_clipboard_read: None,
6999        clipboard_generation: Arc::new(std::sync::atomic::AtomicU64::new(0)),
7000        current_selection_mimes: Vec::new(),
7001        last_log_time: Instant::now(),
7002        start_time: Instant::now(),
7003        clock: Clock::new(),
7004        use_gpu,
7005        cursor_helper: Cursor::load(cursor_size),
7006        keymap_policy: wayland::keymap::KeymapPolicy::seat(),
7007        host: None,
7008        host_layout_pending: std::collections::HashMap::new(),
7009        host_frame_tx,
7010        host_mode_refusals: std::collections::HashMap::new(),
7011        current_cursor_icon: None,
7012        cursor_surface_pending: false,
7013        cursor_buffer: None,
7014        dnd_icon: None,
7015        render_cursor_on_framebuffer: false,
7016        render_node_path,
7017        auto_gpu_selected,
7018        pending_screenshots: wayland::frontend::ScreenshotQueue::default(),
7019        screenshot_readbacks: ScreenshotReadbacks::default(),
7020        command_rx: None,
7021        last_input_at: None,
7022        pointer_motion_at: None,
7023        pointer_share,
7024        motion_applied: 0,
7025        input_interval: None,
7026        frame_idle_long: false,
7027        last_idle_service_at: None,
7028        deliver_reaper: Vec::new(),
7029        encode_reaper: Vec::new(),
7030        wm: None,
7031        foreign_toplevel: wayland::foreign_toplevel::ForeignToplevelState::new(&dh),
7032        workspace: wayland::workspace::WorkspaceState::new(&dh),
7033        xwm: wayland::xwm::XwmState::new(
7034            smithay::wayland::xwayland_shell::XWaylandShellState::new::<AppState>(&dh),
7035        ),
7036        loop_handle: event_loop.handle(),
7037        keyboard_focus: None,
7038    };
7039    // Seed the keymap policy with the seat's initial keymap so overlay binds splice onto
7040    // the exact text clients received.
7041    {
7042        let initial_keymap = if let Some(kb) = state.seat.get_keyboard() {
7043            kb.with_xkb_state(&mut state, |context| match context.xkb().lock() {
7044                Ok(guard) => {
7045                    let keymap = unsafe { guard.keymap() };
7046                    keymap.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
7047                }
7048                Err(_) => String::new(),
7049            })
7050        } else {
7051            String::new()
7052        };
7053        state.keymap_policy.rebuild_base(initial_keymap);
7054    }
7055
7056    let output = Output::new(
7057        "HEADLESS-1".into(),
7058        PhysicalProperties {
7059            size: (width, height).into(),
7060            subpixel: Subpixel::Unknown,
7061            make: "Pixelflux".into(),
7062            model: "Virtual".into(),
7063            serial_number: "001".into(),
7064        },
7065    );
7066    output.change_current_state(
7067        Some(OutputMode {
7068            size: (width, height).into(),
7069            refresh: 60_000,
7070        }),
7071        Some(Transform::Normal),
7072        Some(OutputScale::Fractional(1.0)),
7073        Some((0, 0).into()),
7074    );
7075    output.set_preferred(OutputMode {
7076        size: (width, height).into(),
7077        refresh: 60_000,
7078    });
7079    state.space.map_output(&output, (0, 0));
7080    let global = output.create_global::<AppState>(&dh);
7081    let damage_tracker = OutputDamageTracker::from_output(&output);
7082    state.output_nodes.push(wayland::frontend::OutputNode {
7083        id: 0,
7084        output,
7085        global: Some(global),
7086        owner: None,
7087        view_size: (width, height),
7088        view_scale: 0.0,
7089        pos: (0, 0),
7090        damage_tracker,
7091        content_tracker: None,
7092        frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
7093        offscreen_buffer,
7094        undrawn_ticks: 0,
7095        overlay_state: OverlayState::default(),
7096        capture: None,
7097        frame_seq: 0,
7098        target_seeded: false,
7099        content_hold_until: None,
7100    });
7101    // The screen always carries one view covering it, so a consumer that asks for
7102    // nothing but a single display finds one there: the screen itself is never
7103    // captured, only the views cut from it are.
7104    create_view_on(&mut state, DEFAULT_VIEW_ID, 0, 0, 0, width, height);
7105
7106    /// Apply every queued control command in FIFO order, and say whether any was input. Sends
7107    /// wake the loop through the
7108    /// separate wake channel, and the render tick ALSO drains before starting its work, so
7109    /// queued input is applied ahead of a long render/encode instead of waiting it out.
7110    /// Fold one pointer move into the smoothed spacing between moves, the cadence the client
7111    /// sends its motion at; a gap of more than a few frame periods is a pause and is clamped so
7112    /// the next moves read as paced again within a couple of them.
7113    fn note_pointer_motion(state: &mut AppState) {
7114        let now = Instant::now();
7115        if let Some(prev) = state.pointer_motion_at {
7116            let cap = frame_period(state.settings.target_fps) * 4;
7117            let dt = now.saturating_duration_since(prev).min(cap);
7118            state.input_interval = Some(match state.input_interval {
7119                None => dt,
7120                Some(avg) => avg.mul_f64(0.875) + dt.mul_f64(0.125),
7121            });
7122        }
7123        state.pointer_motion_at = Some(now);
7124    }
7125
7126    fn drain_thread_commands(state: &mut AppState) -> bool {
7127        let Some(rx) = state.command_rx.take() else {
7128            return false;
7129        };
7130        let mut had_input = false;
7131        while let Ok(cmd) = rx.try_recv() {
7132            had_input |= matches!(
7133                cmd,
7134                ThreadCommand::KeyboardKey { .. }
7135                    | ThreadCommand::KeyboardKeys { .. }
7136                    | ThreadCommand::PointerMotion { .. }
7137                    | ThreadCommand::PointerRelativeMotion { .. }
7138                    | ThreadCommand::PointerButton { .. }
7139                    | ThreadCommand::PointerAxis { .. }
7140                    | ThreadCommand::PointerScroll { .. }
7141                    | ThreadCommand::PointerScrollEnd
7142            );
7143            if matches!(
7144                cmd,
7145                ThreadCommand::PointerMotion { .. } | ThreadCommand::PointerRelativeMotion { .. }
7146            ) {
7147                note_pointer_motion(state);
7148            }
7149            handle_thread_command(state, cmd);
7150        }
7151        if had_input {
7152            state.last_input_at = Some(Instant::now());
7153        }
7154        state.command_rx = Some(rx);
7155        had_input
7156    }
7157
7158    /// One idle-tick service pass for committed clients while nothing captures:
7159    /// frame callbacks unblock vsynced clients, requested presentation feedback
7160    /// is discarded (nothing presents), and the renderer's import cache is
7161    /// pruned since no render will do it. Shared by the frame timer's idle
7162    /// branch and the input wake handler, which must not let a fresh keypress
7163    /// wait out an already-armed long idle deadline.
7164    fn send_idle_frame_callbacks(state: &mut AppState) {
7165        let time = state.clock.now();
7166        for node in state.output_nodes.iter().filter(|n| n.owner.is_none()) {
7167            let mut feedback = OutputPresentationFeedback::new(&node.output);
7168            for window in state.space.elements_for_output(&node.output) {
7169                window.send_frame(&node.output, time, Some(Duration::ZERO), |_, _| {
7170                    Some(node.output.clone())
7171                });
7172                window.take_presentation_feedback(
7173                    &mut feedback,
7174                    |_, _| Some(node.output.clone()),
7175                    |_, _| wp_presentation_feedback::Kind::empty(),
7176                );
7177            }
7178            for layer in layer_map_for_output(&node.output).layers() {
7179                layer.send_frame(&node.output, time, Some(Duration::ZERO), |_, _| {
7180                    Some(node.output.clone())
7181                });
7182                layer.take_presentation_feedback(
7183                    &mut feedback,
7184                    |_, _| Some(node.output.clone()),
7185                    |_, _| wp_presentation_feedback::Kind::empty(),
7186                );
7187            }
7188            feedback.discarded();
7189        }
7190        if let Some(output) = state.primary_output() {
7191            send_cursor_frame(state, output, time);
7192        }
7193        if let Some(renderer) = state.gles_renderer.as_mut() {
7194            let _ = renderer.cleanup_texture_cache();
7195        }
7196    }
7197
7198    fn handle_thread_command(state: &mut AppState, cmd: ThreadCommand) {
7199        match cmd {
7200            ThreadCommand::StartCapture {
7201                display_id,
7202                callback,
7203                settings,
7204            } => {
7205                start_capture_on_display(state, display_id, callback, settings);
7206            }
7207            ThreadCommand::StopCapture { display_id } => {
7208                // Cursor and clipboard sinks deliberately SURVIVE StopCapture:
7209                // captures cycle on client disconnects and setting restarts, and a
7210                // copy or cursor change during that gap must still reach the consumer.
7211                // SHUTDOWN gates every use against a consumer that is gone.
7212                stop_capture_on_display(state, display_id);
7213            }
7214            ThreadCommand::CreateOutput {
7215                id,
7216                width,
7217                height,
7218                x,
7219                y,
7220                scale,
7221                reply,
7222            } => {
7223                let _ = reply.send(create_output_on(state, id, width, height, x, y, scale));
7224            }
7225            ThreadCommand::CreateView {
7226                id,
7227                owner,
7228                x,
7229                y,
7230                width,
7231                height,
7232                reply,
7233            } => {
7234                let _ = reply.send(create_view_on(state, id, owner, x, y, width, height));
7235            }
7236            ThreadCommand::ResizeOutput {
7237                id,
7238                width,
7239                height,
7240                scale,
7241                reply,
7242            } => {
7243                let _ = reply.send(resize_output_on(state, id, width, height, scale));
7244            }
7245            ThreadCommand::DestroyOutput { id, reply } => {
7246                let _ = reply.send(destroy_output_on(state, id));
7247            }
7248            ThreadCommand::OutputCapacity { reply } => {
7249                let _ = reply.send(state.host.as_ref().map_or(-1, |h| h.output_count() as i64));
7250            }
7251            ThreadCommand::RepositionOutput { id, x, y, reply } => {
7252                let _ = reply.send(reposition_output_on(state, id, x, y));
7253            }
7254            ThreadCommand::ListOutputs { reply } => {
7255                let list = state
7256                    .output_nodes
7257                    .iter()
7258                    .map(|n| {
7259                        // A view's size is its own rectangle, not the screen it is
7260                        // cut from, which is the size a caller lays displays out by.
7261                        let (w, h) = if n.owner.is_some() {
7262                            n.capture
7263                                .as_ref()
7264                                .map(|c| (c.settings.width, c.settings.height))
7265                                .unwrap_or(n.view_size)
7266                        } else {
7267                            n.output
7268                                .current_mode()
7269                                .map(|m| (m.size.w, m.size.h))
7270                                .unwrap_or((0, 0))
7271                        };
7272                        (
7273                            n.id,
7274                            n.pos.0,
7275                            n.pos.1,
7276                            w,
7277                            h,
7278                            n.output.current_scale().fractional_scale(),
7279                            n.capture.is_some(),
7280                        )
7281                    })
7282                    .collect();
7283                let _ = reply.send(list);
7284            }
7285            ThreadCommand::MoveWindowToOutput {
7286                window_id,
7287                output_id,
7288                reply,
7289            } => {
7290                let window = state
7291                    .space
7292                    .elements()
7293                    .find(|w| {
7294                        wayland::frontend::window_meta(w)
7295                            .map(|m| m.id == window_id)
7296                            .unwrap_or(false)
7297                    })
7298                    .cloned();
7299                let ok = match window {
7300                    Some(w) => state.place_window_on_output(&w, output_id),
7301                    None => false,
7302                };
7303                let _ = reply.send(ok);
7304            }
7305            ThreadCommand::DesktopView { reply } => {
7306                let _ = reply.send(wayland::wm::view(state));
7307            }
7308            ThreadCommand::WindowRequest { id, request, reply } => {
7309                let known = wayland::wm::rect_of(state, id).is_some();
7310                let _ = reply.send(known && wayland::wm::request(state, id, request));
7311            }
7312            ThreadCommand::ListWindows { reply } => {
7313                use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
7314                let mut list = Vec::new();
7315                for window in state.space.elements() {
7316                    let Some(meta) = wayland::frontend::window_meta(window) else {
7317                        continue;
7318                    };
7319                    let (title, app_id) = window
7320                        .toplevel()
7321                        .map(|tl| {
7322                            with_states(tl.wl_surface(), |states| {
7323                                states
7324                                    .data_map
7325                                    .get::<XdgToplevelSurfaceData>()
7326                                    .map(|d| {
7327                                        let a = d.lock().unwrap();
7328                                        (
7329                                            a.title.clone().unwrap_or_default(),
7330                                            a.app_id.clone().unwrap_or_default(),
7331                                        )
7332                                    })
7333                                    .unwrap_or_default()
7334                            })
7335                        })
7336                        .unwrap_or_default();
7337                    list.push((
7338                        meta.id,
7339                        title,
7340                        app_id,
7341                        meta.output.load(Ordering::Relaxed),
7342                        meta.parked.load(Ordering::Relaxed),
7343                    ));
7344                }
7345                let _ = reply.send(list);
7346            }
7347            ThreadCommand::InstallWindowPolicy(policy) => {
7348                wayland::wm::install(state, policy);
7349            }
7350            ThreadCommand::StartXwayland { notify } => {
7351                let handle = state.loop_handle.clone();
7352                wayland::xwm::start(state, handle, notify);
7353            }
7354            ThreadCommand::SetClipboardCallback(cb) => {
7355                state.clipboard_callback = Some(cb);
7356                // Re-stage a read of the CURRENT selection so a copy made before this
7357                // callback was (re)armed is delivered rather than lost; the post-dispatch
7358                // drain performs the read. An empty or compositor-owned selection stages
7359                // nothing, since there is no copy to deliver.
7360                state.pending_clipboard_read = (!state.current_selection_mimes.is_empty())
7361                    .then(|| state.current_selection_mimes.clone());
7362            }
7363            ThreadCommand::SetClipboard { entries } => {
7364                // Every text alias is offered once, for the first text entry.
7365                let mut mimes: Vec<String> = Vec::new();
7366                for (mime, _) in &entries {
7367                    if !mime.starts_with("text/plain") {
7368                        mimes.push(mime.clone());
7369                    } else if !mimes.iter().any(|m| m == "TEXT") {
7370                        mimes.extend(
7371                            [
7372                                "text/plain;charset=utf-8",
7373                                "UTF8_STRING",
7374                                "text/plain",
7375                                "STRING",
7376                                "TEXT",
7377                            ]
7378                            .iter()
7379                            .map(|s| s.to_string()),
7380                        );
7381                    }
7382                }
7383                let payload = std::sync::Arc::new(entries);
7384                smithay::wayland::selection::data_device::set_data_device_selection(
7385                    &state.dh,
7386                    &state.seat,
7387                    mimes.clone(),
7388                    payload.clone(),
7389                );
7390                // Middle-click parity with the X11 clipboard bridge: the same
7391                // offer backs the primary selection too.
7392                smithay::wayland::selection::primary_selection::set_primary_selection(
7393                    &state.dh,
7394                    &state.seat,
7395                    mimes,
7396                    payload,
7397                );
7398                // The selection is compositor-owned now; a later SetClipboardCallback
7399                // must not try to re-read a client source that no longer holds it.
7400                state.current_selection_mimes.clear();
7401            }
7402            ThreadCommand::SetCursorCallback(cb) => {
7403                state.cursor_callback_set = cb.is_some();
7404                let _ = state.cursor_tx.send(CursorJob::SetCallback(cb));
7405                if state.cursor_callback_set {
7406                    // With no client cursor yet the default theme sprite stands in, the
7407                    // same one the render path draws for None, so a consumer registering
7408                    // before the first client cursor event still sees a pointer.
7409                    let icon = state
7410                        .current_cursor_icon
7411                        .clone()
7412                        .unwrap_or(CursorImageStatus::Named(Default::default()));
7413                    state.send_cursor_image(&icon);
7414                }
7415            }
7416            ThreadCommand::KeyboardKeys { events } => {
7417                for (scancode, key_state_val) in events {
7418                    if let Some(host) = state.host.as_ref() {
7419                        host.key(scancode, key_state_val > 0);
7420                        continue;
7421                    }
7422                    let key_state = if key_state_val > 0 {
7423                        KeyState::Pressed
7424                    } else {
7425                        KeyState::Released
7426                    };
7427                    let serial = next_serial();
7428                    let time = wayland_time();
7429                    if let Some(keyboard) = state.seat.get_keyboard() {
7430                        let pressed = key_state == KeyState::Pressed;
7431                        keyboard.input(
7432                            state,
7433                            Keycode::new(scancode),
7434                            key_state,
7435                            serial,
7436                            time,
7437                            |state, _, handle| wayland::wm::key_filter(state, handle, pressed),
7438                        );
7439                    }
7440                }
7441            }
7442            ThreadCommand::KeyboardKey {
7443                scancode,
7444                state: key_state_val,
7445            } => {
7446                if let Some(host) = state.host.as_ref() {
7447                    host.key(scancode, key_state_val > 0);
7448                    return;
7449                }
7450                let key_state = if key_state_val > 0 {
7451                    KeyState::Pressed
7452                } else {
7453                    KeyState::Released
7454                };
7455                let serial = next_serial();
7456                let time = wayland_time();
7457                if let Some(keyboard) = state.seat.get_keyboard() {
7458                    let pressed = key_state == KeyState::Pressed;
7459                    keyboard.input(
7460                        state,
7461                        Keycode::new(scancode),
7462                        key_state,
7463                        serial,
7464                        time,
7465                        |state, _, handle| wayland::wm::key_filter(state, handle, pressed),
7466                    );
7467                }
7468            }
7469            ThreadCommand::SetKeymapString(text) => {
7470                // rebuild_base rejects a string that will not compile without touching
7471                // the policy, so the seat keymap survives a bad one either way.
7472                if state.keymap_policy.rebuild_base(text) {
7473                    state.apply_keymap_policy();
7474                } else {
7475                    eprintln!(
7476                        "[Wayland] set_keymap_string: keymap failed to compile; keeping current keymap."
7477                    );
7478                }
7479            }
7480            ThreadCommand::SetXkbLayout {
7481                rules,
7482                model,
7483                layout,
7484                variant,
7485                options,
7486                reply,
7487            } => {
7488                match crate::wayland::keymap::compile_rmlvo(
7489                    &rules, &model, &layout, &variant, &options,
7490                ) {
7491                    Some(text) => {
7492                        state.keymap_policy.rebuild_base(text);
7493                        state.apply_keymap_policy();
7494                        let _ = reply.send(true);
7495                    }
7496                    None => {
7497                        eprintln!(
7498                            "[Wayland] set_xkb_layout: RMLVO ({rules:?}, {model:?}, {layout:?}, {variant:?}, {options:?}) failed to compile."
7499                        );
7500                        let _ = reply.send(false);
7501                    }
7502                }
7503            }
7504            ThreadCommand::BindKeysyms { keysyms, reply } => {
7505                let _ = reply.send(state.bind_keysyms(&keysyms));
7506            }
7507            ThreadCommand::SetKeymapOverlay { binds } => {
7508                if state.keymap_policy.has_base() {
7509                    state.keymap_policy.set_manual_overlay(&binds);
7510                    state.apply_keymap_policy();
7511                } else {
7512                    eprintln!("[Wayland] set_keymap_overlay: no base keymap to splice onto.");
7513                }
7514            }
7515            ThreadCommand::GetKeyboardState { reply } => {
7516                let (pressed, mods) = state
7517                    .seat
7518                    .get_keyboard()
7519                    .map(|kb| {
7520                        let pressed: Vec<u32> = kb.pressed_keys().iter().map(|c| c.raw()).collect();
7521                        let m = kb.modifier_state();
7522                        let mask = u32::from(m.ctrl)
7523                            | u32::from(m.shift) << 1
7524                            | u32::from(m.alt) << 2
7525                            | u32::from(m.logo) << 3
7526                            | u32::from(m.caps_lock) << 4
7527                            | u32::from(m.num_lock) << 5
7528                            | u32::from(m.iso_level3_shift) << 6
7529                            | u32::from(m.iso_level5_shift) << 7;
7530                        (pressed, mask)
7531                    })
7532                    .unwrap_or_default();
7533                let _ = reply.send((pressed, mods));
7534            }
7535            ThreadCommand::Barrier { reply } => {
7536                // The shutdown path fences on this after its StopCaptures:
7537                // joining the reaped threads before acknowledging means
7538                // nothing that can attach to Python survives past the
7539                // fence, so the interpreter never finalizes under a live
7540                // callback. Bounded — discard flags are up and senders
7541                // dropped, so each chain exits after at most its
7542                // in-flight callback.
7543                for join in state.encode_reaper.drain(..) {
7544                    let _ = join.join();
7545                }
7546                for join in state.deliver_reaper.drain(..) {
7547                    let _ = join.join();
7548                }
7549                let _ = reply.send(());
7550            }
7551            ThreadCommand::GetXkbKeymap { reply } => {
7552                let mut keymap_str = String::new();
7553                if let Some(keyboard) = state.seat.get_keyboard() {
7554                    keymap_str =
7555                        keyboard.with_xkb_state(state, |context| match context.xkb().lock() {
7556                            Ok(guard) => {
7557                                let keymap = unsafe { guard.keymap() };
7558                                keymap.get_as_string(
7559                                    smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1,
7560                                )
7561                            }
7562                            Err(_) => String::new(),
7563                        });
7564                }
7565                let _ = reply.send(keymap_str);
7566            }
7567            ThreadCommand::PointerMotion { x, y, motion } => {
7568                state.motion_applied = state.motion_applied.max(motion);
7569                if let Some(host) = state.host.as_ref() {
7570                    crate::wayland::pwcapture::note_host_motion(motion);
7571                    host.pointer_motion_abs(x, y);
7572                    return;
7573                }
7574                let serial = next_serial();
7575                let time = wayland_time();
7576                // (x, y) are physical union-layout coordinates: each output occupies
7577                // the physical rectangle at its layout offset, and the point maps
7578                // through the CONTAINING output's scale (clamped into the nearest
7579                // output when outside all of them).
7580                let p = state.layout_physical_to_logical(x, y);
7581
7582                if let Some(pointer) = state.seat.get_pointer() {
7583                    let under = state.pointer_target_under(p);
7584                    if wayland::wm::pointer(
7585                        state,
7586                        crate::wm::PointerKind::Motion,
7587                        p,
7588                        wayland::wm::under_of(state, &under),
7589                    ) == crate::wm::Verdict::Consumed
7590                    {
7591                        return;
7592                    }
7593
7594                    state.release_grab_across_screens(&pointer, &under, serial, time);
7595                    let entered = pointer.current_focus() != under.as_ref().map(|(t, _)| t.clone());
7596                    pointer.motion(
7597                        state,
7598                        under.clone(),
7599                        &MotionEvent {
7600                            location: p,
7601                            serial,
7602                            time,
7603                        },
7604                    );
7605                    // A nested wlroots session takes its cursor position from motion
7606                    // events alone, so the position the enter carried must be repeated
7607                    // or its cursor stays behind until the next move -- and a button
7608                    // landing first presses at that stale spot.
7609                    if entered && under.is_some() {
7610                        pointer.motion(
7611                            state,
7612                            under.clone(),
7613                            &MotionEvent {
7614                                location: p,
7615                                serial: next_serial(),
7616                                time,
7617                            },
7618                        );
7619                    }
7620                    pointer.frame(state);
7621                    state.activate_constraint_under(&pointer, &under, p);
7622                }
7623            }
7624            ThreadCommand::PointerRelativeMotion { dx, dy, motion } => {
7625                state.motion_applied = state.motion_applied.max(motion);
7626                if let Some(host) = state.host.as_ref() {
7627                    crate::wayland::pwcapture::note_host_motion(motion);
7628                    host.pointer_motion_rel(dx, dy);
7629                    return;
7630                }
7631                // A nested KWin drops the delta the seat's relative_motion carries;
7632                // its fake-input device takes it instead, ahead of the seat's
7633                // absolute move below. KWin sets its pointer from that move rather
7634                // than adding to it, so the two agree when aligned, and a KWin that
7635                // moved its pointer on its own (a screen change) is realigned by the
7636                // next delta. The seat's relative_motion is then withheld, since a
7637                // KWin that did bind the seat's relative pointer would count it twice.
7638                let via_fake_input = crate::wayland::ficlient::pointer_motion_rel(dx, dy);
7639                let time = wayland_time();
7640                let serial = next_serial();
7641
7642                if let Some(pointer) = state.seat.get_pointer() {
7643                    // A delta adds to where the policy last took the pointer while it takes
7644                    // it: under a client's grab (the press that starts a frame drag) the seat
7645                    // is not moved, and adding to the seat would start every delta of the
7646                    // drag from where it began.
7647                    let current_pos =
7648                        wayland::wm::judged_location(state, pointer.current_location());
7649                    let event = RelativeMotionEvent {
7650                        time,
7651                        delta: (dx, dy).into(),
7652                        delta_unaccel: (dx, dy).into(),
7653                    };
7654                    let resting = state
7655                        .space
7656                        .element_under(current_pos)
7657                        .map(|(window, loc)| (FocusTarget::Window(window.clone()), loc.to_f64()));
7658                    // A lock's holder is given the delta and nothing else: moved as well, it
7659                    // reads the move a second time off the position it is handed, and a game
7660                    // turns twice as far as the hand did.
7661                    if state.pointer_locked(&pointer, &resting, current_pos) {
7662                        if !via_fake_input {
7663                            pointer.relative_motion(state, resting, &event);
7664                        }
7665                        pointer.frame(state);
7666                        return;
7667                    }
7668                    let new_pos =
7669                        state.clamp_logical((current_pos.x + dx, current_pos.y + dy).into());
7670
7671                    let under = state.pointer_target_under(new_pos);
7672                    if wayland::wm::pointer(
7673                        state,
7674                        crate::wm::PointerKind::Motion,
7675                        new_pos,
7676                        wayland::wm::under_of(state, &under),
7677                    ) == crate::wm::Verdict::Consumed
7678                    {
7679                        return;
7680                    }
7681
7682                    state.release_grab_across_screens(&pointer, &under, serial, time);
7683                    let entered = pointer.current_focus() != under.as_ref().map(|(t, _)| t.clone());
7684                    pointer.motion(
7685                        state,
7686                        under.clone(),
7687                        &MotionEvent {
7688                            location: new_pos,
7689                            serial,
7690                            time,
7691                        },
7692                    );
7693                    // Same repeat as the absolute arm: an entered nested session
7694                    // learns the position only from a motion event.
7695                    if entered && under.is_some() {
7696                        pointer.motion(
7697                            state,
7698                            under.clone(),
7699                            &MotionEvent {
7700                                location: new_pos,
7701                                serial: next_serial(),
7702                                time,
7703                            },
7704                        );
7705                    }
7706
7707                    if !via_fake_input {
7708                        pointer.relative_motion(state, under.clone(), &event);
7709                    }
7710
7711                    pointer.frame(state);
7712                    state.activate_constraint_under(&pointer, &under, new_pos);
7713                }
7714            }
7715            ThreadCommand::PointerButton {
7716                btn,
7717                state: btn_state_val,
7718            } => {
7719                if let Some(host) = state.host.as_ref() {
7720                    host.pointer_button(btn, btn_state_val > 0);
7721                    return;
7722                }
7723                let serial = next_serial();
7724                let time = wayland_time();
7725                let button_state = if btn_state_val > 0 {
7726                    smithay::backend::input::ButtonState::Pressed
7727                } else {
7728                    smithay::backend::input::ButtonState::Released
7729                };
7730
7731                state.refocus_pointer();
7732                if let Some(pointer) = state.seat.get_pointer() {
7733                    let pos = wayland::wm::judged_location(state, pointer.current_location());
7734                    let under = state.pointer_target_under(pos);
7735                    let kind = crate::wm::PointerKind::Button {
7736                        button: btn,
7737                        pressed: btn_state_val > 0,
7738                    };
7739                    if wayland::wm::pointer(state, kind, pos, wayland::wm::under_of(state, &under))
7740                        == crate::wm::Verdict::Consumed
7741                    {
7742                        return;
7743                    }
7744                    if button_state == smithay::backend::input::ButtonState::Pressed
7745                        && !wayland::wm::active(state)
7746                    {
7747                        let target_window = state.space.element_under(pos).map(|(w, _)| w.clone());
7748
7749                        if let Some(window) = target_window {
7750                            state.space.raise_element(&window, true);
7751                            if let Some(keyboard) = state.seat.get_keyboard() {
7752                                keyboard.set_focus(
7753                                    state,
7754                                    Some(FocusTarget::Window(window)),
7755                                    serial,
7756                                );
7757                            }
7758                        }
7759                    }
7760                    let button = btn;
7761                    pointer.button(
7762                        state,
7763                        &ButtonEvent {
7764                            button,
7765                            state: button_state,
7766                            serial,
7767                            time,
7768                        },
7769                    );
7770                    pointer.frame(state);
7771                }
7772            }
7773            ThreadCommand::PointerAxis { x, y } => {
7774                if let Some(host) = state.host.as_ref() {
7775                    host.pointer_axis(x, y);
7776                    return;
7777                }
7778                let time = wayland_time();
7779                state.refocus_pointer();
7780                if let Some(pointer) = state.seat.get_pointer() {
7781                    let pos = wayland::wm::judged_location(state, pointer.current_location());
7782                    let under = state.pointer_target_under(pos);
7783                    let kind = crate::wm::PointerKind::Axis {
7784                        horizontal: x,
7785                        vertical: y,
7786                    };
7787                    if wayland::wm::pointer(state, kind, pos, wayland::wm::under_of(state, &under))
7788                        == crate::wm::Verdict::Consumed
7789                    {
7790                        return;
7791                    }
7792                    let mut frame = AxisFrame::new(time).source(AxisSource::Wheel);
7793
7794                    if x != 0.0 {
7795                        frame = frame
7796                            .value(Axis::Horizontal, x)
7797                            .v120(Axis::Horizontal, (x * SCROLL_V120_PER_UNIT) as i32);
7798                    }
7799
7800                    if y != 0.0 {
7801                        frame = frame
7802                            .value(Axis::Vertical, y)
7803                            .v120(Axis::Vertical, (y * SCROLL_V120_PER_UNIT) as i32);
7804                    }
7805
7806                    if x != 0.0 || y != 0.0 {
7807                        pointer.axis(state, frame);
7808                        pointer.frame(state);
7809                    }
7810                }
7811            }
7812            ThreadCommand::PointerScroll { dx, dy } => {
7813                if let Some(host) = state.host.as_ref() {
7814                    host.pointer_scroll(dx, dy);
7815                    return;
7816                }
7817                let time = wayland_time();
7818                state.refocus_pointer();
7819                if let Some(pointer) = state.seat.get_pointer()
7820                    && (dx != 0.0 || dy != 0.0)
7821                {
7822                    let pos = wayland::wm::judged_location(state, pointer.current_location());
7823                    let under = state.pointer_target_under(pos);
7824                    let kind = crate::wm::PointerKind::Axis {
7825                        horizontal: dx,
7826                        vertical: dy,
7827                    };
7828                    if wayland::wm::pointer(state, kind, pos, wayland::wm::under_of(state, &under))
7829                        == crate::wm::Verdict::Consumed
7830                    {
7831                        return;
7832                    }
7833                    let (_, _, scale) = state.layout_logical_to_physical_scaled(pos);
7834                    let mut frame = AxisFrame::new(time).source(AxisSource::Finger);
7835                    if dx != 0.0 {
7836                        frame = frame.value(Axis::Horizontal, dx / scale);
7837                    }
7838                    if dy != 0.0 {
7839                        frame = frame.value(Axis::Vertical, dy / scale);
7840                    }
7841                    pointer.axis(state, frame);
7842                    pointer.frame(state);
7843                }
7844            }
7845            ThreadCommand::PointerScrollEnd => {
7846                if let Some(host) = state.host.as_ref() {
7847                    host.pointer_scroll_end();
7848                    return;
7849                }
7850                let time = wayland_time();
7851                if let Some(pointer) = state.seat.get_pointer() {
7852                    let frame = AxisFrame::new(time)
7853                        .source(AxisSource::Finger)
7854                        .stop(Axis::Horizontal)
7855                        .stop(Axis::Vertical);
7856                    pointer.axis(state, frame);
7857                    pointer.frame(state);
7858                }
7859            }
7860            ThreadCommand::UpdateCursorConfig {
7861                render_on_framebuffer,
7862            } => {
7863                state.render_cursor_on_framebuffer = render_on_framebuffer;
7864                if let Some(host) = state.host.as_ref() {
7865                    host.set_cursor_painting(render_on_framebuffer);
7866                }
7867            }
7868            ThreadCommand::SetCursorSize { size, reply } => {
7869                if size <= 0 {
7870                    let _ = reply.send(false);
7871                } else {
7872                    state.cursor_helper = Cursor::load(size);
7873                    let _ = state.cursor_tx.send(CursorJob::SetSize(size));
7874                    // The burned-in cursor changed size; force a repaint everywhere so
7875                    // a static screen doesn't keep showing the old sprite.
7876                    for node in state.output_nodes.iter_mut() {
7877                        if let Some(cap) = node.capture.as_mut() {
7878                            cap.needs_full_render = true;
7879                        }
7880                    }
7881                    let _ = reply.send(true);
7882                }
7883            }
7884            ThreadCommand::RequestIdr { display_id } => {
7885                if let Some(idx) = state.node_idx_for_id(display_id)
7886                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
7887                {
7888                    cap.request_idr();
7889                }
7890            }
7891            ThreadCommand::InvalidateReference {
7892                display_id,
7893                frame_id,
7894                withheld,
7895            } => {
7896                if let Some(idx) = state.node_idx_for_id(display_id)
7897                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
7898                {
7899                    cap.invalidate_reference(frame_id, withheld);
7900                }
7901            }
7902            ThreadCommand::AcknowledgeReference {
7903                display_id,
7904                frame_id,
7905                held,
7906            } => {
7907                if let Some(idx) = state.node_idx_for_id(display_id)
7908                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
7909                {
7910                    cap.acknowledge_reference(frame_id, held);
7911                }
7912            }
7913            ThreadCommand::UpdateLinkRate {
7914                display_id,
7915                link_kbps,
7916            } => {
7917                if let Some(idx) = state.node_idx_for_id(display_id)
7918                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
7919                {
7920                    cap.settings.video_link_kbps = link_kbps;
7921                    cap.encode_controls
7922                        .link_kbps
7923                        .store(link_kbps, Ordering::Relaxed);
7924                    if display_id == 0 {
7925                        state.settings.video_link_kbps = link_kbps;
7926                    }
7927                }
7928            }
7929            ThreadCommand::UpdateRate {
7930                display_id,
7931                bitrate_kbps,
7932                vbv_multiplier,
7933                fps,
7934            } => {
7935                if let Some(idx) = state.node_idx_for_id(display_id)
7936                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
7937                {
7938                    if let Some(b) = bitrate_kbps {
7939                        cap.settings.video_bitrate_kbps = b;
7940                    }
7941                    if let Some(v) = vbv_multiplier {
7942                        cap.settings.video_vbv_multiplier = v;
7943                    }
7944                    if let Some(f) = fps
7945                        && f > 0.0
7946                    {
7947                        cap.settings.target_fps = f;
7948                        if let Some(host) = state.host.as_ref() {
7949                            host.set_fps(display_id, f);
7950                        }
7951                    }
7952                    let c = &cap.encode_controls;
7953                    c.bitrate_kbps
7954                        .store(cap.settings.video_bitrate_kbps, Ordering::Relaxed);
7955                    c.vbv_mult_milli.store(
7956                        (cap.settings.video_vbv_multiplier * 1000.0).round() as i32,
7957                        Ordering::Relaxed,
7958                    );
7959                    c.fps_bits.store(
7960                        cap.settings.target_fps.max(1.0).to_bits(),
7961                        Ordering::Relaxed,
7962                    );
7963                    c.rate_dirty.store(true, Ordering::Release);
7964                    if display_id == 0 {
7965                        state.settings.video_bitrate_kbps = cap.settings.video_bitrate_kbps;
7966                        state.settings.video_vbv_multiplier = cap.settings.video_vbv_multiplier;
7967                        state.settings.target_fps = cap.settings.target_fps;
7968                    }
7969                }
7970            }
7971            ThreadCommand::UpdateTunables {
7972                display_id,
7973                tunables: t,
7974            } => {
7975                state.render_cursor_on_framebuffer = t.capture_cursor;
7976                if let Some(host) = state.host.as_ref() {
7977                    host.set_cursor_painting(t.capture_cursor);
7978                }
7979                let _ = state
7980                    .cursor_tx
7981                    .send(CursorJob::SetSizeCap(t.cursor_size_cap));
7982                if display_id == 0 {
7983                    t.apply_to(&mut state.settings);
7984                }
7985                if let Some(idx) = state.node_idx_for_id(display_id)
7986                    && let Some(cap) = state.output_nodes[idx].capture.as_mut()
7987                {
7988                    t.apply_to(&mut cap.settings);
7989                    *cap.encode_controls.tunables.lock().unwrap() = Some(t);
7990                    cap.encode_controls
7991                        .tunables_dirty
7992                        .store(true, Ordering::Release);
7993                }
7994            }
7995            ThreadCommand::CuScreenshot {
7996                display_id,
7997                resp,
7998                canceled,
7999                deadline,
8000            } => {
8001                if state.node_idx_for_id(display_id).is_some() {
8002                    state.pending_screenshots.enqueue(
8003                        wayland::frontend::ScreenshotRequest {
8004                            display_id,
8005                            resp,
8006                            canceled,
8007                            deadline,
8008                        },
8009                        Instant::now(),
8010                    );
8011                } else {
8012                    let _ = resp.send(Err(format!("Unknown display: {display_id}")));
8013                }
8014            }
8015            ThreadCommand::CuCursorPosition { resp } => {
8016                let pos = state
8017                    .pointer_shown_at()
8018                    .unwrap_or_else(|| (0.0f64, 0.0f64).into());
8019                let _ = resp.send(state.layout_logical_to_physical(pos));
8020            }
8021            ThreadCommand::CuGetInfo { display_id, resp } => {
8022                // A host-capture start whose mode the host has not answered yet:
8023                // the read parks until it has (the reply then carries the size
8024                // actually captured), which is what makes it a barrier.
8025                match state.host_layout_pending.get_mut(&display_id) {
8026                    Some(p) => p.geometry_waiters.push(resp),
8027                    None => {
8028                        let _ = resp.send(realized_geometry(state, display_id));
8029                    }
8030                }
8031            }
8032        }
8033    }
8034
8035    state.command_rx = Some(command_rx);
8036    event_loop
8037        .handle()
8038        .insert_source(wake_rx, |_, _, state| {
8039            let had_input = drain_thread_commands(state);
8040            if wayland::wm::tick_due(state, Instant::now()) {
8041                wayland::wm::tick(state, Instant::now());
8042            }
8043            // Input landing while the frame timer sits on its long idle
8044            // deadline must not wait it out: service the frame callbacks now
8045            // (at most at frame pace) so the app's echo repaint starts with
8046            // the keypress; the timer's own next fire re-arms the short pace.
8047            if state.frame_idle_long
8048                && state
8049                    .last_input_at
8050                    .is_some_and(|t| t.elapsed() < Duration::from_millis(50))
8051                && state
8052                    .last_idle_service_at
8053                    .is_none_or(|t| t.elapsed() >= Duration::from_millis(16))
8054            {
8055                state.last_idle_service_at = Some(Instant::now());
8056                send_idle_frame_callbacks(state);
8057            }
8058            // Input may pull a capture's due frame forward to its own phase (see
8059            // FramePace). Not under host capture: a pull there would re-encode the frame
8060            // the host already delivered, so the host's own frames wake the render instead.
8061            if had_input
8062                && state.host.is_none()
8063                && state.output_nodes.iter().any(|n| n.capture.is_some())
8064            {
8065                // A capture whose period a frame already took shows the input when the next opens.
8066                for node in &mut state.output_nodes {
8067                    if let Some(cap) = node.capture.as_mut() {
8068                        cap.change_pending = true;
8069                    }
8070                }
8071                state.space.refresh();
8072                render_pass(state, TickTrigger::Input);
8073            } else if !state.pending_screenshots.is_empty()
8074                && state.output_nodes.iter().all(|n| n.capture.is_none())
8075            {
8076                // With no active capture, serve the screenshot without the idle wait.
8077                // Active captures retain their frame cadence; input was drained above.
8078                state.space.refresh();
8079                render_pass(state, TickTrigger::Timer);
8080            }
8081        })
8082        .unwrap();
8083
8084    event_loop
8085        .handle()
8086        .insert_source(host_frame_rx, |event, _, state| {
8087            // A host frame landed for one output: its display renders now, at most at frame
8088            // pace (FramePace), rather than on a timer tick up to a period away. A wake the
8089            // timer already served finds nothing queued and does nothing.
8090            let smithay::reexports::calloop::channel::Event::Msg(index) = event else {
8091                return;
8092            };
8093            let Some(id) = state
8094                .host
8095                .as_ref()
8096                .filter(|h| h.has_queued_frame(index))
8097                .and_then(|h| h.display_for_output(index))
8098            else {
8099                return;
8100            };
8101            // A host frame landing in a period a frame already took waits for the next to open.
8102            if let Some(cap) = state
8103                .output_nodes
8104                .iter_mut()
8105                .find(|n| n.id == id)
8106                .and_then(|n| n.capture.as_mut())
8107            {
8108                cap.change_pending = true;
8109            }
8110            state.space.refresh();
8111            render_display(state, id, TickTrigger::HostFrame);
8112        })
8113        .unwrap();
8114
8115    let source = match ListeningSocketSource::new_auto() {
8116        Ok(s) => s,
8117        Err(e) => {
8118            eprintln!(
8119                "[Wayland] compositor thread aborting: could not bind a wayland-N socket (XDG_RUNTIME_DIR unset/full?): {e}"
8120            );
8121            return;
8122        }
8123    };
8124    let socket_name = source.socket_name().to_string_lossy().into_owned();
8125    println!("[Wayland] Socket listening on: {:?}", socket_name);
8126    // Writing the process environment races any concurrent getenv, which is why
8127    // it is unsafe: nothing here can stop a thread already running from reading
8128    // it. This one write happens during compositor bring-up, before any capture
8129    // or encoder thread exists, and the value is what children of this process
8130    // and the backend probe below need to find the compositor.
8131    unsafe { std::env::set_var("WAYLAND_DISPLAY", &socket_name) };
8132    publish_socket_name(&socket_name);
8133
8134    event_loop
8135        .handle()
8136        .insert_source(source, |client_stream, _, state| {
8137            if let Err(err) = state
8138                .dh
8139                .insert_client(client_stream, Arc::new(ClientState::default()))
8140            {
8141                eprintln!("Error adding wayland client: {:?}", err);
8142            }
8143        })
8144        .expect("Failed to init wayland socket source");
8145
8146    let timer = Timer::immediate();
8147    event_loop
8148        .handle()
8149        .insert_source(timer, move |_, _, state| {
8150            // Apply queued commands (input above all) BEFORE the render/encode work: a
8151            // command that raced the timer wakeup would otherwise wait out the whole tick.
8152            reap_dead_host(state);
8153            drain_thread_commands(state);
8154            reconcile_host_layouts(state);
8155            {
8156                let now = Instant::now();
8157                let handle = state.loop_handle.clone();
8158                wayland::xwm::maintain(state, &handle, now);
8159                if wayland::wm::tick_due(state, now) {
8160                    wayland::wm::tick(state, now);
8161                }
8162            }
8163            // Deliver and deferred encode threads of stopped captures are
8164            // joined only once they report finished (a plain atomic load), so
8165            // the tick never waits on one; whatever remains at shutdown, the
8166            // Barrier drain joins.
8167            let mut i = 0;
8168            while i < state.deliver_reaper.len() {
8169                if state.deliver_reaper[i].is_finished() {
8170                    let _ = state.deliver_reaper.swap_remove(i).join();
8171                } else {
8172                    i += 1;
8173                }
8174            }
8175            let mut i = 0;
8176            while i < state.encode_reaper.len() {
8177                if state.encode_reaper[i].is_finished() {
8178                    let _ = state.encode_reaper.swap_remove(i).join();
8179                } else {
8180                    i += 1;
8181                }
8182            }
8183            if state.screenshot_readbacks.has_idle(Instant::now())
8184                && let Some(renderer) = state.gles_renderer.as_mut()
8185            {
8186                let readbacks = &mut state.screenshot_readbacks;
8187                let _ = renderer.with_context(|gl| readbacks.reap_idle(gl, Instant::now()));
8188            }
8189            let loop_start_time = Instant::now();
8190            state.space.refresh();
8191
8192            let now = Instant::now();
8193            let elapsed = now.duration_since(state.last_log_time).as_secs_f64();
8194            if elapsed >= 1.0 {
8195                // Memory is read here rather than per tick: it is reported, not acted on,
8196                // and /proc plus a /dev/shm walk have no place in the render path.
8197                let mut mem: Option<(usize, u64)> = None;
8198                for node in &state.output_nodes {
8199                    let Some(cap) = node.capture.as_ref() else { continue };
8200                    let frames = cap.encode_stats.frames.swap(0, Ordering::Relaxed);
8201                    let stripes = cap.encode_stats.stripes.swap(0, Ordering::Relaxed);
8202                    if crate::log::debug_enabled() {
8203                        let actual_fps = f64::from(frames) / elapsed;
8204                        let stripes_per_sec = f64::from(stripes) / elapsed;
8205                        let mode_str = cap.encode_stats.desc.lock().unwrap().clone();
8206                        let n_stripes = cap.encode_stats.n_stripes.load(Ordering::Relaxed);
8207                        let (current_rss, shm_usage) = *mem
8208                            .get_or_insert_with(|| (get_process_rss_bytes(), get_shm_usage_bytes()));
8209
8210                        println!("Display: {} Res: {}x{} Mode: {} Stripes: {} EncFPS: {:.2} EncStripes/s: {:.2} Mem: {}MB SHM: {}MB",
8211                            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);
8212                    }
8213                }
8214                state.last_log_time = now;
8215            }
8216
8217            let any_capturing = state.output_nodes.iter().any(|n| n.capture.is_some());
8218            let any_copy_frame = state.copy_sessions.iter().any(|cs| cs.pending.is_some());
8219            state.pending_screenshots.prune(Instant::now());
8220            if !any_capturing && state.pending_screenshots.is_empty() && !any_copy_frame {
8221                // No render/encode work, but committed clients still need their
8222                // frame callbacks: a vsynced client (FIFO Vulkan present, games, a
8223                // nested compositor's own clients) otherwise blocks in its swap
8224                // until a viewer attaches — apps appear frozen whenever nobody is
8225                // watching.
8226                //
8227                // Slowly, though: at a viewing rate the clients draw at a viewing rate
8228                // too, and every frame they hand over is one that nothing renders,
8229                // encodes, or looks at. Unblocking them is the whole purpose here, and
8230                // that costs a callback every so often rather than sixty a second.
8231                state.last_idle_service_at = Some(Instant::now());
8232                send_idle_frame_callbacks(state);
8233                // A parked capture session is an attached consumer that may request a
8234                // frame at any moment: tick at frame pace so that request waits at most
8235                // one frame, not a quarter second. Input gets the same window: apps
8236                // pacing on frame callbacks see their echo promptly after a keypress,
8237                // or the idle rate alone turns typing into a 250ms stutter. The wake
8238                // handler covers the first keypress landing while the long deadline
8239                // is already armed.
8240                let post_input = state
8241                    .last_input_at
8242                    .is_some_and(|t| t.elapsed() < Duration::from_secs(1));
8243                let idle = if !state.copy_sessions.is_empty() || post_input {
8244                    Duration::from_millis(16)
8245                } else {
8246                    IDLE_FRAME_INTERVAL
8247                };
8248                let idle = match wayland::wm::next_wake(state) {
8249                    Some(at) => idle.min(at.saturating_duration_since(Instant::now()).max(Duration::from_millis(1))),
8250                    None => idle,
8251                };
8252                state.frame_idle_long = idle == IDLE_FRAME_INTERVAL;
8253                return TimeoutAction::ToDuration(idle);
8254            }
8255            state.frame_idle_long = false;
8256
8257            answer_frame_callbacks(state);
8258            serve_changes(state);
8259            let pool_busy = render_pass(state, TickTrigger::Timer);
8260            answer_frame_callbacks(state);
8261            if pool_busy {
8262                return TimeoutAction::ToDuration(Duration::from_millis(1));
8263            }
8264            // The next fire is the earliest capture's due time, so a frame input pulled
8265            // forward carries the cadence with it; with no capture (a screenshot or a
8266            // capture client alone) the primary's rate paces from this tick's start.
8267            let now = Instant::now();
8268            let next_due = state
8269                .output_nodes
8270                .iter()
8271                .filter_map(|n| n.capture.as_ref())
8272                .map(|cap| cap.pace.next_wake(capture_period(cap), now, cap.change_pending))
8273                .min()
8274                .unwrap_or(loop_start_time + frame_period(state.settings.target_fps));
8275            let next_due = match wayland::wm::next_wake(state) {
8276                Some(at) => next_due.min(at),
8277                None => next_due,
8278            };
8279            let wait = next_due.saturating_duration_since(now);
8280            // A zero wait is a change no render could take yet (a busy encoder): retry shortly
8281            // rather than spin. Shorter waits stand, a boundary being a fraction of a millisecond
8282            // away at high frame rates.
8283            TimeoutAction::ToDuration(if wait.is_zero() { Duration::from_millis(1) } else { wait })
8284        })
8285        .expect("Failed to init capture timer");
8286
8287    event_loop
8288        .handle()
8289        .insert_source(
8290            Generic::new(display, Interest::READ, Mode::Level),
8291            |_, display, _state| {
8292                // A single misbehaving client must not take the compositor (and every other
8293                // session) down with it.
8294                if let Err(e) = unsafe { display.get_mut().dispatch_clients(_state) } {
8295                    eprintln!("[Wayland] client dispatch error: {e:?}");
8296                }
8297                Ok(PostAction::Continue)
8298            },
8299        )
8300        .unwrap();
8301
8302    crate::computer_use::register_wayland_backend(command_tx, wake_tx);
8303    #[cfg(feature = "computer-use-server")]
8304    crate::computer_use::server::spawn_cu_from_env();
8305    crate::wayland::ficlient::arm(crate::computer_use::app_wayland_socket_path());
8306
8307    let ran = event_loop.run(None, &mut state, |state| {
8308        serve_changes(state);
8309        state.process_pending_clipboard_read();
8310        state.flush_pending_cursor();
8311        if state.host.is_none()
8312            && let Some(at) = state.pointer_shown_at()
8313        {
8314            let (x, y, scale) = state.layout_logical_to_physical_scaled(at);
8315            state
8316                .pointer_share
8317                .publish(crate::wayland::frontend::PointerAt {
8318                    x,
8319                    y,
8320                    scale,
8321                    motion: state.motion_applied,
8322                });
8323        }
8324        let _ = state.dh.flush_clients();
8325    });
8326    if let Err(e) = ran {
8327        eprintln!("[Wayland] compositor event loop ended: {e}");
8328    }
8329}
8330
8331/// The handle to the one long-lived compositor thread. Because calloop, EGL/GBM, and the
8332/// Wayland display are all thread-affine and the compositor has to keep running to serve its
8333/// clients even between captures, the backend cannot be a passive object that starts work on
8334/// demand: constructing it spawns that thread and it stays up for the process lifetime. The
8335/// struct itself is nothing but the command-channel sender used to drive that thread across
8336/// the boundary; `ensure_wayland_display` hands out the process-wide one.
8337pub struct Compositor {
8338    tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
8339    /// The compositor's pointer, as its thread last published it (`pointer_location`).
8340    pointer: Arc<crate::wayland::frontend::PointerShare>,
8341    /// The number the last motion sent was given.
8342    motions: AtomicU64,
8343    /// Wakes the calloop after each command send: the command channel itself is drained in
8344    /// place by the compositor thread (render tick and wake handler), not registered as its
8345    /// own source, so input never waits behind an in-flight render tick's timer wakeup.
8346    wake_tx: smithay::reexports::calloop::channel::Sender<()>,
8347}
8348
8349impl Compositor {
8350    fn send(&self, cmd: ThreadCommand) -> Result<(), String> {
8351        self.tx.send(cmd).map_err(|e| e.to_string())?;
8352        let _ = self.wake_tx.send(());
8353        Ok(())
8354    }
8355
8356    /// Register the consumer of cursor images; `None` withdraws it.
8357    pub fn set_cursor_sink(
8358        &self,
8359        sink: Option<Arc<dyn crate::sink::CursorSink>>,
8360    ) -> Result<(), Error> {
8361        self.send(ThreadCommand::SetCursorCallback(sink))
8362            .map_err(|e| Error::Compositor(format!("Failed to set cursor callback: {e}")))
8363    }
8364
8365    /// Install the window policy: from here on it decides placement, focus, stacking, frames,
8366    /// and bindings for every window.
8367    pub fn install_window_policy(
8368        &self,
8369        policy: Box<dyn crate::wm::WindowPolicy>,
8370    ) -> Result<(), Error> {
8371        self.send(ThreadCommand::InstallWindowPolicy(policy))
8372            .map_err(|e| Error::Compositor(format!("Failed to install the window policy: {e}")))
8373    }
8374
8375    /// Start Xwayland for X11 clients, managed with the Wayland ones; `notify` receives the
8376    /// display number once the server is up.
8377    pub fn start_xwayland(
8378        &self,
8379        notify: Option<std::sync::mpsc::Sender<u32>>,
8380    ) -> Result<(), Error> {
8381        self.send(ThreadCommand::StartXwayland { notify })
8382            .map_err(|e| Error::Compositor(format!("Failed to start Xwayland: {e}")))
8383    }
8384
8385    /// Register the consumer of what client applications copy to the clipboard.
8386    pub fn set_clipboard_sink(
8387        &self,
8388        sink: Arc<dyn crate::sink::ClipboardSink>,
8389    ) -> Result<(), Error> {
8390        self.send(ThreadCommand::SetClipboardCallback(sink))
8391            .map_err(|e| Error::Compositor(format!("Failed to set clipboard callback: {e}")))
8392    }
8393}
8394
8395impl Compositor {
8396    /// Construct the backend and spawn the long-lived compositor thread, handing it the
8397    /// strongest scheduling edge (nice -15) because it drives the calloop, input dispatch, the render
8398    /// loop, and — on the zero-copy path — the encode itself, all on this single thread, so any
8399    /// scheduling starvation here surfaces directly as dropped or late frames.
8400    pub fn new(
8401        width: i32,
8402        height: i32,
8403        dri_node: String,
8404        auto_gpu_selected: bool,
8405        cursor_size: i32,
8406    ) -> Self {
8407        let (tx, rx) = smithay::reexports::calloop::channel::channel();
8408        let (wake_tx, wake_rx) = smithay::reexports::calloop::channel::channel();
8409        let cu_tx = tx.clone();
8410        let cu_wake_tx = wake_tx.clone();
8411        let pointer = Arc::new(crate::wayland::frontend::PointerShare::default());
8412        let pointer_share = pointer.clone();
8413        thread::spawn(move || {
8414            crate::boost_thread_priority(-15);
8415            run_wayland_thread(WaylandThreadConfig {
8416                command_rx: rx,
8417                wake_rx,
8418                wake_tx: cu_wake_tx,
8419                command_tx: cu_tx,
8420                initial_width: width,
8421                initial_height: height,
8422                explicit_dri_node: dri_node,
8423                auto_gpu_selected,
8424                cursor_size,
8425                pointer_share,
8426            });
8427        });
8428        Compositor {
8429            tx,
8430            wake_tx,
8431            pointer,
8432            motions: AtomicU64::new(0),
8433        }
8434    }
8435
8436    /// Where the compositor's pointer is as of its thread's last pass, as `(x, y, scale,
8437    /// motion)`: physical union-layout coordinates (where `inject_mouse_move` puts it), the
8438    /// scale of the output under it, by which a relative move's pixels travel physically,
8439    /// and the number of the last move applied (what `inject_mouse_move` and
8440    /// `inject_relative_mouse_move` return), so a caller can tell which of its moves the
8441    /// position includes. An app's warp is included too, since the position is read back
8442    /// rather than summed from what was injected. None before the first pass and, under host
8443    /// capture, before its first cursor meta or where the host paints its cursor into the
8444    /// frames. Never waits on the compositor.
8445    pub fn pointer_location(&self) -> Option<(f64, f64, f64, u64)> {
8446        let host = *crate::wayland::pwcapture::HOST_POINTER.lock().unwrap();
8447        crate::wayland::pwcapture::prefer_host_pointer(host, || self.pointer.read())
8448            .map(|at| (at.x, at.y, at.scale, at.motion))
8449    }
8450
8451    /// Begin a capture with the given frame callback and settings. The target display is
8452    /// the settings' `display_id` attribute (absent = 0, the primary); each display id runs
8453    /// at most one capture, independent of every other display's.
8454    ///
8455    /// Issuing the start also clears the interpreter-teardown gate: starting from Python proves the
8456    /// interpreter is live again after a manual atexit sweep.
8457    pub fn start_capture(
8458        &self,
8459        sink: Arc<dyn crate::sink::FrameSink>,
8460        settings: CaptureSettings,
8461    ) -> Result<(), Error> {
8462        let display_id = settings.display_id;
8463
8464        SHUTDOWN.store(false, Ordering::Relaxed);
8465        self.send(ThreadCommand::StartCapture {
8466            display_id,
8467            callback: Some(sink),
8468            settings,
8469        })
8470        .map_err(|e| Error::Compositor(format!("Failed to send start command: {e}")))?;
8471        Ok(())
8472    }
8473
8474    /// Stop the capture bound to `display_id` (default: the primary display).
8475    pub fn stop_capture(&self, display_id: u32) -> Result<(), Error> {
8476        self.send(ThreadCommand::StopCapture { display_id })
8477            .map_err(|e| Error::Compositor(format!("Failed to send stop command: {e}")))?;
8478        Ok(())
8479    }
8480
8481    /// Create an additional output (`WxH` physical pixels at fractional `scale`) mapped into
8482    /// the layout at offset `(x, y)`; `id` is the display key used by every per-display API.
8483    /// False when the id is taken, the geometry/scale is invalid, the rectangle overlaps a
8484    /// live output, or the GPU render target cannot be allocated. Capture reconfigures
8485    /// (`start_capture` on an existing output) resize without this validation, so a
8486    /// multi-step relayout must stay overlap-free at every step by caller ordering.
8487    pub fn create_output(
8488        &self,
8489        id: u32,
8490        width: i32,
8491        height: i32,
8492        x: i32,
8493        y: i32,
8494        scale: f64,
8495    ) -> Result<bool, Error> {
8496        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
8497        self.send(ThreadCommand::CreateOutput {
8498            id,
8499            width,
8500            height,
8501            x,
8502            y,
8503            scale,
8504            reply: reply_tx,
8505        })
8506        .map_err(|e| Error::Compositor(format!("Failed to create output: {e}")))?;
8507        Ok(reply_rx
8508            .recv_timeout(Duration::from_secs(2))
8509            .unwrap_or(false))
8510    }
8511
8512    /// Add a display over a rectangle of an existing output, capturing that rectangle
8513    /// without publishing a screen of its own.
8514    ///
8515    /// The session then sees one screen where the client sees several, which is what a
8516    /// window drag needs: a pointer grab stays inside the one surface as it crosses from
8517    /// one display to the next, instead of stopping at the edge of the screen it started
8518    /// on. `x` and `y` are relative to `owner`'s output. False when the id is taken, the
8519    /// owner is unknown or itself a view, or the rectangle leaves the owner's output.
8520    pub fn create_view(
8521        &self,
8522        id: u32,
8523        owner: u32,
8524        x: i32,
8525        y: i32,
8526        width: i32,
8527        height: i32,
8528    ) -> Result<bool, Error> {
8529        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
8530        self.send(ThreadCommand::CreateView {
8531            id,
8532            owner,
8533            x,
8534            y,
8535            width,
8536            height,
8537            reply: reply_tx,
8538        })
8539        .map_err(|e| Error::Compositor(format!("Failed to create view: {e}")))?;
8540        Ok(reply_rx
8541            .recv_timeout(Duration::from_secs(2))
8542            .unwrap_or(false))
8543    }
8544
8545    /// Resize a screen's output in place, keeping its position: the desk the displays cut
8546    /// out of it are laid over. False for a view, an unknown id, or a size that would leave
8547    /// one of the screen's views outside it.
8548    pub fn resize_output(
8549        &self,
8550        id: u32,
8551        width: i32,
8552        height: i32,
8553        scale: f64,
8554    ) -> Result<bool, Error> {
8555        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
8556        self.send(ThreadCommand::ResizeOutput {
8557            id,
8558            width,
8559            height,
8560            scale,
8561            reply: reply_tx,
8562        })
8563        .map_err(|e| Error::Compositor(format!("Failed to resize output: {e}")))?;
8564        Ok(reply_rx
8565            .recv_timeout(Duration::from_secs(2))
8566            .unwrap_or(false))
8567    }
8568
8569    /// Destroy a secondary output: its capture ends cleanly and its windows relocate to the
8570    /// primary output. False for the primary (id 0) or an unknown id.
8571    pub fn destroy_output(&self, id: u32) -> Result<bool, Error> {
8572        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
8573        self.send(ThreadCommand::DestroyOutput {
8574            id,
8575            reply: reply_tx,
8576        })
8577        .map_err(|e| Error::Compositor(format!("Failed to destroy output: {e}")))?;
8578        Ok(reply_rx
8579            .recv_timeout(Duration::from_secs(2))
8580            .unwrap_or(false))
8581    }
8582
8583    /// Move an existing output (the primary, id 0, included) to layout offset `(x, y)`;
8584    /// its windows, absolute input injection, and cursor compositing follow. False for an
8585    /// unknown id or a destination overlapping a live output. Capture reconfigures
8586    /// (`start_capture` on an existing output) resize without this validation, so a
8587    /// multi-step relayout must stay overlap-free at every step by caller ordering.
8588    pub fn reposition_output(&self, id: u32, x: i32, y: i32) -> Result<bool, Error> {
8589        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
8590        self.send(ThreadCommand::RepositionOutput {
8591            id,
8592            x,
8593            y,
8594            reply: reply_tx,
8595        })
8596        .map_err(|e| Error::Compositor(format!("Failed to reposition output: {e}")))?;
8597        Ok(reply_rx
8598            .recv_timeout(Duration::from_secs(2))
8599            .unwrap_or(false))
8600    }
8601
8602    /// Every live output as `(id, x, y, width, height, scale, capturing)` — width/height in
8603    /// physical pixels, `(x, y)` the layout offset.
8604    pub fn list_outputs(&self) -> Result<Vec<OutputDesc>, Error> {
8605        let (reply_tx, reply_rx) = std::sync::mpsc::channel();
8606        self.send(ThreadCommand::ListOutputs { reply: reply_tx })
8607            .map_err(|e| Error::Compositor(format!("Failed to list outputs: {e}")))?;
8608        Ok(reply_rx
8609            .recv_timeout(Duration::from_secs(2))
8610            .unwrap_or_default())
8611    }
8612
8613    /// How many displays this backend can back with real content: -1 when
8614    /// self-compositing (outputs are created on demand, no fixed bound), otherwise the
8615    /// host compositor's output count. Host-capture mode reports -1 until the first
8616    /// capture start establishes the host session; 0 when the backend is unresponsive.
8617    pub fn output_capacity(&self) -> Result<i64, Error> {
8618        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<i64>();
8619        self.send(ThreadCommand::OutputCapacity { reply: reply_tx })
8620            .map_err(|e| Error::Compositor(format!("Failed to query output capacity: {e}")))?;
8621        Ok(reply_rx.recv_timeout(Duration::from_secs(2)).unwrap_or(0))
8622    }
8623
8624    /// Move the window with the given id onto output `output_id`, fullscreened at that
8625    /// output's logical size. False for an unknown window or output id.
8626    pub fn move_window_to_output(&self, window_id: u32, output_id: u32) -> Result<bool, Error> {
8627        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
8628        self.send(ThreadCommand::MoveWindowToOutput {
8629            window_id,
8630            output_id,
8631            reply: reply_tx,
8632        })
8633        .map_err(|e| Error::Compositor(format!("Failed to move window: {e}")))?;
8634        Ok(reply_rx
8635            .recv_timeout(Duration::from_secs(2))
8636            .unwrap_or(false))
8637    }
8638
8639    /// Every mapped window as `(window_id, title, app_id, output_id, waiting)`. A waiting
8640    /// window is tagged for that output but mapped clear of every one of them, holding its
8641    /// size until an output exists for it — a nested session's spare screens.
8642    pub fn list_windows(&self) -> Result<Vec<WindowDesc>, Error> {
8643        let (reply_tx, reply_rx) = std::sync::mpsc::channel();
8644        self.send(ThreadCommand::ListWindows { reply: reply_tx })
8645            .map_err(|e| Error::Compositor(format!("Failed to list windows: {e}")))?;
8646        Ok(reply_rx
8647            .recv_timeout(Duration::from_secs(2))
8648            .unwrap_or_default())
8649    }
8650
8651    /// The desktop as the installed window policy holds it: every window's place, state, and
8652    /// stacking. `None` with no policy installed, or where the compositor does not answer.
8653    pub fn desktop_view(&self) -> Result<Option<wm::DesktopView>, Error> {
8654        let (reply_tx, reply_rx) = std::sync::mpsc::channel();
8655        self.send(ThreadCommand::DesktopView { reply: reply_tx })
8656            .map_err(|e| Error::Compositor(format!("Failed to describe the desktop: {e}")))?;
8657        Ok(reply_rx
8658            .recv_timeout(Duration::from_secs(2))
8659            .unwrap_or_default())
8660    }
8661
8662    /// Ask the installed window policy to act on a window (activate, close, minimize, and the
8663    /// like), as a taskbar asks. False with no policy installed, for a window the policy has
8664    /// not placed, or where the compositor does not answer; the policy decides what becomes of
8665    /// the request.
8666    pub fn window_request(&self, id: wm::WindowId, request: wm::Request) -> Result<bool, Error> {
8667        let (reply_tx, reply_rx) = std::sync::mpsc::channel();
8668        self.send(ThreadCommand::WindowRequest {
8669            id,
8670            request,
8671            reply: reply_tx,
8672        })
8673        .map_err(|e| Error::Compositor(format!("Failed to send a window request: {e}")))?;
8674        Ok(reply_rx
8675            .recv_timeout(Duration::from_secs(2))
8676            .unwrap_or(false))
8677    }
8678
8679    /// Recreate the cursor theme at `size` pixels — no restart: subsequent named-cursor
8680    /// callbacks and the burned-in cursor overlay render at the new size. False for a
8681    /// non-positive size.
8682    pub fn set_cursor_size(&self, size: i32) -> Result<bool, Error> {
8683        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
8684        self.send(ThreadCommand::SetCursorSize {
8685            size,
8686            reply: reply_tx,
8687        })
8688        .map_err(|e| Error::Compositor(format!("Failed to set cursor size: {e}")))?;
8689        Ok(reply_rx
8690            .recv_timeout(Duration::from_secs(2))
8691            .unwrap_or(false))
8692    }
8693
8694    /// Compositor-side clipboard offer: serve one payload per `(mime, data)` entry, so a
8695    /// client pasting rich text takes the markup and one pasting into a plain field takes
8696    /// the text the source itself wrote, instead of both taking whichever flavor was
8697    /// picked for them.
8698    pub fn set_clipboard(&self, entries: Vec<(String, Vec<u8>)>) -> Result<(), Error> {
8699        self.send(ThreadCommand::SetClipboard { entries })
8700            .map_err(|e| Error::Compositor(format!("Failed to set clipboard: {e}")))?;
8701        Ok(())
8702    }
8703
8704    pub fn inject_key(&self, scancode: u32, state: u32) -> Result<(), Error> {
8705        self.send(ThreadCommand::KeyboardKey { scancode, state })
8706            .map_err(|e| Error::Compositor(format!("Failed to inject key: {e}")))?;
8707        Ok(())
8708    }
8709
8710    /// Inject an ordered run of `(keycode, state)` events as one message, so a paste
8711    /// costs one channel send and one wake instead of one per event.
8712    pub fn inject_keys(&self, events: Vec<(u32, u32)>) -> Result<(), Error> {
8713        if events.is_empty() {
8714            return Ok(());
8715        }
8716        self.send(ThreadCommand::KeyboardKeys { events })
8717            .map_err(|e| Error::Compositor(format!("Failed to inject keys: {e}")))?;
8718        Ok(())
8719    }
8720
8721    /// Swap the seat keyboard's xkb keymap (XKB_KEYMAP_FORMAT_TEXT_V1 text). The caller
8722    /// owns keysym-to-keycode policy: define keycodes here, then press them via
8723    /// `inject_key`. Ordered with key events on the one compositor channel.
8724    /// Bind explicit `(keycode, keysym)` pairs onto the current base keymap in ONE swap.
8725    ///
8726    /// The caller owns the assignment — which keysym goes to which keycode, and when to
8727    /// recycle one — because that tracks layouts and user reports. This end only assembles
8728    /// the xkb text and delivers it, reusing the installed base rather than recompiling a
8729    /// re-supplied one. False when no base keymap is installed yet.
8730    pub fn set_keymap_overlay(&self, binds: Vec<(u32, u32)>) -> Result<(), Error> {
8731        self.send(ThreadCommand::SetKeymapOverlay { binds })
8732            .map_err(|e| Error::Compositor(format!("Failed to set overlay: {e}")))?;
8733        Ok(())
8734    }
8735
8736    pub fn set_keymap_string(&self, text: String) -> Result<(), Error> {
8737        self.send(ThreadCommand::SetKeymapString(text))
8738            .map_err(|e| Error::Compositor(format!("Failed to set keymap: {e}")))?;
8739        Ok(())
8740    }
8741
8742    /// Return the active xkb keymap as an XKB_KEYMAP_FORMAT_TEXT_V1 string so a consumer can
8743    /// build a reverse keysym->keycode map from the identical keymap.
8744    ///
8745    /// The GIL is released while awaiting the reply, because the Wayland thread can call back into
8746    /// Python and would otherwise deadlock; the wait is bounded so a stall cannot hang the caller,
8747    /// and an empty string is returned when the keymap cannot be read in time.
8748    pub fn get_xkb_keymap_string(&self) -> Result<String, Error> {
8749        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<String>();
8750        self.send(ThreadCommand::GetXkbKeymap { reply: reply_tx })
8751            .map_err(|e| Error::Compositor(format!("Failed to request keymap: {e}")))?;
8752        let result = reply_rx.recv_timeout(Duration::from_secs(2));
8753        match result {
8754            Ok(s) => Ok(s),
8755            Err(_) => Ok(String::new()),
8756        }
8757    }
8758
8759    /// Move the pointer to a point in physical union-layout coordinates. Returns the
8760    /// move's number (`pointer_location`).
8761    pub fn inject_mouse_move(&self, x: f64, y: f64) -> Result<u64, Error> {
8762        let motion = self.motions.fetch_add(1, Ordering::Relaxed) + 1;
8763        self.send(ThreadCommand::PointerMotion { x, y, motion })
8764            .map_err(|e| Error::Compositor(format!("Failed to inject motion: {e}")))?;
8765        Ok(motion)
8766    }
8767
8768    /// Move the pointer by a delta. On the seat this is an absolute move plus a
8769    /// `zwp_relative_pointer_v1` event; a nested KWin session, which forwards no
8770    /// delta from its host seat, receives it through its `org_kde_kwin_fake_input`
8771    /// device on the app compositor socket named by `set_app_wayland_display`. Returns the
8772    /// move's number (`pointer_location`).
8773    pub fn inject_relative_mouse_move(&self, dx: f64, dy: f64) -> Result<u64, Error> {
8774        let motion = self.motions.fetch_add(1, Ordering::Relaxed) + 1;
8775        self.send(ThreadCommand::PointerRelativeMotion { dx, dy, motion })
8776            .map_err(|e| Error::Compositor(format!("Failed to inject relative motion: {e}")))?;
8777        Ok(motion)
8778    }
8779
8780    pub fn inject_mouse_button(&self, btn: u32, state: u32) -> Result<(), Error> {
8781        self.send(ThreadCommand::PointerButton { btn, state })
8782            .map_err(|e| Error::Compositor(format!("Failed to inject button: {e}")))?;
8783        Ok(())
8784    }
8785
8786    pub fn inject_mouse_scroll(&self, x: f64, y: f64) -> Result<(), Error> {
8787        self.send(ThreadCommand::PointerAxis { x, y })
8788            .map_err(|e| Error::Compositor(format!("Failed to inject axis: {e}")))?;
8789        Ok(())
8790    }
8791
8792    /// Scroll by a finger's travel of (dx, dy) physical pixels, the way a touchpad does:
8793    /// fractional, with no wheel steps, until `inject_finger_scroll_end`.
8794    pub fn inject_finger_scroll(&self, dx: f64, dy: f64) -> Result<(), Error> {
8795        self.send(ThreadCommand::PointerScroll { dx, dy })
8796            .map_err(|e| Error::Compositor(format!("Failed to inject finger scroll: {e}")))?;
8797        Ok(())
8798    }
8799
8800    /// End a finger's scroll: the axis stop a toolkit starts its kinetic scroll on.
8801    pub fn inject_finger_scroll_end(&self) -> Result<(), Error> {
8802        self.send(ThreadCommand::PointerScrollEnd)
8803            .map_err(|e| Error::Compositor(format!("Failed to end finger scroll: {e}")))?;
8804        Ok(())
8805    }
8806
8807    pub fn set_cursor_rendering(&self, enabled: bool) -> Result<(), Error> {
8808        self.send(ThreadCommand::UpdateCursorConfig {
8809            render_on_framebuffer: enabled,
8810        })
8811        .map_err(|e| Error::Compositor(format!("Failed to set cursor config: {e}")))?;
8812        Ok(())
8813    }
8814
8815    /// Forces an IDR/keyframe on the next captured frame so a (re)connecting client
8816    /// or a decoder reset can resume immediately. With the default infinite GOP this
8817    /// is the only recovery path, so every consumer that can lose decoder state must
8818    /// call it. No-op cost on the JPEG/software path (keyframes are N/A).
8819    pub fn request_idr_frame(&self, display_id: u32) -> Result<(), Error> {
8820        self.send(ThreadCommand::RequestIdr { display_id })
8821            .map_err(|e| Error::Compositor(format!("Failed to request IDR: {e}")))?;
8822        Ok(())
8823    }
8824
8825    /// A client lost frame `frame_id` of the given display's capture, or its sender withheld it to
8826    /// hold the stream to a rate (`withheld`): the frames after it stop predicting from it, so the
8827    /// next one decodes there without a keyframe. An encoder that cannot leave a frame out codes a
8828    /// keyframe instead.
8829    pub fn invalidate_reference(
8830        &self,
8831        frame_id: u16,
8832        display_id: u32,
8833        withheld: bool,
8834    ) -> Result<(), Error> {
8835        self.send(ThreadCommand::InvalidateReference {
8836            display_id,
8837            frame_id,
8838            withheld,
8839        })
8840        .map_err(|e| Error::Compositor(format!("Failed to invalidate reference: {e}")))
8841    }
8842
8843    /// Every client holds frame `frame_id` of one display's capture, or where not `held` was
8844    /// sent it, so a session keeping long-term references pins the newest anchor they all hold
8845    /// and predicts its anchors from a frame they were all sent: each client recovers on its own
8846    /// from a loss of any depth, without a keyframe.
8847    pub fn acknowledge_reference(
8848        &self,
8849        frame_id: u16,
8850        display_id: u32,
8851        held: bool,
8852    ) -> Result<(), Error> {
8853        self.send(ThreadCommand::AcknowledgeReference {
8854            display_id,
8855            frame_id,
8856            held,
8857        })
8858        .map_err(|e| Error::Compositor(format!("Failed to acknowledge reference: {e}")))
8859    }
8860
8861    /// Apply a live bitrate (kbps) / VBV (kb) / framerate change to the given display's
8862    /// running capture.
8863    pub fn update_rate(
8864        &self,
8865        bitrate_kbps: Option<i32>,
8866        vbv_multiplier: Option<f64>,
8867        fps: Option<f64>,
8868        display_id: u32,
8869    ) -> Result<(), Error> {
8870        self.send(ThreadCommand::UpdateRate {
8871            display_id,
8872            bitrate_kbps,
8873            vbv_multiplier,
8874            fps,
8875        })
8876        .map_err(|e| Error::Compositor(format!("Failed to update rate: {e}")))?;
8877        Ok(())
8878    }
8879
8880    /// Set the seat's BASE xkb layout from RMLVO names at runtime (empty strings select the
8881    /// xkbcommon defaults). Returns whether the layout compiled and was applied; overlay binds
8882    /// rebuild on top with their keycodes unchanged.
8883    pub fn set_xkb_layout(
8884        &self,
8885        layout: String,
8886        variant: String,
8887        options: String,
8888        model: String,
8889        rules: String,
8890    ) -> Result<bool, Error> {
8891        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
8892        self.send(ThreadCommand::SetXkbLayout {
8893            rules,
8894            model,
8895            layout,
8896            variant,
8897            options,
8898            reply: reply_tx,
8899        })
8900        .map_err(|e| Error::Compositor(format!("Failed to set layout: {e}")))?;
8901        Ok(reply_rx
8902            .recv_timeout(Duration::from_secs(2))
8903            .unwrap_or(false))
8904    }
8905
8906    /// Debug/verification readback of the seat keyboard: `(pressed_keycodes, modifier_mask)`
8907    /// with mask bits 1 ctrl, 2 shift, 4 alt, 8 logo, 16 caps, 32 num, 64 altgr, 128 level5.
8908    pub fn get_keyboard_state(&self) -> Result<(Vec<u32>, u32), Error> {
8909        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<(Vec<u32>, u32)>();
8910        self.send(ThreadCommand::GetKeyboardState { reply: reply_tx })
8911            .map_err(|e| Error::Compositor(format!("Failed to read keyboard state: {e}")))?;
8912        Ok(reply_rx
8913            .recv_timeout(Duration::from_secs(2))
8914            .unwrap_or_default())
8915    }
8916
8917    /// The capture geometry actually live on the given display: `(width, height, scale)` in
8918    /// physical pixels, or `None` when the compositor did not answer in time. Reflects any
8919    /// degrade a `start_capture` performed (H.264 even-masking, GBM allocation failure keeping
8920    /// the previous mode, a host that kept its own mode), and the command channel is FIFO, so
8921    /// calling this after `start_capture` returns what that start realized. A host-capture
8922    /// start answers once the host has ruled on the requested mode (the compositor thread
8923    /// keeps serving input meanwhile), so a slow-but-live start reads its real size instead
8924    /// of being reported as a timeout; the `None` a genuine timeout returns lets the caller
8925    /// treat the geometry as unknown, not as "nothing to reconcile", which a `(0, 0, 0.0)`
8926    /// sentinel could not.
8927    pub fn get_realized_geometry(&self, display_id: u32) -> Result<Option<(i32, i32, f64)>, Error> {
8928        let (reply_tx, reply_rx) = std::sync::mpsc::channel::<(i32, i32, f64)>();
8929        self.send(ThreadCommand::CuGetInfo {
8930            display_id,
8931            resp: reply_tx,
8932        })
8933        .map_err(|e| Error::Compositor(format!("Failed to read geometry: {e}")))?;
8934        Ok(reply_rx.recv_timeout(GEOMETRY_BARRIER_TIMEOUT).ok())
8935    }
8936
8937    /// Lifecycle of `display_id`'s capture as `(state, last_error)`: `state` is `"running"`
8938    /// (pipeline live), `"failed"` (start left no live pipeline), or `"idle"` (never started
8939    /// / stopped clean); `last_error` is the reason a start failed, or a caveat a live
8940    /// capture came up with (host connect refused -> local compositing, a hardware encoder
8941    /// that fell back to CPU, a refused resize). Read straight from the recorded outcome, so
8942    /// pair it with a prior `get_realized_geometry` when the ordering after a start matters.
8943    pub fn capture_state(&self, display_id: u32) -> (String, Option<String>) {
8944        wayland_capture_state(display_id)
8945    }
8946}
8947
8948/// Bound on the geometry read-back barrier. Behind a host-capture start the read is held
8949/// until the host has ruled on the requested mode, which the host's control thread bounds
8950/// by `LAYOUT_DEADLINE`, so this clears that with margin or a live-but-slow start would read
8951/// as a timeout.
8952const GEOMETRY_BARRIER_TIMEOUT: Duration = Duration::from_secs(6);
8953
8954/// `(state, last_error)` for `display_id`'s Wayland capture, from the shared liveness and
8955/// outcome maps (no command round-trip).
8956fn wayland_capture_state(display_id: u32) -> (String, Option<String>) {
8957    let running = wayland_alive().lock().unwrap().contains(&display_id);
8958    let last_error = wayland_capture_err()
8959        .lock()
8960        .unwrap()
8961        .get(&display_id)
8962        .cloned();
8963    let state = if running {
8964        "running"
8965    } else if last_error.is_some() {
8966        "failed"
8967    } else {
8968        "idle"
8969    };
8970    (state.to_string(), last_error)
8971}
8972
8973impl Compositor {
8974    /// Retune the live per-frame knobs of `display_id`'s capture without restarting it.
8975    pub fn update_tunables(&self, display_id: u32, t: LiveTunables) -> Result<(), Error> {
8976        self.send(ThreadCommand::UpdateTunables {
8977            display_id,
8978            tunables: t,
8979        })
8980        .map_err(|e| Error::Compositor(format!("Failed to update tunables: {e}")))
8981    }
8982}
8983
8984use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering};
8985use std::sync::{Condvar, Mutex, OnceLock};
8986
8987use crate::encoders::software::{EncodedStripe, FrameTiming};
8988
8989/// Process-wide Wayland backend: input and capture share ONE compositor (constructed lazily).
8990static WAYLAND_BACKEND: OnceLock<Mutex<Option<Arc<Compositor>>>> = OnceLock::new();
8991/// The compositor's auto-picked socket name (e.g. "wayland-1"), published by the compositor
8992/// thread once its listening socket exists. `ListeningSocketSource::new_auto` binds the first
8993/// FREE wayland-N, which need not match any configured index — consumers must read the real
8994/// name from here instead of assuming one.
8995static WAYLAND_SOCKET_NAME: Mutex<Option<String>> = Mutex::new(None);
8996static WAYLAND_SOCKET_CV: Condvar = Condvar::new();
8997
8998fn publish_socket_name(name: &str) {
8999    *WAYLAND_SOCKET_NAME.lock().unwrap() = Some(name.to_string());
9000    WAYLAND_SOCKET_CV.notify_all();
9001}
9002
9003/// Wait (bounded) for the compositor thread to publish its socket name.
9004fn wait_socket_name(timeout: Duration) -> Option<String> {
9005    let deadline = Instant::now() + timeout;
9006    let mut g = WAYLAND_SOCKET_NAME.lock().unwrap();
9007    loop {
9008        if let Some(name) = g.as_ref() {
9009            return Some(name.clone());
9010        }
9011        let now = Instant::now();
9012        if now >= deadline {
9013            return None;
9014        }
9015        let (gg, _) = WAYLAND_SOCKET_CV.wait_timeout(g, deadline - now).unwrap();
9016        g = gg;
9017    }
9018}
9019/// Cursor callback registered before the backend exists (selkies registers it pre-start);
9020/// applied when the backend is created, which is deferred to capture start so the real
9021/// render node (not a placeholder) reaches the compositor.
9022static PENDING_CURSOR_CALLBACK: Mutex<Option<Arc<dyn crate::sink::CursorSink>>> = Mutex::new(None);
9023/// Every capture's own cursor callback, the latest registration last: the process-wide slots
9024/// carry the latest one's, so a capture withdrawing its own (a second display leaving) hands
9025/// the cursor back to the capture registered before it.
9026static CURSOR_REGISTRATIONS: Mutex<Vec<(u64, Arc<dyn crate::sink::CursorSink>)>> =
9027    Mutex::new(Vec::new());
9028/// Consumer-gone gate, set by `shutdown`: the detached compositor and delivery threads must
9029/// never call a sink again, since the Python bridge runs `shutdown` from `atexit` and a sink
9030/// attaching to a finalizing interpreter aborts the process (pre-3.13). Cleared by a fresh
9031/// capture start (only a live consumer can start one).
9032pub(crate) static SHUTDOWN: AtomicBool = AtomicBool::new(false);
9033/// Per-display capture ownership: display id -> the ScreenCapture id that owns that
9034/// display's capture. Only the owner may stop it, so an input-only or stale instance can't
9035/// tear down a live capture.
9036static WAYLAND_OWNERS: OnceLock<Mutex<std::collections::HashMap<u32, u64>>> = OnceLock::new();
9037
9038fn wayland_owners() -> &'static Mutex<std::collections::HashMap<u32, u64>> {
9039    WAYLAND_OWNERS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
9040}
9041
9042/// Display ids whose capture pipeline is actually running (StartCapture inserts,
9043/// StopCapture/DestroyOutput remove), so the Python-facing is_capturing() reports pipeline
9044/// liveness, not merely which ScreenCapture owns the backend.
9045static WAYLAND_ALIVE_DISPLAYS: OnceLock<Mutex<std::collections::HashSet<u32>>> = OnceLock::new();
9046
9047fn wayland_alive() -> &'static Mutex<std::collections::HashSet<u32>> {
9048    WAYLAND_ALIVE_DISPLAYS.get_or_init(|| Mutex::new(std::collections::HashSet::new()))
9049}
9050/// Per-display outcome of the most recent capture start, recorded by the calloop thread
9051/// so the Python-facing `capture_state` can report it. A start that came up clean removes
9052/// its entry; a hard failure (no output, ...) or a caveat that still capturing (host
9053/// connect failed -> local compositing, hardware encoder fell back to CPU) leaves the
9054/// reason here. Distinct from `wayland_alive`, which says only whether the pipeline runs.
9055static WAYLAND_CAPTURE_ERR: OnceLock<Mutex<std::collections::HashMap<u32, String>>> =
9056    OnceLock::new();
9057
9058fn wayland_capture_err() -> &'static Mutex<std::collections::HashMap<u32, String>> {
9059    WAYLAND_CAPTURE_ERR.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
9060}
9061
9062/// Record (or clear, with `None`) the last-start outcome for `display_id`.
9063static WAYLAND_ACTIVE_CODEC: OnceLock<Mutex<std::collections::HashMap<u32, u32>>> = OnceLock::new();
9064
9065/// The codec each Wayland display's capture streams, as `Codec::id`, read by
9066/// `ScreenCapture::active_codec` without a round trip to the compositor thread.
9067fn wayland_active_codec() -> &'static Mutex<std::collections::HashMap<u32, u32>> {
9068    WAYLAND_ACTIVE_CODEC.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
9069}
9070
9071fn set_wayland_active_codec(display_id: u32, codec: Option<Codec>) {
9072    let mut map = wayland_active_codec().lock().unwrap();
9073    match codec {
9074        Some(c) => {
9075            map.insert(display_id, c.id());
9076        }
9077        None => {
9078            map.remove(&display_id);
9079        }
9080    }
9081}
9082
9083fn set_wayland_capture_err(display_id: u32, err: Option<String>) {
9084    let mut map = wayland_capture_err().lock().unwrap();
9085    match err {
9086        Some(e) => {
9087            map.insert(display_id, e);
9088        }
9089        None => {
9090            map.remove(&display_id);
9091        }
9092    }
9093}
9094/// Hands each `ScreenCapture` a unique, monotonic id — the token `WAYLAND_OWNERS` compares to
9095/// decide which instance is allowed to stop a display's capture. Starts at 1 so 0 can
9096/// mean "no owner".
9097static NEXT_CAPTURE_ID: AtomicU64 = AtomicU64::new(1);
9098/// Registry of every live X11 capture's `Controls`, so the atexit sweep can flag them all to
9099/// stop before the interpreter finalizes even after the owning `ScreenCapture` Python handles have
9100/// been dropped — without a central registry those captures would have no reachable stop switch.
9101static LIVE_X11: OnceLock<Mutex<Vec<Arc<crate::x11::Controls>>>> = OnceLock::new();
9102
9103fn live_x11() -> &'static Mutex<Vec<Arc<crate::x11::Controls>>> {
9104    LIVE_X11.get_or_init(|| Mutex::new(Vec::new()))
9105}
9106
9107/// Convert premultiplied-alpha RGBA pixels to the straight alpha PNG expects. Every cursor
9108/// source feeding the callback stores premultiplied color (XFixes and Xcursor by format
9109/// definition, wl_shm/dmabuf by Wayland convention); encoding those values as straight
9110/// alpha double-darkens antialiased edges. Rounds as `(c * 255 + a/2) / a`, clamped —
9111/// selkies' python seed path mirrors this exact integer math so both sources hash a cursor
9112/// to the same content handle.
9113pub(crate) fn unpremultiply_rgba(image: &mut image::RgbaImage) {
9114    for p in image.pixels_mut() {
9115        let a = u32::from(p.0[3]);
9116        if a == 0 {
9117            p.0 = [0, 0, 0, 0];
9118        } else if a < 255 {
9119            for c in &mut p.0[..3] {
9120                *c = ((u32::from(*c) * 255 + a / 2) / a).min(255) as u8;
9121            }
9122        }
9123    }
9124}
9125
9126/// Best-effort nice boost for the calling capture/encode/delivery thread. These threads
9127/// compete with the very workload being captured, so a scheduling edge keeps frame pacing
9128/// steady under load. Requires CAP_SYS_NICE (or root); otherwise EPERM and silently a no-op.
9129/// How long a stop waits for a capture or delivery thread to end before leaving it behind.
9130const STOP_JOIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
9131
9132/// Join `handle` if it ends within `STOP_JOIN_TIMEOUT`, else leave the thread behind with one
9133/// line saying so: a thread held inside a driver call that never returns must not hold the
9134/// stop, and with it every later capture of the session, with nothing said.
9135pub(crate) fn join_within(handle: thread::JoinHandle<()>, what: &str) {
9136    let deadline = std::time::Instant::now() + STOP_JOIN_TIMEOUT;
9137    while !handle.is_finished() {
9138        if std::time::Instant::now() >= deadline {
9139            eprintln!(
9140                "[X11] the {what} thread did not end within {} s; it is left behind",
9141                STOP_JOIN_TIMEOUT.as_secs()
9142            );
9143            return;
9144        }
9145        thread::sleep(std::time::Duration::from_millis(10));
9146    }
9147    let _ = handle.join();
9148}
9149
9150#[cfg(test)]
9151mod gpu_bring_up_tests {
9152    use super::*;
9153
9154    /// A render target the allocator refuses fails the bring-up, which holds no Wayland display
9155    /// and so cannot have advertised the GPU, and a size it takes comes up whole. Ignored by
9156    /// default: needs a render node (the first one).
9157    #[test]
9158    #[ignore]
9159    fn gpu_bring_up_fails_whole_on_a_target_the_allocator_refuses() {
9160        let index = first_render_node_index().expect("needs a render node");
9161        let node = format!("/dev/dri/renderD{}", 128 + index);
9162        let path = std::path::Path::new(&node);
9163        let err = gpu_bring_up(path, 0, 0)
9164            .err()
9165            .expect("an empty target is refused");
9166        assert_eq!(err, "Failed to allocate GBM buffer");
9167        let up = gpu_bring_up(path, 64, 64).expect("a 64x64 target is taken");
9168        assert!(!up.formats.is_empty());
9169        assert_eq!((up.target.0.width(), up.target.0.height()), (64, 64));
9170    }
9171}
9172
9173#[cfg(test)]
9174mod join_tests {
9175    use super::*;
9176
9177    /// A thread that ends is joined; one that does not is left behind once the bound passes.
9178    #[test]
9179    fn join_within_is_bounded() {
9180        let quick = thread::spawn(|| {});
9181        let started = std::time::Instant::now();
9182        join_within(quick, "quick");
9183        assert!(started.elapsed() < STOP_JOIN_TIMEOUT);
9184        let (tx, rx) = std::sync::mpsc::channel::<()>();
9185        let stuck = thread::spawn(move || {
9186            let _ = rx.recv();
9187        });
9188        let started = std::time::Instant::now();
9189        join_within(stuck, "stuck");
9190        assert!(started.elapsed() >= STOP_JOIN_TIMEOUT);
9191        drop(tx);
9192    }
9193}
9194
9195/// Give the calling thread the nice level `nice`, below the default. `setpriority` takes it
9196/// only with CAP_SYS_NICE or an RLIMIT_NICE that reaches it, which a session's process rarely
9197/// has; where it refuses, the request goes to rtkit over the system bus, which grants a
9198/// level within the daemon's own bounds to any process of the seat. A host without rtkit,
9199/// or without a system bus at all as a container is, leaves the thread at the default, said
9200/// once in the log.
9201pub(crate) fn boost_thread_priority(nice: libc::c_int) {
9202    static RTKIT: OnceLock<zbus::Result<zbus::Connection>> = OnceLock::new();
9203    static REFUSED: std::sync::Once = std::sync::Once::new();
9204    let tid = unsafe { libc::syscall(libc::SYS_gettid) };
9205    if unsafe { libc::setpriority(libc::PRIO_PROCESS, tid as libc::id_t, nice) } == 0 {
9206        return;
9207    }
9208    let granted = RTKIT
9209        .get_or_init(|| zbus::block_on(zbus::Connection::system()))
9210        .as_ref()
9211        .map_err(|e| e.to_string())
9212        .and_then(|conn| {
9213            zbus::block_on(async {
9214                zbus::Proxy::new(
9215                    conn,
9216                    "org.freedesktop.RealtimeKit1",
9217                    "/org/freedesktop/RealtimeKit1",
9218                    "org.freedesktop.RealtimeKit1",
9219                )
9220                .await?
9221                .call_method("MakeThreadHighPriority", &(tid as u64, nice))
9222                .await
9223            })
9224            .map(|_| ())
9225            .map_err(|e| e.to_string())
9226        });
9227    if let Err(why) = granted {
9228        REFUSED.call_once(|| {
9229            eprintln!("[pixelflux] Thread priority {nice} refused and rtkit does not grant it; capture threads run at normal priority.");
9230            crate::log::debug!("[pixelflux] rtkit: {why}");
9231        });
9232    }
9233}
9234
9235/// Forward a live rate change to the shared Wayland backend (no-op if none is running).
9236fn wayland_update_rate(
9237    display_id: u32,
9238    bitrate_kbps: Option<i32>,
9239    vbv_multiplier: Option<f64>,
9240    fps: Option<f64>,
9241) {
9242    if let Some(slot) = WAYLAND_BACKEND.get()
9243        && let Some(be) = slot.lock().unwrap().as_ref()
9244    {
9245        let _ = be.update_rate(bitrate_kbps, vbv_multiplier, fps, display_id);
9246    }
9247}
9248
9249/// Forward the rate a display's path carries to the shared Wayland backend (no-op if none is
9250/// running).
9251fn wayland_update_link_rate(display_id: u32, link_kbps: i32) {
9252    if let Some(slot) = WAYLAND_BACKEND.get()
9253        && let Some(be) = slot.lock().unwrap().as_ref()
9254    {
9255        let _ = be.send(ThreadCommand::UpdateLinkRate {
9256            display_id,
9257            link_kbps,
9258        });
9259    }
9260}
9261
9262/// Forward live per-frame tunables to the shared Wayland backend (no-op if none is running).
9263fn wayland_update_tunables(display_id: u32, t: LiveTunables) {
9264    if let Some(slot) = WAYLAND_BACKEND.get()
9265        && let Some(be) = slot.lock().unwrap().as_ref()
9266    {
9267        let _ = be.update_tunables(display_id, t);
9268    }
9269}
9270
9271/// Get-or-create the singleton Wayland backend (idempotent: the first dimensions and render
9272/// node win, and a later capture just resizes).
9273///
9274/// Called from capture start (which knows the operator's real node) and from the import-time
9275/// bootstrap when the deployment opts into pixelflux-as-compositor. The render node is chosen once,
9276/// at creation, by precedence: an explicit `render_node_path`, then an `auto_gpu` pick, then the
9277/// encoder node (so a caller that sets only one node still renders on that GPU); empty selects the
9278/// software renderer.
9279pub fn ensure_compositor(
9280    width: i32,
9281    height: i32,
9282    explicit_node: String,
9283    auto_gpu: String,
9284    fallback_node: String,
9285    cursor_size: i32,
9286) -> Result<Arc<Compositor>, Error> {
9287    let slot = WAYLAND_BACKEND.get_or_init(|| Mutex::new(None));
9288    let mut g = slot.lock().unwrap();
9289    if g.is_none() {
9290        let mut node = (!explicit_node.is_empty()).then_some(explicit_node);
9291        let mut auto_gpu_selected = false;
9292        if node.is_none()
9293            && let Some(request) = parse_auto_gpu(&auto_gpu)
9294        {
9295            match auto_select_render_node(request.as_deref()) {
9296                Some(picked) => {
9297                    println!("[Wayland] AUTO_GPU selected {picked}.");
9298                    node = Some(picked);
9299                    auto_gpu_selected = true;
9300                }
9301                None => {
9302                    if let Some(token) = request {
9303                        eprintln!("[pixelflux] AUTO_GPU={token}: no matching GPU found.");
9304                    }
9305                }
9306            }
9307        }
9308        let node = node.unwrap_or(fallback_node);
9309        let be = Arc::new(Compositor::new(
9310            width,
9311            height,
9312            node,
9313            auto_gpu_selected,
9314            cursor_size,
9315        ));
9316        if let Some(cb) = PENDING_CURSOR_CALLBACK.lock().unwrap().take() {
9317            let _ = be.set_cursor_sink(Some(cb));
9318        }
9319        *g = Some(be);
9320    }
9321    Ok(g.as_ref().unwrap().clone())
9322}
9323
9324/// Install a window policy on the running compositor; an error where none runs.
9325pub fn install_window_policy(policy: Box<dyn crate::wm::WindowPolicy>) -> Result<(), Error> {
9326    running_compositor()
9327        .ok_or_else(|| Error::Compositor("no compositor is running".to_string()))?
9328        .install_window_policy(policy)
9329}
9330
9331/// The desktop as the window policy installed on the running compositor holds it; `None` where
9332/// no compositor runs or none has a policy.
9333pub fn desktop_view() -> Option<wm::DesktopView> {
9334    running_compositor()?.desktop_view().ok().flatten()
9335}
9336
9337/// Ask the window policy installed on the running compositor to act on a window; false where
9338/// no compositor runs or none has a policy.
9339pub fn window_request(id: wm::WindowId, request: wm::Request) -> bool {
9340    running_compositor().is_some_and(|be| be.window_request(id, request).unwrap_or(false))
9341}
9342
9343/// The variable naming the Xwayland a caller ships, found ahead of the host's
9344/// ([`wayland::xwm::XWAYLAND_VAR`]).
9345pub const XWAYLAND_VAR: &str = wayland::xwm::XWAYLAND_VAR;
9346
9347/// Start Xwayland on the running compositor; `notify` is told the display number.
9348pub fn start_xwayland(notify: Option<std::sync::mpsc::Sender<u32>>) -> Result<(), Error> {
9349    running_compositor()
9350        .ok_or_else(|| Error::Compositor("no compositor is running".to_string()))?
9351        .start_xwayland(notify)
9352}
9353
9354/// The live Wayland backend, if any — never creates one. The pre-capture entry points
9355/// (input injection, cursor/config setters) use this so they can't lock in a backend
9356/// with a placeholder render node.
9357pub fn running_compositor() -> Option<Arc<Compositor>> {
9358    let slot = WAYLAND_BACKEND.get()?;
9359    let g = slot.lock().unwrap();
9360    g.as_ref().cloned()
9361}
9362
9363/// Mutable per-capture state behind `ScreenCapture`'s mutex: the active backend, the live
9364/// X11 controls and thread handle, and the capture / encode thread ids used to detect a re-entrant
9365/// stop.
9366struct ScState {
9367    /// 0 = idle, 1 = X11, 2 = Wayland.
9368    backend: u8,
9369    /// This capture holds one reference on the shared X11 cursor monitor. Set in
9370    /// the same locked section as `backend = 1` and TAKEN in the same locked
9371    /// section a stop reads the backend, so acquire/release pair exactly per
9372    /// capture — inferring the reference from `backend` alone would let a stop
9373    /// that interleaves with a start release a reference not yet taken (leaking
9374    /// the monitor once the acquire lands). The GIL happens to serialize that
9375    /// window today; the pairing must not depend on it.
9376    cursor_ref: bool,
9377    controls: Option<Arc<crate::x11::Controls>>,
9378    /// The controls of an X11 capture whose threads already run while `start_capture` has not
9379    /// published it: its first frames can reach a consumer before the start returns, and what
9380    /// that consumer asks for in reply (a lost frame, a key frame, a rate) has to reach the
9381    /// capture. A stop or a newer start in that window ends that capture, which is then never
9382    /// published.
9383    starting: Option<Arc<crate::x11::Controls>>,
9384    handle: Option<thread::JoinHandle<()>>,
9385    cap_thread_id: Option<thread::ThreadId>,
9386    /// The internal encode thread's id, so a re-entrant stop arriving on it is
9387    /// detected and doesn't try to join a chain that includes itself.
9388    encode_thread_id: Option<thread::ThreadId>,
9389    /// Handshake receiver kept when the bounded start-time wait for the encode thread id
9390    /// lapsed (slow X11 setup precedes the encode-thread spawn): the id is late-resolved
9391    /// from here on demand, so the re-entrant-stop guard still recognizes the encode
9392    /// thread — with `encode_thread_id` stuck at `None`, a stop from inside the delivery
9393    /// callback would join the capture thread, which joins the encode thread (the
9394    /// caller), a deadlock cycle. The id send strictly precedes any callback running on
9395    /// that thread, so a `try_recv` at stop time cannot miss it.
9396    encode_tid_rx: Option<std::sync::mpsc::Receiver<thread::ThreadId>>,
9397    /// Delivery thread: owns the GIL-bound Python callback so encode(N+1) never
9398    /// serializes behind deliver(N). Joined on stop; a re-entrant stop from
9399    /// inside the callback (which runs on this thread) must detach instead of
9400    /// self-joining.
9401    deliver_handle: Option<thread::JoinHandle<()>>,
9402    deliver_thread_id: Option<thread::ThreadId>,
9403    /// The Wayland display id this instance's capture is bound to (backend == 2).
9404    wl_display: u32,
9405    /// The X11 capture thread's exit error (backend == 1): the thread records why it died
9406    /// mid-run here, so `capture_state` can report it instead of the error scrolling past in
9407    /// a single stderr line. `None` while healthy.
9408    err: Option<Arc<Mutex<Option<String>>>>,
9409}
9410
9411impl ScState {
9412    /// The X11 controls a request reaches: the published capture's, else those of one still
9413    /// starting.
9414    fn x11_controls(&self) -> Option<Arc<crate::x11::Controls>> {
9415        match self.backend {
9416            1 => self.controls.clone(),
9417            0 => self.starting.clone(),
9418            _ => None,
9419        }
9420    }
9421}
9422
9423/// Unified capture handle, the one a consumer holds. Drives the X11 capture directly or delegates to the
9424/// shared Wayland backend, chosen at `start_capture` time. Exposes start_capture / stop_capture /
9425/// request_idr_frame / update_* / is_capturing, plus the Wayland input-injection methods.
9426/// What a capture streams and how it got there (`ScreenCapture::stream_info`): the report
9427/// with the encoder's GPU named, and the compositor's renderer where the capture runs on
9428/// Wayland.
9429#[derive(Clone, Debug)]
9430pub struct StreamInfo {
9431    pub info: report::StreamInfo,
9432    pub renderer: Option<report::Renderer>,
9433}
9434
9435/// A capture's counters (`ScreenCapture::stream_stats`): the totals since it started and the
9436/// extremes since the previous read.
9437#[derive(Clone, Copy, Debug)]
9438pub struct StreamStats {
9439    pub totals: report::StreamTotals,
9440    pub peaks: report::StreamPeaks,
9441}
9442
9443pub struct ScreenCapture {
9444    id: u64,
9445    inner: Mutex<ScState>,
9446}
9447
9448impl Default for ScreenCapture {
9449    fn default() -> Self {
9450        Self::new()
9451    }
9452}
9453
9454impl ScreenCapture {
9455    /// Register this capture's consumer of cursor images in the process-wide slots: the X11
9456    /// cursor monitor's, and the compositor's where one runs, else the stash
9457    /// `PENDING_CURSOR_CALLBACK` that `ensure_compositor` applies at creation (the
9458    /// backend slot lock is held across the check so a concurrent creation cannot miss it).
9459    /// Both slots outlive the capture that set them, so they carry the latest live
9460    /// registration (`CURSOR_REGISTRATIONS`): `None` withdraws this capture's own, releasing
9461    /// what it holds and restoring the one registered before it.
9462    pub fn set_cursor_sink(
9463        &self,
9464        sink: Option<Arc<dyn crate::sink::CursorSink>>,
9465    ) -> Result<(), Error> {
9466        let sink = {
9467            let mut regs = CURSOR_REGISTRATIONS.lock().unwrap();
9468            regs.retain(|(id, _)| *id != self.id);
9469            if let Some(cb) = sink {
9470                regs.push((self.id, cb));
9471            }
9472            regs.last().map(|(_, cb)| cb.clone())
9473        };
9474        crate::x11::cursor::set_callback(sink.clone());
9475        let slot = WAYLAND_BACKEND.get_or_init(|| Mutex::new(None));
9476        let g = slot.lock().unwrap();
9477        match g.as_ref() {
9478            Some(be) => be.set_cursor_sink(sink),
9479            None => {
9480                *PENDING_CURSOR_CALLBACK.lock().unwrap() = sink;
9481                Ok(())
9482            }
9483        }
9484    }
9485
9486    /// Hand what a client said of a frame to the capture: the X11 controls, drained ahead of the
9487    /// next encode, or the shared Wayland backend.
9488    fn hand_report(&self, report: encoders::reference::ReferenceReport) {
9489        use encoders::reference::ReferenceReport;
9490        let (backend, controls, did) = {
9491            let st = self.inner.lock().unwrap();
9492            (st.backend, st.x11_controls(), st.wl_display)
9493        };
9494        match backend {
9495            0 | 1 => {
9496                if let Some(c) = controls {
9497                    c.reference_reports.lock().unwrap().push(report);
9498                }
9499            }
9500            2 => {
9501                if let Some(slot) = WAYLAND_BACKEND.get()
9502                    && let Some(be) = slot.lock().unwrap().as_ref()
9503                {
9504                    let _ = match report {
9505                        ReferenceReport::Lost(frame_id) => {
9506                            be.invalidate_reference(frame_id, did, false)
9507                        }
9508                        ReferenceReport::Withheld(frame_id) => {
9509                            be.invalidate_reference(frame_id, did, true)
9510                        }
9511                        ReferenceReport::Held(frame_id) => {
9512                            be.acknowledge_reference(frame_id, did, true)
9513                        }
9514                        ReferenceReport::Sent(frame_id) => {
9515                            be.acknowledge_reference(frame_id, did, false)
9516                        }
9517                    };
9518                }
9519            }
9520            _ => {}
9521        }
9522    }
9523
9524    /// Stop this capture: signal the capture thread, drop the live controls, and join.
9525    ///
9526    /// The path forks on the backend. A **Wayland** capture only tells the shared compositor to
9527    /// stop when this instance still owns it — ownership is claimed-and-cleared atomically so a
9528    /// stale stop cannot tear down a capture another instance just started. An **X11** capture joins
9529    /// its capture thread (which also joins the encode thread) and then its deliver thread,
9530    /// releasing the GIL first because the deliver thread runs the Python callback and holding the
9531    /// GIL across the joins would deadlock. A re-entrant stop arriving on the capture, encode, or
9532    /// deliver thread cannot join itself, so it detaches and lets the threads exit on the stop flag.
9533    /// An X11 capture still starting is only signaled: its start joins it and publishes nothing.
9534    fn stop_internal(&self) -> Result<(), Error> {
9535        let (handle, deliver_handle, same_thread, backend, controls, wl_display, cursor_ref) = {
9536            let mut st = self.inner.lock().unwrap();
9537            if let Some(c) = &st.controls {
9538                c.stop.store(true, Ordering::Relaxed);
9539            }
9540            if let Some(c) = st.starting.take() {
9541                c.stop.store(true, Ordering::Relaxed);
9542            }
9543            let cur = Some(thread::current().id());
9544            if st.encode_thread_id.is_none()
9545                && let Some(rx) = st.encode_tid_rx.as_ref()
9546                && let Ok(id) = rx.try_recv()
9547            {
9548                st.encode_thread_id = Some(id);
9549            }
9550            let same = st.cap_thread_id == cur
9551                || st.encode_thread_id == cur
9552                || st.deliver_thread_id == cur;
9553            let controls = st.controls.take();
9554            let handle = st.handle.take();
9555            let deliver_handle = st.deliver_handle.take();
9556            let backend = st.backend;
9557            let cursor_ref = std::mem::take(&mut st.cursor_ref);
9558            let wl_display = st.wl_display;
9559            st.backend = 0;
9560            st.cap_thread_id = None;
9561            st.encode_thread_id = None;
9562            st.encode_tid_rx = None;
9563            st.deliver_thread_id = None;
9564            st.wl_display = 0;
9565            st.err = None;
9566            (
9567                handle,
9568                deliver_handle,
9569                same,
9570                backend,
9571                controls,
9572                wl_display,
9573                cursor_ref,
9574            )
9575        };
9576        if let Some(c) = &controls {
9577            live_x11().lock().unwrap().retain(|x| !Arc::ptr_eq(x, c));
9578        }
9579        if cursor_ref {
9580            crate::x11::cursor::release();
9581        }
9582        if backend == 2 {
9583            let did = wl_display;
9584            let owned = {
9585                let mut owners = wayland_owners().lock().unwrap();
9586                if owners.get(&did) == Some(&self.id) {
9587                    owners.remove(&did);
9588                    true
9589                } else {
9590                    false
9591                }
9592            };
9593            if owned
9594                && let Some(slot) = WAYLAND_BACKEND.get()
9595                && let Some(be) = slot.lock().unwrap().as_ref()
9596            {
9597                let _ = be.stop_capture(did);
9598            }
9599        } else {
9600            if same_thread {
9601                // Detach: the threads exit on the stop flag once the callback returns.
9602                drop(handle);
9603                drop(deliver_handle);
9604            } else {
9605                {
9606                    if let Some(h) = handle {
9607                        join_within(h, "capture");
9608                    }
9609                    // The capture thread's end drops the delivery sender, so the deliver
9610                    // thread drains its one queued frame and exits behind it.
9611                    if let Some(h) = deliver_handle {
9612                        join_within(h, "delivery");
9613                    }
9614                };
9615            }
9616        }
9617        Ok(())
9618    }
9619}
9620
9621impl ScreenCapture {
9622    /// The report of the capture this handle runs, whichever backend it is on.
9623    fn report(&self) -> Option<Arc<report::StreamReport>> {
9624        let st = self.inner.lock().unwrap();
9625        match st.backend {
9626            1 => st.controls.as_ref().map(|c| c.report.clone()),
9627            2 => report::wayland_reports()
9628                .lock()
9629                .unwrap()
9630                .get(&st.wl_display)
9631                .cloned(),
9632            _ => None,
9633        }
9634    }
9635}
9636
9637impl ScreenCapture {
9638    pub fn new() -> Self {
9639        Self {
9640            id: NEXT_CAPTURE_ID.fetch_add(1, Ordering::Relaxed),
9641            inner: Mutex::new(ScState {
9642                backend: 0,
9643                cursor_ref: false,
9644                controls: None,
9645                starting: None,
9646                handle: None,
9647                cap_thread_id: None,
9648                encode_thread_id: None,
9649                encode_tid_rx: None,
9650                deliver_handle: None,
9651                deliver_thread_id: None,
9652                wl_display: 0,
9653                err: None,
9654            }),
9655        }
9656    }
9657
9658    /// The codec this capture streams, by name, once its pipeline has been built: the
9659    /// requested one, or the one the selection ladder demoted it to when no encoder could
9660    /// serve the request. None before the first frame or after a stop.
9661    pub fn active_codec(&self) -> Result<Option<String>, Error> {
9662        let (backend, controls, wl_display) = {
9663            let st = self.inner.lock().unwrap();
9664            (st.backend, st.controls.clone(), st.wl_display)
9665        };
9666        let id = match backend {
9667            1 => controls.map_or(u32::MAX, |c| c.codec.load(Ordering::Relaxed)),
9668            2 => {
9669                wayland_active_codec()
9670                    .lock()
9671                    .unwrap()
9672                    .get(&wl_display)
9673                    .copied()
9674            }
9675            .unwrap_or(u32::MAX),
9676            _ => u32::MAX,
9677        };
9678        Ok(Codec::from_id(id).map(|c| c.name().to_string()))
9679    }
9680
9681    /// What this capture streams and how it got there, or None before a start and after a
9682    /// stop: `backend` (`x11`, `wayland`), `capture` (the path: `NvFBC`, `DRI3`, `XShm`,
9683    /// `dmabuf`, `readback`), `zero_copy` and the `capture_reason` a faster path was declined
9684    /// for, `zero_copy_available` (whether the display server offered the encoder a zero-copy
9685    /// path at all), `encoder` (`NVENC`, `VAAPI`, or the software library), `hardware` and the
9686    /// `encoder_reason` it is not, `codec`, `fullcolor`, `striped`, and for a hardware session
9687    /// its `gpu`, kernel `driver`, and `encode_node`. A Wayland capture adds how its compositor
9688    /// renders: `renderer` (`gl`, `pixman`), `render_node`, `render_gpu`, `renderer_reason`.
9689    /// The first read of a VA-API session brings a GL context up once to name its GPU, so a
9690    /// caller with an event loop reads this off it.
9691    pub fn stream_info(&self) -> Option<StreamInfo> {
9692        let report = self.report()?;
9693        let (info, renderer) = {
9694            let mut info = report.info();
9695            let renderer = (info.backend == "wayland").then(report::renderer);
9696            if info.hardware && info.gpu.is_empty() {
9697                info.gpu = match &renderer {
9698                    Some(r) if render_node_index(&r.node) == Some(info.encode_node) => {
9699                        r.gpu.clone()
9700                    }
9701                    _ => node_gpu_name(info.encode_node),
9702                };
9703            }
9704            (info, renderer)
9705        };
9706        Some(StreamInfo { info, renderer })
9707    }
9708
9709    /// Cumulative counters of this capture since it started, or None without one: `frames`
9710    /// and `bytes` delivered, and the nanoseconds those frames spent encoding (`encode_ns`)
9711    /// and from capture to the end of the encode (`pipeline_ns`). A caller differences two
9712    /// reads; a restart begins again from zero. Beside them, the extremes since the previous
9713    /// read, which each read starts again: `pipeline_min_ns` and `pipeline_max_ns`, zero where
9714    /// no frame was timed, and `frame_max_bytes`, the largest frame.
9715    pub fn stream_stats(&self) -> Option<StreamStats> {
9716        let report = self.report()?;
9717        let totals = report.totals();
9718        let peaks = report.take_peaks();
9719        Some(StreamStats { totals, peaks })
9720    }
9721
9722    pub fn stop_capture(&self) -> Result<(), Error> {
9723        self.stop_internal()
9724    }
9725
9726    pub fn request_idr_frame(&self) -> Result<(), Error> {
9727        let (backend, controls, did) = {
9728            let st = self.inner.lock().unwrap();
9729            (st.backend, st.x11_controls(), st.wl_display)
9730        };
9731        match backend {
9732            0 | 1 => {
9733                if let Some(c) = controls {
9734                    c.force_idr.store(true, Ordering::Relaxed);
9735                }
9736            }
9737            2 => {
9738                if let Some(slot) = WAYLAND_BACKEND.get()
9739                    && let Some(be) = slot.lock().unwrap().as_ref()
9740                {
9741                    let _ = be.request_idr_frame(did);
9742                }
9743            }
9744            _ => {}
9745        }
9746        Ok(())
9747    }
9748
9749    /// A client lost frame `frame_id`, or its sender withheld it to hold the stream to a rate
9750    /// (`withheld`): the frames after it stop predicting from it, so the next one decodes there
9751    /// without a keyframe. An encoder that cannot leave a frame out of its predictions codes a
9752    /// keyframe instead. Non-blocking, like `request_idr_frame`.
9753    pub fn invalidate_reference(&self, frame_id: u16, withheld: bool) -> Result<(), Error> {
9754        use encoders::reference::ReferenceReport;
9755        let report = if withheld {
9756            ReferenceReport::Withheld(frame_id)
9757        } else {
9758            ReferenceReport::Lost(frame_id)
9759        };
9760        self.hand_report(report);
9761        Ok(())
9762    }
9763
9764    /// Every client holds frame `frame_id`, or where not `held` was sent it (and may yet lose
9765    /// it): a session keeping long-term references pins the newest anchor they all hold and
9766    /// predicts its anchors from a frame they were all sent, so each client recovers on its own
9767    /// from a loss of any depth, without a keyframe. Non-blocking, like `invalidate_reference`.
9768    pub fn acknowledge_reference(&self, frame_id: u16, held: bool) -> Result<(), Error> {
9769        use encoders::reference::ReferenceReport;
9770        let report = if held {
9771            ReferenceReport::Held(frame_id)
9772        } else {
9773            ReferenceReport::Sent(frame_id)
9774        };
9775        self.hand_report(report);
9776        Ok(())
9777    }
9778
9779    /// Apply a live target-bitrate (kbps) change to the running capture.
9780    ///
9781    /// On the X11 path the dirty flag is Release-published after the payload store, so the encode
9782    /// thread's Acquire read can never observe the flag set against a stale bitrate.
9783    pub fn update_video_bitrate(&self, kbps: i32) -> Result<(), Error> {
9784        let (backend, controls, did) = {
9785            let st = self.inner.lock().unwrap();
9786            (st.backend, st.x11_controls(), st.wl_display)
9787        };
9788        match backend {
9789            0 | 1 => {
9790                if let Some(c) = &controls {
9791                    c.bitrate_kbps.store(kbps, Ordering::Relaxed);
9792                    c.rate_dirty.store(true, Ordering::Release);
9793                }
9794            }
9795            2 => wayland_update_rate(did, Some(kbps), None, None),
9796            _ => {}
9797        }
9798        Ok(())
9799    }
9800
9801    /// Apply the rate (kbps) the path the stream crosses carries, as the transport measures it, which
9802    /// a still screen's constant-rate cleanup may run at; 0 where the transport has no measure,
9803    /// which keeps the cleanup to the stream's rate.
9804    pub fn update_link_rate(&self, kbps: i32) -> Result<(), Error> {
9805        let (backend, controls, did) = {
9806            let st = self.inner.lock().unwrap();
9807            (st.backend, st.x11_controls(), st.wl_display)
9808        };
9809        match backend {
9810            0 | 1 => {
9811                if let Some(c) = &controls {
9812                    c.link_kbps.store(kbps, Ordering::Relaxed);
9813                }
9814            }
9815            2 => wayland_update_link_rate(did, kbps),
9816            _ => {}
9817        }
9818        Ok(())
9819    }
9820
9821    pub fn update_framerate(&self, fps: f64) -> Result<(), Error> {
9822        let (backend, controls, did) = {
9823            let st = self.inner.lock().unwrap();
9824            (st.backend, st.x11_controls(), st.wl_display)
9825        };
9826        match backend {
9827            0 | 1 => {
9828                if let Some(c) = &controls {
9829                    c.fps_bits.store(fps.max(1.0).to_bits(), Ordering::Relaxed);
9830                    c.rate_dirty.store(true, Ordering::Release);
9831                    crate::x11::follow_frame_rate();
9832                }
9833            }
9834            2 => wayland_update_rate(did, None, None, Some(fps)),
9835            _ => {}
9836        }
9837        Ok(())
9838    }
9839
9840    /// Live CBR VBV change, as a multiple of one frame's bit budget (<= 0 = policy default).
9841    pub fn update_vbv_multiplier(&self, multiplier: f64) -> Result<(), Error> {
9842        let (backend, controls, did) = {
9843            let st = self.inner.lock().unwrap();
9844            (st.backend, st.x11_controls(), st.wl_display)
9845        };
9846        match backend {
9847            0 | 1 => {
9848                if let Some(c) = &controls {
9849                    c.vbv_mult_milli
9850                        .store((multiplier * 1000.0).round() as i32, Ordering::Relaxed);
9851                    c.rate_dirty.store(true, Ordering::Release);
9852                }
9853            }
9854            2 => wayland_update_rate(did, None, Some(multiplier), None),
9855            _ => {}
9856        }
9857        Ok(())
9858    }
9859
9860    /// Move/resize the live X11 capture region (root-relative). The capture loop drains
9861    /// in-flight frames, re-targets its surfaces, and the encoder follows in place where
9862    /// it can (NVENC reconfigure / stripe re-derive) -- no capture restart. `width`/
9863    /// `height` <= 0 mean "to the root edge". On Wayland the output IS the capture
9864    /// region: restart the capture with new dimensions instead (in-place there too).
9865    pub fn update_capture_region(
9866        &self,
9867        x: i32,
9868        y: i32,
9869        width: i32,
9870        height: i32,
9871    ) -> Result<(), Error> {
9872        let controls = {
9873            let st = self.inner.lock().unwrap();
9874            if st.backend == 2 {
9875                return Err(Error::Capture("update_capture_region is X11-only; on Wayland restart the capture with new dimensions".into()));
9876            }
9877            st.x11_controls()
9878        };
9879        if let Some(c) = &controls {
9880            *c.region.lock().unwrap() = (x.max(0), y.max(0), width, height);
9881            c.region_dirty.store(true, Ordering::Release);
9882        }
9883        Ok(())
9884    }
9885
9886    pub fn is_capturing(&self) -> bool {
9887        let st = self.inner.lock().unwrap();
9888        match st.backend {
9889            1 => st
9890                .controls
9891                .as_ref()
9892                .map(|c| !c.stop.load(Ordering::Relaxed))
9893                .unwrap_or(false),
9894            2 => {
9895                wayland_owners().lock().unwrap().get(&st.wl_display) == Some(&self.id)
9896                    && wayland_alive().lock().unwrap().contains(&st.wl_display)
9897            }
9898            _ => false,
9899        }
9900    }
9901
9902    pub fn inject_key(&self, scancode: u32, state: u32) -> Result<(), Error> {
9903        running_compositor().map_or(Ok(()), |be| be.inject_key(scancode, state))
9904    }
9905
9906    /// Inject an ordered run of `(keycode, state)` events in one message.
9907    pub fn inject_keys(&self, events: Vec<(u32, u32)>) -> Result<(), Error> {
9908        running_compositor().map_or(Ok(()), |be| be.inject_keys(events))
9909    }
9910
9911    pub fn set_keymap_string(&self, text: String) -> Result<(), Error> {
9912        running_compositor().map_or(Ok(()), |be| be.set_keymap_string(text))
9913    }
9914
9915    /// Bind explicit `(keycode, keysym)` pairs onto the current base keymap in one swap;
9916    /// false when no backend runs or no base keymap is installed. See the backend method
9917    /// for the split of responsibility.
9918    pub fn set_keymap_overlay(&self, binds: Vec<(u32, u32)>) -> Result<(), Error> {
9919        running_compositor().map_or(Ok(()), |be| be.set_keymap_overlay(binds))
9920    }
9921
9922    /// Compositor apps run under (a nested labwc/kwin session pixelflux captures),
9923    /// the target for Computer-Use text injection and, under KWin, for relative
9924    /// pointer motion through its fake-input device; selkies resolves it and hands
9925    /// it over. Empty clears it. Stored process-wide since the CU server is per-process.
9926    pub fn set_app_wayland_display(&self, display: String) {
9927        crate::computer_use::set_app_wayland_display(if display.is_empty() {
9928            None
9929        } else {
9930            Some(display)
9931        });
9932    }
9933
9934    /// Type `text` through `display`'s zwp_virtual_keyboard_manager_v1 as a one-shot
9935    /// client: selkies' text-injection path, targeting whichever compositor the apps
9936    /// live under (the nested session's, or pixelflux's own in a direct session).
9937    /// Blocking; releases the GIL for the duration. Raises
9938    /// [`VirtualKeyboardUnavailable`] when the compositor lacks the protocol.
9939    pub fn type_text_wayland(&self, display: &str, text: &str) -> Result<(), Error> {
9940        {
9941            let path = crate::wayland::wlclient::socket_path(display)
9942                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
9943            crate::wayland::vkclient::type_text_to(&path, text)
9944        }
9945        .map_err(|e: String| {
9946            if e.contains("zwp_virtual_keyboard_manager_v1") {
9947                Error::Unavailable(e)
9948            } else {
9949                Error::Capture(e)
9950            }
9951        })
9952    }
9953
9954    /// Tap `keysyms` in order through `display`'s virtual keyboard, verbatim: the
9955    /// caller owns which keysym spells which character (selkies' policy layer);
9956    /// this owns delivery. Same one-shot client and errors as `type_text_wayland`.
9957    pub fn type_keysyms_wayland(&self, display: &str, keysyms: &[u32]) -> Result<(), Error> {
9958        {
9959            let path = crate::wayland::wlclient::socket_path(display)
9960                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
9961            crate::wayland::vkclient::type_keysyms_to(&path, keysyms)
9962        }
9963        .map_err(|e: String| {
9964            if e.contains("zwp_virtual_keyboard_manager_v1") {
9965                Error::Unavailable(e)
9966            } else {
9967                Error::Capture(e)
9968            }
9969        })
9970    }
9971
9972    /// Scale the app compositor's `index`-th screen, so its applications draw
9973    /// larger while the capture keeps its full resolution. False when that
9974    /// compositor manages no outputs for clients (KWin), whose scale follows
9975    /// the capture output's instead.
9976    pub fn set_app_output_scale(
9977        &self,
9978        display: &str,
9979        scale: f64,
9980        index: usize,
9981    ) -> Result<bool, Error> {
9982        {
9983            let path = crate::wayland::wlclient::socket_path(display)
9984                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
9985            crate::wayland::outclient::set_output_scale(&path, index, scale)
9986        }
9987        .map(|outcome| matches!(outcome, crate::wayland::outclient::ScaleOutcome::Applied))
9988        .map_err(Error::Capture)
9989    }
9990
9991    /// Give the app compositor's `index`-th screen this mode and scale in one
9992    /// configuration, so the session lays its desktop out once. Setting them
9993    /// separately exposes a geometry that never exists — the pre-connect mode at
9994    /// the new scale — and a client that does not lay out again keeps it.
9995    /// False = that compositor manages no outputs for clients: a nested KWin
9996    /// accepts a scale over its own output management and then ignores it, its
9997    /// screens taking their scale from the host window's preferred fractional
9998    /// scale, which is the capture output's.
9999    pub fn set_app_screen_geometry(
10000        &self,
10001        display: &str,
10002        width: i32,
10003        height: i32,
10004        scale: f64,
10005        index: usize,
10006    ) -> Result<bool, Error> {
10007        {
10008            let path = crate::wayland::wlclient::socket_path(display)
10009                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
10010            crate::wayland::outclient::set_screen_geometry(&path, index, (width, height), scale)
10011        }
10012        .map(|outcome| matches!(outcome, crate::wayland::outclient::ScaleOutcome::Applied))
10013        .map_err(Error::Capture)
10014    }
10015
10016    /// Lay the app compositor's screens out at `rects` — `(x, y, width, height)`
10017    /// per screen in screen order — so the session arranges its desktop the way
10018    /// the capture outputs were placed rather than by its own default rule.
10019    /// A fallback ladder: `zwlr_output_management_v1` first, then KWin's
10020    /// `kde_output_management_v2` (positions only there — a nested KWin
10021    /// screen's size follows its host window). Returns how many were
10022    /// positioned; 0 = that compositor manages no outputs for clients.
10023    pub fn set_app_screen_layout(
10024        &self,
10025        display: &str,
10026        rects: Vec<(i32, i32, i32, i32)>,
10027    ) -> Result<usize, Error> {
10028        {
10029            let path = crate::wayland::wlclient::socket_path(display)
10030                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
10031            let placed = crate::wayland::outclient::set_screen_layout(&path, rects.clone())?;
10032            if placed == 0 {
10033                crate::wayland::kdeclient::set_screen_layout(&path, &rects)
10034            } else {
10035                Ok(placed)
10036            }
10037        }
10038        .map_err(Error::Capture)
10039    }
10040
10041    /// The app compositor's enabled screens as `(name, x, y, width, height)` in
10042    /// screen order, which is what it did with a layout and a mode rather than
10043    /// what it was asked for — the readback `set_app_screen_geometry` has no
10044    /// other way to be checked against. The same ladder as
10045    /// `set_app_screen_layout` (zwlr, then KWin's output devices). Empty =
10046    /// that compositor manages no outputs for clients.
10047    pub fn list_app_screens(&self, display: &str) -> Result<Vec<crate::wayland::AppScreen>, Error> {
10048        {
10049            let path = crate::wayland::wlclient::socket_path(display)
10050                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
10051            let screens = crate::wayland::outclient::list_screens(&path)?;
10052            if screens.is_empty() {
10053                crate::wayland::kdeclient::list_screens(&path)
10054            } else {
10055                Ok(screens)
10056            }
10057        }
10058        .map_err(Error::Capture)
10059    }
10060
10061    /// Whether the app compositor serves KWin's `zkde_screencast_unstable_v1`
10062    /// virtual outputs -- the rung selkies offers a second display on when the
10063    /// session serves no control socket. A registry read that touches no
10064    /// screen: whether the compositor registers the screen it grows is proven
10065    /// when a display asks for one (`add_app_screen` refuses one that does not).
10066    pub fn app_screen_control_offered(&self, display: &str) -> Result<bool, Error> {
10067        {
10068            let path = crate::wayland::wlclient::socket_path(display)
10069                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
10070            crate::wayland::kdeclient::screen_control_offered(&path)
10071        }
10072        .map_err(Error::Capture)
10073    }
10074
10075    /// Grow a screen named `name` on the app compositor, held open until
10076    /// `remove_app_screen`. The name is what `list_app_screens` reports and
10077    /// removal addresses; `width`/`height`/`scale` only seed the screen, whose
10078    /// size follows the capture output that adopts its host window.
10079    pub fn add_app_screen(
10080        &self,
10081        display: &str,
10082        name: &str,
10083        width: i32,
10084        height: i32,
10085        scale: f64,
10086    ) -> Result<(), Error> {
10087        {
10088            let path = crate::wayland::wlclient::socket_path(display)
10089                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
10090            crate::wayland::kdeclient::add_screen(&path, name, (width, height), scale)
10091        }
10092        .map_err(Error::Capture)
10093    }
10094
10095    /// Remove a screen `add_app_screen` grew; the compositor's own
10096    /// output-destroy path returns its windows to the primary. False when no
10097    /// such screen is held.
10098    pub fn remove_app_screen(&self, name: &str) -> Result<bool, Error> {
10099        Ok(crate::wayland::kdeclient::remove_screen(name))
10100    }
10101
10102    /// Hold the app compositor's screens past the first `keep` at a small size,
10103    /// so a session that opened more screens than there are capture outputs does
10104    /// not lay its desktop out across one nobody sees. Returns how many were
10105    /// resized (0 = that compositor manages no outputs for clients).
10106    pub fn hold_spare_app_screens(
10107        &self,
10108        display: &str,
10109        keep: usize,
10110        width: i32,
10111        height: i32,
10112    ) -> Result<usize, Error> {
10113        {
10114            let path = crate::wayland::wlclient::socket_path(display)
10115                .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
10116            crate::wayland::outclient::hold_spare_screens(&path, keep, (width, height))
10117        }
10118        .map_err(Error::Capture)
10119    }
10120
10121    /// Mimes the app compositor's current selection offers (empty = nothing copied).
10122    pub fn clipboard_types_app(&self, display: &str) -> Result<Vec<String>, Error> {
10123        (crate::wayland::dcclient::list_types(&app_socket_path(display)?)).map_err(Error::Capture)
10124    }
10125
10126    /// The app compositor selection's payload for `mime`, or None when nothing is
10127    /// copied or the selection does not offer that mime.
10128    pub fn clipboard_read_app(&self, display: &str, mime: &str) -> Result<Option<Vec<u8>>, Error> {
10129        let data = (crate::wayland::dcclient::read(&app_socket_path(display)?, mime))
10130            .map_err(Error::Capture)?;
10131        Ok(data)
10132    }
10133
10134    /// Take the app compositor's selection, serving `entries` (mime, bytes) to
10135    /// every paster from a background thread until another client copies.
10136    pub fn clipboard_write_app(
10137        &self,
10138        display: &str,
10139        entries: Vec<(String, Vec<u8>)>,
10140    ) -> Result<(), Error> {
10141        (crate::wayland::dcclient::write(&app_socket_path(display)?, entries))
10142            .map_err(Error::Capture)
10143    }
10144
10145    /// The mimes the selection `clipboard_write_app` last took has served to pasters,
10146    /// in order, once it has served more than `after` of them or `timeout_s` has
10147    /// passed (at most a minute).
10148    pub fn clipboard_served_app(&self, after: usize, timeout_s: f64) -> Vec<String> {
10149        let timeout = Duration::from_secs_f64(if timeout_s.is_finite() {
10150            timeout_s.clamp(0.0, 60.0)
10151        } else {
10152            0.0
10153        });
10154        crate::wayland::dcclient::served(after, timeout)
10155    }
10156
10157    /// Drop the app compositor's selection.
10158    pub fn clipboard_clear_app(&self, display: &str) -> Result<(), Error> {
10159        (crate::wayland::dcclient::clear(&app_socket_path(display)?)).map_err(Error::Capture)
10160    }
10161
10162    /// Stop the selection watch for `display` (no-op without one).
10163    pub fn clipboard_unwatch_app(&self, display: &str) {
10164        if let Ok(path) = app_socket_path(display) {
10165            crate::wayland::dcclient::unwatch(&path);
10166        }
10167    }
10168
10169    /// The move's number (`pointer_location`), or None without a running compositor.
10170    pub fn inject_mouse_move(&self, x: f64, y: f64) -> Result<Option<u64>, Error> {
10171        running_compositor().map_or(Ok(None), |be| be.inject_mouse_move(x, y).map(Some))
10172    }
10173
10174    /// The move's number (`pointer_location`), or None without a running compositor.
10175    pub fn inject_relative_mouse_move(&self, dx: f64, dy: f64) -> Result<Option<u64>, Error> {
10176        running_compositor().map_or(Ok(None), |be| {
10177            be.inject_relative_mouse_move(dx, dy).map(Some)
10178        })
10179    }
10180
10181    pub fn inject_mouse_button(&self, btn: u32, state: u32) -> Result<(), Error> {
10182        running_compositor().map_or(Ok(()), |be| be.inject_mouse_button(btn, state))
10183    }
10184
10185    pub fn inject_mouse_scroll(&self, x: f64, y: f64) -> Result<(), Error> {
10186        running_compositor().map_or(Ok(()), |be| be.inject_mouse_scroll(x, y))
10187    }
10188
10189    /// `WaylandBackend.inject_finger_scroll`; nothing without a running compositor.
10190    pub fn inject_finger_scroll(&self, dx: f64, dy: f64) -> Result<(), Error> {
10191        running_compositor().map_or(Ok(()), |be| be.inject_finger_scroll(dx, dy))
10192    }
10193
10194    /// `WaylandBackend.inject_finger_scroll_end`; nothing without a running compositor.
10195    pub fn inject_finger_scroll_end(&self) -> Result<(), Error> {
10196        running_compositor().map_or(Ok(()), |be| be.inject_finger_scroll_end())
10197    }
10198
10199    /// Where the Wayland compositor's pointer is, as `(x, y, scale, motion)`
10200    /// (`WaylandBackend.pointer_location`), or None without a running compositor or before its
10201    /// first pass. Under host capture the host's cursor answers, None before its first cursor
10202    /// meta or where the host paints its cursor. An X11 session reads its pointer from the X server.
10203    pub fn pointer_location(&self) -> Option<(f64, f64, f64, u64)> {
10204        running_compositor().and_then(|be| be.pointer_location())
10205    }
10206
10207    /// Toggle compositing the cursor into captured frames (the alternative to the
10208    /// out-of-band cursor callback): the X11 grab re-reads the flag per frame, Wayland
10209    /// forwards to the compositor.
10210    pub fn set_cursor_rendering(&self, enabled: bool) -> Result<(), Error> {
10211        let controls = self.inner.lock().unwrap().x11_controls();
10212        if let Some(c) = &controls {
10213            c.capture_cursor.store(enabled, Ordering::Relaxed);
10214            return Ok(());
10215        }
10216        running_compositor().map_or(Ok(()), |be| be.set_cursor_rendering(enabled))
10217    }
10218
10219    pub fn get_xkb_keymap_string(&self) -> Result<String, Error> {
10220        running_compositor().map_or(Ok(String::new()), |be| be.get_xkb_keymap_string())
10221    }
10222
10223    /// Compositor-side clipboard offer: serve one payload per `(mime, data)` entry.
10224    pub fn set_clipboard(&self, entries: Vec<(String, Vec<u8>)>) -> Result<(), Error> {
10225        match running_compositor() {
10226            Some(be) => be.set_clipboard(entries),
10227            None => Err(Error::Capture("wayland backend not running".into())),
10228        }
10229    }
10230
10231    /// Set the seat's BASE xkb layout from RMLVO names; false when no backend runs or the
10232    /// layout fails to compile.
10233    pub fn set_xkb_layout(
10234        &self,
10235        layout: String,
10236        variant: String,
10237        options: String,
10238        model: String,
10239        rules: String,
10240    ) -> Result<bool, Error> {
10241        running_compositor().map_or(Ok(false), |be| {
10242            be.set_xkb_layout(layout, variant, options, model, rules)
10243        })
10244    }
10245
10246    /// Seat keyboard readback: `(pressed_keycodes, modifier_mask)`; empty when no backend runs.
10247    pub fn get_keyboard_state(&self) -> Result<(Vec<u32>, u32), Error> {
10248        running_compositor().map_or(Ok((Vec::new(), 0)), |be| be.get_keyboard_state())
10249    }
10250
10251    /// The capture geometry actually live on the given display `(width, height, scale)`;
10252    /// `None` when no Wayland backend runs or the compositor did not answer in time (a
10253    /// timeout is distinct from a real answer so the caller treats it as unknown rather than
10254    /// "nothing to reconcile"). X11 has no compositor to read back and returns `None`.
10255    pub fn get_realized_geometry(&self, display_id: u32) -> Result<Option<(i32, i32, f64)>, Error> {
10256        running_compositor().map_or(Ok(None), |be| be.get_realized_geometry(display_id))
10257    }
10258
10259    /// Lifecycle of this capture as `(state, last_error)`: `state` is `"running"`, `"failed"`,
10260    /// or `"idle"`; `last_error` gives the reason a start failed, or a caveat a live capture
10261    /// came up with (encoder fell back to CPU, host connect refused, a refused resize). The
10262    /// Wayland outcome is recorded by the compositor thread; the X11 outcome is the capture
10263    /// thread's own exit error. `display_id` selects a Wayland output; X11 ignores it.
10264    pub fn capture_state(&self, display_id: u32) -> (String, Option<String>) {
10265        let (backend, running, err) = {
10266            let st = self.inner.lock().unwrap();
10267            let running = st
10268                .controls
10269                .as_ref()
10270                .map(|c| !c.stop.load(Ordering::Relaxed))
10271                .unwrap_or(false);
10272            let err = st
10273                .err
10274                .as_ref()
10275                .and_then(|e| e.lock().ok().and_then(|g| g.clone()));
10276            (st.backend, running, err)
10277        };
10278        match backend {
10279            1 => {
10280                let state = if running {
10281                    "running"
10282                } else if err.is_some() {
10283                    "failed"
10284                } else {
10285                    "idle"
10286                };
10287                (state.to_string(), err)
10288            }
10289            2 => wayland_capture_state(display_id),
10290            _ => ("idle".to_string(), None),
10291        }
10292    }
10293
10294    /// Create an additional Wayland output (see `WaylandBackend.create_output`); false when
10295    /// no backend runs.
10296    pub fn create_output(
10297        &self,
10298        id: u32,
10299        width: i32,
10300        height: i32,
10301        x: i32,
10302        y: i32,
10303        scale: f64,
10304    ) -> Result<bool, Error> {
10305        running_compositor().map_or(Ok(false), |be| {
10306            be.create_output(id, width, height, x, y, scale)
10307        })
10308    }
10309
10310    /// Add a display over a rectangle of an existing Wayland output (see
10311    /// `WaylandBackend.create_view`); false when no backend runs.
10312    pub fn create_view(
10313        &self,
10314        id: u32,
10315        owner: u32,
10316        x: i32,
10317        y: i32,
10318        width: i32,
10319        height: i32,
10320    ) -> Result<bool, Error> {
10321        running_compositor().map_or(Ok(false), |be| {
10322            be.create_view(id, owner, x, y, width, height)
10323        })
10324    }
10325
10326    /// Resize a Wayland screen's output in place (see `WaylandBackend.resize_output`);
10327    /// false when no backend runs.
10328    pub fn resize_output(
10329        &self,
10330        id: u32,
10331        width: i32,
10332        height: i32,
10333        scale: f64,
10334    ) -> Result<bool, Error> {
10335        running_compositor().map_or(Ok(false), |be| be.resize_output(id, width, height, scale))
10336    }
10337
10338    /// Destroy a secondary Wayland output; false when no backend runs.
10339    pub fn destroy_output(&self, id: u32) -> Result<bool, Error> {
10340        running_compositor().map_or(Ok(false), |be| be.destroy_output(id))
10341    }
10342
10343    /// Move a Wayland output (the primary included) to layout offset `(x, y)`; false when
10344    /// no backend runs or the id is unknown.
10345    pub fn reposition_output(&self, id: u32, x: i32, y: i32) -> Result<bool, Error> {
10346        running_compositor().map_or(Ok(false), |be| be.reposition_output(id, x, y))
10347    }
10348
10349    /// Recreate the Wayland cursor theme at `size` pixels (named-cursor callbacks and the
10350    /// burned-in overlay); false when no backend runs or the size is non-positive.
10351    pub fn set_cursor_size(&self, size: i32) -> Result<bool, Error> {
10352        running_compositor().map_or(Ok(false), |be| be.set_cursor_size(size))
10353    }
10354
10355    /// Every live Wayland output as `(id, x, y, width, height, scale, capturing)`; empty
10356    /// when no backend runs.
10357    pub fn list_outputs(&self) -> Result<Vec<OutputDesc>, Error> {
10358        running_compositor().map_or(Ok(Vec::new()), |be| be.list_outputs())
10359    }
10360
10361    /// Display capacity (see `WaylandBackend.output_capacity`); -1 when no backend runs.
10362    pub fn output_capacity(&self) -> Result<i64, Error> {
10363        running_compositor().map_or(Ok(-1), |be| be.output_capacity())
10364    }
10365
10366    /// Move a window onto an output (fullscreened there); false when no backend runs.
10367    pub fn move_window_to_output(&self, window_id: u32, output_id: u32) -> Result<bool, Error> {
10368        running_compositor().map_or(Ok(false), |be| {
10369            be.move_window_to_output(window_id, output_id)
10370        })
10371    }
10372
10373    /// Every mapped window as `(window_id, title, app_id, output_id, waiting)`; empty when
10374    /// no backend runs.
10375    pub fn list_windows(&self) -> Result<Vec<WindowDesc>, Error> {
10376        running_compositor().map_or(Ok(Vec::new()), |be| be.list_windows())
10377    }
10378
10379    /// Begin a capture delivering every frame to `sink`, on the backend `settings` select
10380    /// (`CaptureSettings::wants_wayland`): the shared compositor's display `display_id`, or
10381    /// an X11 capture of this instance's own. A start on a Wayland display this instance
10382    /// already captures reconfigures it in place; any other start stops what ran before.
10383    pub fn start_capture(
10384        &self,
10385        sink: Arc<dyn crate::sink::FrameSink>,
10386        settings: CaptureSettings,
10387    ) -> Result<(), Error> {
10388        let rs = settings;
10389        crate::log::set_debug(rs.debug_logging);
10390        let display_id = rs.display_id;
10391        let wants_wayland = rs.wants_wayland();
10392        let live_wayland_restart = wants_wayland
10393            && {
10394                let st = self.inner.lock().unwrap();
10395                st.backend == 2 && st.wl_display == display_id
10396            }
10397            && wayland_owners().lock().unwrap().get(&display_id) == Some(&self.id)
10398            && wayland_alive().lock().unwrap().contains(&display_id);
10399        if !live_wayland_restart {
10400            self.stop_internal()?;
10401        }
10402
10403        if wants_wayland {
10404            let be = ensure_compositor(
10405                rs.width,
10406                rs.height,
10407                rs.render_node_path.clone(),
10408                rs.auto_gpu.clone(),
10409                rs.encode_node_path.clone(),
10410                rs.cursor_size,
10411            )?;
10412            be.start_capture(sink, rs)?;
10413            wayland_owners().lock().unwrap().insert(display_id, self.id);
10414            {
10415                let mut st = self.inner.lock().unwrap();
10416                st.backend = 2;
10417                st.wl_display = display_id;
10418            }
10419            return Ok(());
10420        }
10421
10422        // A fresh start proves the interpreter is alive: clear the teardown flag the atexit
10423        // sweep may have set (the Wayland start path already does the same), or the delivery
10424        // thread below would drop every frame.
10425        SHUTDOWN.store(false, Ordering::Relaxed);
10426        let mut rs = rs;
10427        if rs.encode_node_index < -1 {
10428            if let Some(picked) = auto_render_node(&rs.auto_gpu)
10429                && let Some(idx) = render_node_index(&picked)
10430            {
10431                println!("[X11] AUTO_GPU selected {picked}.");
10432                rs.encode_node_index = idx;
10433            } else if let Some(idx) = first_render_node_index() {
10434                rs.encode_node_index = idx;
10435            }
10436        }
10437
10438        println!(
10439            "[X11] Configuring Output: {}x{} @ {:.2} FPS (Encode Node: {})",
10440            rs.width, rs.height, rs.target_fps, rs.encode_node_index
10441        );
10442
10443        let controls = Arc::new(crate::x11::Controls::new(&rs));
10444        crate::x11::cursor::set_size_cap(rs.cursor_size_cap);
10445        live_x11().lock().unwrap().push(controls.clone());
10446        if let Some(other) = self
10447            .inner
10448            .lock()
10449            .unwrap()
10450            .starting
10451            .replace(controls.clone())
10452        {
10453            other.stop.store(true, Ordering::Relaxed);
10454        }
10455        let c2 = controls.clone();
10456        let c3 = controls.clone();
10457        let cb = sink;
10458        let err_slot: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
10459        let err_slot2 = err_slot.clone();
10460
10461        // The consumer runs on its own thread (as the Wayland backend and pcmflux
10462        // do) so encode(N+1) never serializes behind deliver(N). Bounded at one
10463        // in-flight frame: a slower consumer backpressures the encoder by exactly
10464        // one frame instead of growing a queue, and no frame is ever dropped.
10465        let (deliver_tx, deliver_rx) = std::sync::mpsc::sync_channel::<Vec<EncodedStripe>>(1);
10466        let deliver_handle = thread::spawn(move || {
10467            crate::boost_thread_priority(-10);
10468            while let Ok(frame) = deliver_rx.recv() {
10469                if SHUTDOWN.load(Ordering::Relaxed) {
10470                    continue;
10471                }
10472                cb.frames(frame);
10473            }
10474        });
10475        let deliver_thread_id = deliver_handle.thread().id();
10476
10477        let report = controls.report.clone();
10478        let on_frame = move |frame: Vec<EncodedStripe>| {
10479            report.tally(&frame);
10480            // Blocks only when the single slot is still occupied (consumer more
10481            // than one frame behind); a dropped receiver (stop) discards.
10482            let _ = deliver_tx.send(frame);
10483        };
10484
10485        let (tid_tx, tid_rx) = std::sync::mpsc::channel();
10486        let (etid_tx, etid_rx) = std::sync::mpsc::channel();
10487        let handle = thread::spawn(move || {
10488            crate::boost_thread_priority(-15);
10489            let _ = tid_tx.send(thread::current().id());
10490            let res = crate::x11::run_capture(rs, c2, etid_tx, on_frame);
10491            c3.stop.store(true, Ordering::Release);
10492            if let Err(msg) = res {
10493                eprintln!("[X11] capture error: {msg}");
10494                if let Ok(mut g) = err_slot2.lock() {
10495                    *g = Some(msg);
10496                }
10497            }
10498            // run_capture has joined its encode thread by now (NVENC/CUDA session dropped),
10499            // so the atexit sweep may finalize the interpreter without racing that drop.
10500            c3.finished.store(true, Ordering::Release);
10501        });
10502        let (tid, etid_res, etid_rx) = {
10503            let tid = tid_rx.recv().ok();
10504            let etid_res = etid_rx.recv_timeout(std::time::Duration::from_secs(2));
10505            (tid, etid_res, etid_rx)
10506        };
10507        let mut late_etid_rx = None;
10508        let etid = match etid_res {
10509            Ok(id) => Some(id),
10510            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
10511                let _ = handle.join();
10512                live_x11()
10513                    .lock()
10514                    .unwrap()
10515                    .retain(|x| !Arc::ptr_eq(x, &controls));
10516                let mut st = self.inner.lock().unwrap();
10517                if !st
10518                    .starting
10519                    .as_ref()
10520                    .is_some_and(|c| Arc::ptr_eq(c, &controls))
10521                {
10522                    return Ok(());
10523                }
10524                st.starting = None;
10525                drop(st);
10526                let msg = err_slot
10527                    .lock()
10528                    .ok()
10529                    .and_then(|g| g.clone())
10530                    .unwrap_or_else(|| "X11 capture thread exited during start".to_string());
10531                return Err(Error::Capture(msg));
10532            }
10533            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
10534                // Slow pre-spawn X11 setup: the id will still arrive on this
10535                // channel. Keep the receiver so stop_internal can late-resolve
10536                // it — a None id would blind the re-entrant-stop guard.
10537                late_etid_rx = Some(etid_rx);
10538                None
10539            }
10540        };
10541        let mut st = self.inner.lock().unwrap();
10542        if !st
10543            .starting
10544            .as_ref()
10545            .is_some_and(|c| Arc::ptr_eq(c, &controls))
10546        {
10547            drop(st);
10548            live_x11()
10549                .lock()
10550                .unwrap()
10551                .retain(|x| !Arc::ptr_eq(x, &controls));
10552            {
10553                join_within(handle, "capture");
10554                join_within(deliver_handle, "delivery");
10555            };
10556            return Ok(());
10557        }
10558        st.starting = None;
10559        // Take the monitor reference BEFORE publishing the capture: a stop that
10560        // observes this capture must always find the reference it is to release.
10561        crate::x11::cursor::acquire();
10562        st.backend = 1;
10563        st.cursor_ref = true;
10564        st.controls = Some(controls);
10565        st.handle = Some(handle);
10566        st.cap_thread_id = tid;
10567        st.encode_thread_id = etid;
10568        st.encode_tid_rx = late_etid_rx;
10569        st.deliver_handle = Some(deliver_handle);
10570        st.deliver_thread_id = Some(deliver_thread_id);
10571        st.err = Some(err_slot);
10572        drop(st);
10573        Ok(())
10574    }
10575
10576    /// Retune the live per-frame knobs (`LiveTunables::from_settings`) of a running capture
10577    /// without restarting it.
10578    pub fn update_tunables(&self, settings: &CaptureSettings) -> Result<(), Error> {
10579        let rs = settings;
10580        let t = LiveTunables::from_settings(rs);
10581        let (backend, controls, did) = {
10582            let st = self.inner.lock().unwrap();
10583            (st.backend, st.x11_controls(), st.wl_display)
10584        };
10585        match backend {
10586            0 | 1 => {
10587                if let Some(c) = &controls {
10588                    c.capture_cursor.store(t.capture_cursor, Ordering::Relaxed);
10589                    *c.tunables.lock().unwrap() = Some(t);
10590                    c.tunables_dirty.store(true, Ordering::Release);
10591                    crate::x11::cursor::set_size_cap(rs.cursor_size_cap);
10592                }
10593            }
10594            2 => wayland_update_tunables(did, t),
10595            _ => {}
10596        }
10597        Ok(())
10598    }
10599
10600    /// Register the consumer of what client applications copy to the compositor's clipboard.
10601    pub fn set_clipboard_sink(
10602        &self,
10603        sink: Arc<dyn crate::sink::ClipboardSink>,
10604    ) -> Result<(), Error> {
10605        match running_compositor() {
10606            Some(be) => be.set_clipboard_sink(sink),
10607            None => Err(Error::Compositor("wayland backend not running".into())),
10608        }
10609    }
10610
10611    /// Report every selection change of the application compositor `display` to `watch`,
10612    /// from a thread of its own; a second watch on the same display replaces the first.
10613    pub fn clipboard_watch_app(
10614        &self,
10615        display: &str,
10616        watch: Arc<dyn crate::sink::SelectionWatch>,
10617    ) -> Result<(), Error> {
10618        crate::wayland::dcclient::watch(&app_socket_path(display)?, watch).map_err(Error::Capture)
10619    }
10620}
10621
10622/// Best-effort teardown: flag the capture thread to exit without joining.
10623///
10624/// Joining would need the GIL (the thread calls back into Python), which `Drop` cannot safely take,
10625/// so this only sets the stop flag; the actual join is left to an explicit `stop_capture` or the
10626/// atexit sweep.
10627impl Drop for ScreenCapture {
10628    fn drop(&mut self) {
10629        if let Ok(mut st) = self.inner.lock() {
10630            if let Some(c) = &st.controls {
10631                c.stop.store(true, Ordering::Relaxed);
10632            }
10633            // A Wayland capture is owned by this instance: release it, or a GC'd handle
10634            // would leave the compositor encoding and delivering forever.
10635            if st.backend == 2 && !crate::SHUTDOWN.load(Ordering::Relaxed) {
10636                let did = st.wl_display;
10637                let owned = {
10638                    let mut owners = wayland_owners().lock().unwrap();
10639                    if owners.get(&did) == Some(&self.id) {
10640                        owners.remove(&did);
10641                        true
10642                    } else {
10643                        false
10644                    }
10645                };
10646                if owned
10647                    && let Some(slot) = WAYLAND_BACKEND.get()
10648                    && let Some(be) = slot.lock().unwrap().as_ref()
10649                {
10650                    let _ = be.stop_capture(did);
10651                }
10652            }
10653            // Pair the cursor-monitor acquire from start_capture: a dropped
10654            // handle that never got stop_capture would pin the refcount and its
10655            // monitor thread forever.
10656            if std::mem::take(&mut st.cursor_ref) && !crate::SHUTDOWN.load(Ordering::Relaxed) {
10657                crate::x11::cursor::release();
10658            }
10659        }
10660    }
10661}
10662
10663/// The `pixelflux` Python module: registers the exported classes and functions, and hooks
10664/// `_stop_all_captures` into `atexit` so every live capture is stopped before interpreter shutdown.
10665/// Socket path for a Wayland display name, with the ABI methods' error shape.
10666fn app_socket_path(display: &str) -> Result<String, String> {
10667    crate::wayland::wlclient::socket_path(display)
10668        .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())
10669}
10670
10671/// Stop every live capture ahead of the process's exit, and raise the consumer-gone gate first
10672/// so no detached thread calls a sink again.
10673///
10674/// An active recording is finalized first, so its last buffered MP4 sample is flushed and its
10675/// own capture stops through the normal path. The cursor sinks (the X11 monitor's and a
10676/// never-applied compositor stash) are dropped, every X11 capture's stop flag is set and each
10677/// thread waited for (bounded): it joins its own encode thread on the way out, dropping the
10678/// NVENC/CUDA session, and a process exiting while that drop is in flight segfaults. A live
10679/// Wayland capture is stopped over the command channel and the calloop waited for the same
10680/// way through a barrier. A brief grace sleep lets the stops be observed. The Python bridge
10681/// runs this from `atexit`; a Rust host calls it before `main` returns.
10682pub fn shutdown() {
10683    SHUTDOWN.store(true, Ordering::Relaxed);
10684    crate::recorder::finalize_on_exit();
10685    *PENDING_CURSOR_CALLBACK.lock().unwrap() = None;
10686    crate::x11::cursor::shutdown();
10687    crate::wayland::dcclient::unwatch_all();
10688    let x11: Vec<Arc<crate::x11::Controls>> = live_x11().lock().unwrap().iter().cloned().collect();
10689    for c in &x11 {
10690        c.stop.store(true, Ordering::Relaxed);
10691    }
10692    if !x11.is_empty() {
10693        let deadline = Instant::now() + Duration::from_secs(2);
10694        for c in &x11 {
10695            while !c.finished.load(Ordering::Acquire) && Instant::now() < deadline {
10696                std::thread::sleep(Duration::from_millis(5));
10697            }
10698        }
10699    }
10700    // The handle is cloned out of the slot so its mutex is released before the barrier wait:
10701    // a capture dropped meanwhile takes the same mutex.
10702    let be = WAYLAND_BACKEND
10703        .get()
10704        .and_then(|slot| slot.lock().unwrap().as_ref().cloned());
10705    if let Some(be) = be {
10706        let mut displays: Vec<u32> = wayland_alive().lock().unwrap().iter().copied().collect();
10707        if !displays.contains(&0) {
10708            displays.push(0);
10709        }
10710        for did in displays {
10711            let _ = be.stop_capture(did);
10712        }
10713        let (ack_tx, ack_rx) = std::sync::mpsc::channel::<()>();
10714        if be.send(ThreadCommand::Barrier { reply: ack_tx }).is_ok() {
10715            let _ = ack_rx.recv_timeout(Duration::from_secs(2));
10716        }
10717    }
10718    wayland_owners().lock().unwrap().clear();
10719    wayland_alive().lock().unwrap().clear();
10720    std::thread::sleep(Duration::from_millis(50));
10721}
10722
10723/// Start the built-in MP4 recorder on `path`: with `settings`, a recorder-owned H.264 capture
10724/// of the display and backend they name (any other codec is refused); without, the capture the
10725/// `PIXELFLUX_RECORD_*` environment describes. `audio_socket` names pcmflux's recording socket,
10726/// empty for none.
10727pub fn start_recording(
10728    path: String,
10729    settings: Option<CaptureSettings>,
10730    audio_socket: &str,
10731) -> Result<crate::recorder::RecordingStatus, Error> {
10732    let mut opts = crate::recorder::RecordOptions::from_env(path);
10733    if !audio_socket.is_empty() {
10734        opts.audio_socket = audio_socket.to_string();
10735    }
10736    if let Some(rs) = settings {
10737        if rs.codec != Codec::H264 {
10738            return Err(Error::Settings(
10739                "recording requires H.264 capture settings (codec='h264')".into(),
10740            ));
10741        }
10742        opts.display_id = rs.display_id;
10743        if let Some(explicit) = rs.use_wayland {
10744            opts.backend = Some(if explicit {
10745                crate::recorder::PreferredBackend::Wayland
10746            } else {
10747                crate::recorder::PreferredBackend::X11
10748            });
10749        }
10750        // Explicit settings are authoritative over the env knobs they subsume.
10751        opts.fps = 0.0;
10752        opts.bitrate_kbps = 0;
10753        opts.capture = Some(rs);
10754    }
10755    crate::recorder::start(opts).map_err(Error::Capture)
10756}
10757
10758/// Finalize the recording in progress and report what it wrote.
10759pub fn stop_recording() -> Result<crate::recorder::RecordingStatus, Error> {
10760    crate::recorder::stop().map_err(Error::Capture)
10761}
10762
10763/// The recording in progress or last finished, `None` where none was started.
10764pub fn recording_status() -> Option<crate::recorder::RecordingStatus> {
10765    crate::recorder::status()
10766}
10767
10768/// Bring up the process-wide compositor (`ensure_compositor`) and return its socket name, so a
10769/// consumer can place `WAYLAND_DISPLAY` for the clients it starts; empty where the compositor
10770/// published no socket within five seconds.
10771pub fn ensure_wayland_display(
10772    width: i32,
10773    height: i32,
10774    render_node: String,
10775    auto_gpu: String,
10776    cursor_size: i32,
10777) -> Result<String, Error> {
10778    ensure_compositor(
10779        width,
10780        height,
10781        render_node,
10782        auto_gpu,
10783        String::new(),
10784        cursor_size,
10785    )?;
10786    Ok(wait_socket_name(Duration::from_secs(5)).unwrap_or_default())
10787}
10788
10789/// What `probe_wayland_gpu` found: the render node tried, whether it renders in hardware, the
10790/// GL renderer's name, and why it was refused where it was.
10791#[derive(Clone, Debug, PartialEq, Eq)]
10792pub struct WaylandGpuProbe {
10793    /// The render node probed, empty where none was found.
10794    pub node: String,
10795    /// Whether the node renders in hardware (an allocation, an export, and a GL context that
10796    /// is not a software rasterizer).
10797    pub accelerated: bool,
10798    /// Whether the process sees a GPU at all (`/dev/dri`).
10799    pub gpu: bool,
10800    /// The GL renderer string of the node, empty where no context came up.
10801    pub renderer: String,
10802    /// Why the node was refused, empty when `accelerated`.
10803    pub error: String,
10804}
10805
10806/// Probe the render node the compositor would run on: `render_node` explicitly, else the
10807/// `auto_gpu` pick. Allocates one GBM buffer, exports it, and opens a GL context on it, so a
10808/// deployment learns before starting whether the compositor renders in hardware.
10809pub fn probe_wayland_gpu(render_node: &str, auto_gpu: &str) -> WaylandGpuProbe {
10810    let node = if render_node.is_empty() {
10811        auto_render_node(auto_gpu)
10812    } else {
10813        Some(render_node.to_string())
10814    };
10815    let Some(node) = node else {
10816        return WaylandGpuProbe {
10817            node: String::new(),
10818            accelerated: false,
10819            gpu: gpu_exposed(),
10820            renderer: String::new(),
10821            error: "No render node".to_string(),
10822        };
10823    };
10824    let mut name = String::new();
10825    let result = gpu_render_init(std::path::Path::new(&node)).and_then(|(gbm, mut renderer)| {
10826        let bo = gbm
10827            .create_buffer_object::<()>(64, 64, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
10828            .map_err(|_| "Failed to allocate GBM buffer")?;
10829        bo.fd()
10830            .map_err(|e| format!("Failed to export dmabuf: {e:?}"))?;
10831        name = gl_renderer_name(&mut renderer);
10832        let lowered = name.to_lowercase();
10833        if ["llvmpipe", "softpipe", "swrast", "software rasterizer"]
10834            .iter()
10835            .any(|sw| lowered.contains(sw))
10836        {
10837            return Err(format!("Software rasterizer only ({name})"));
10838        }
10839        Ok(())
10840    });
10841    let error = result.err().unwrap_or_default();
10842    WaylandGpuProbe {
10843        node,
10844        accelerated: error.is_empty(),
10845        gpu: gpu_exposed(),
10846        renderer: name,
10847        error,
10848    }
10849}
10850
10851/// The hardware encoders of the encode node `encode_node_index` names (`-1` for none, below
10852/// `-1` for the `auto_gpu` pick or the first render node), by codec: the backend's name and
10853/// the formats it codes the codec in.
10854pub fn hardware_encoders(encode_node_index: i32, auto_gpu: &str) -> encoders::HardwareEncoders {
10855    let node = match encode_node_index {
10856        -1 => return Vec::new(),
10857        index if index < -1 => auto_render_node(auto_gpu)
10858            .and_then(|picked| render_node_index(&picked))
10859            .or_else(first_render_node_index)
10860            .unwrap_or(0),
10861        index => index,
10862    };
10863    encoders::hardware_encoders(node)
10864}
10865
10866/// The socket name of the running compositor, `None` without one or where it published none
10867/// within two seconds.
10868pub fn wayland_display_name() -> Option<String> {
10869    running_compositor()?;
10870    wait_socket_name(Duration::from_secs(2))
10871}
10872
10873/// Start the standalone Computer-Use HTTP server on `bind`, every request carrying `token`
10874/// where one is given (`computer_use::server`).
10875#[cfg(feature = "computer-use-server")]
10876pub fn start_computer_use(bind: &str, token: Option<&str>) -> Result<(), Error> {
10877    crate::computer_use::server::start_cu_server(bind, token).map_err(Error::Capture)
10878}
10879
10880/// A PNG of display `display` as the capture backend sees it now.
10881pub fn screenshot_png(display: u32) -> Result<Vec<u8>, Error> {
10882    crate::computer_use::resolve_backend()
10883        .and_then(|b| b.screenshot_png(display))
10884        .map_err(Error::Capture)
10885}
10886
10887#[cfg(test)]
10888mod annexb_frame_type_tests {
10889    //! The wire type byte read from real encoder output: the vectors are x264
10890    //! access units (truncated past the classifier's decision point) whose
10891    //! ground truth came from libavcodec's own slice-header trace, an open-GOP
10892    //! stream being the one place non-IDR I frames occur.
10893    use crate::encoders::h264_frame_type as annexb_frame_type;
10894
10895    const IDR_AU: &[u8] = &[
10896        0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x0d, 0xac, 0xd9,
10897        0x41, 0x41, 0xfb, 0x01, 0x10, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x03, 0x03, 0xc0,
10898        0xf1, 0x42, 0x99, 0x60, 0x00, 0x00, 0x00, 0x01, 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, 0x00,
10899        0x00, 0x01, 0x06, 0x05, 0xff, 0xff, 0xa8, 0xdc, 0x45, 0xe9, 0xbd, 0xe6, 0xd9, 0x48, 0xb7,
10900        0x96, 0x2c, 0xd8, 0x20, 0xd9, 0x23, 0xee, 0xef, 0x78, 0x32, 0x36, 0x34, 0x20, 0x2d, 0x20,
10901        0x63, 0x6f, 0x72, 0x65, 0x20, 0x31, 0x36, 0x34, 0x20, 0x72, 0x33, 0x30, 0x39, 0x35, 0x20,
10902        0x62, 0x61, 0x65, 0x65, 0x34, 0x30, 0x30, 0x20, 0x2d, 0x20, 0x48, 0x2e, 0x32, 0x36, 0x34,
10903        0x2f, 0x4d, 0x50, 0x45, 0x47, 0x2d, 0x34, 0x20, 0x41, 0x56, 0x43, 0x20, 0x63, 0x6f, 0x64,
10904        0x65, 0x63, 0x20, 0x2d, 0x20, 0x43, 0x6f, 0x70, 0x79, 0x6c, 0x65, 0x66, 0x74, 0x20, 0x32,
10905        0x30, 0x30, 0x33, 0x2d, 0x32, 0x30, 0x32, 0x32, 0x20, 0x2d, 0x20, 0x68, 0x74, 0x74, 0x70,
10906        0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x6c, 0x61, 0x6e,
10907        0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x78, 0x32, 0x36, 0x34, 0x2e, 0x68, 0x74, 0x6d, 0x6c, 0x20,
10908        0x2d, 0x20, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x20, 0x63, 0x61, 0x62, 0x61,
10909        0x63, 0x3d, 0x31, 0x20, 0x72, 0x65, 0x66, 0x3d, 0x33, 0x20, 0x64, 0x65, 0x62, 0x6c, 0x6f,
10910        0x63, 0x6b, 0x3d, 0x31, 0x3a, 0x30, 0x3a, 0x30, 0x20, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73,
10911        0x65, 0x3d, 0x30, 0x78, 0x33, 0x3a, 0x30, 0x78, 0x31, 0x31, 0x33, 0x20, 0x6d, 0x65, 0x3d,
10912        0x68, 0x65, 0x78, 0x20, 0x73, 0x75, 0x62, 0x6d, 0x65, 0x3d, 0x37, 0x20, 0x70, 0x73, 0x79,
10913        0x3d, 0x31, 0x20, 0x70, 0x73, 0x79, 0x5f, 0x72, 0x64, 0x3d, 0x31, 0x2e, 0x30, 0x30, 0x3a,
10914        0x30, 0x2e, 0x30, 0x30, 0x20, 0x6d, 0x69, 0x78, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x66, 0x3d,
10915        0x31, 0x20, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x3d, 0x31, 0x36, 0x20, 0x63,
10916        0x68, 0x72, 0x6f, 0x6d, 0x61, 0x5f, 0x6d, 0x65, 0x3d, 0x31, 0x20, 0x74, 0x72, 0x65, 0x6c,
10917        0x6c, 0x69, 0x73, 0x3d, 0x31, 0x20, 0x38, 0x78, 0x38, 0x64, 0x63, 0x74, 0x3d, 0x31, 0x20,
10918        0x63, 0x71, 0x6d, 0x3d, 0x30, 0x20, 0x64, 0x65, 0x61, 0x64, 0x7a, 0x6f, 0x6e, 0x65, 0x3d,
10919        0x32, 0x31, 0x2c, 0x31, 0x31, 0x20, 0x66, 0x61, 0x73, 0x74, 0x5f, 0x70, 0x73, 0x6b, 0x69,
10920        0x70, 0x3d, 0x31, 0x20, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x61, 0x5f, 0x71, 0x70, 0x5f, 0x6f,
10921        0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x2d, 0x32, 0x20, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64,
10922        0x73, 0x3d, 0x37, 0x20, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x5f, 0x74,
10923        0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x3d, 0x31, 0x20, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x64,
10924        0x5f, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x3d, 0x30, 0x20, 0x6e, 0x72, 0x3d, 0x30,
10925        0x20, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x3d, 0x31, 0x20, 0x69, 0x6e, 0x74,
10926        0x65, 0x72, 0x6c, 0x61, 0x63, 0x65, 0x64, 0x3d, 0x30, 0x20, 0x62, 0x6c, 0x75, 0x72, 0x61,
10927        0x79, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x3d, 0x30, 0x20, 0x63, 0x6f, 0x6e, 0x73,
10928        0x74, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x64, 0x5f, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x3d, 0x30,
10929        0x20, 0x62, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x3d, 0x32, 0x20, 0x62, 0x5f, 0x70, 0x79,
10930        0x72, 0x61, 0x6d, 0x69, 0x64, 0x3d, 0x32, 0x20, 0x62, 0x5f, 0x61, 0x64, 0x61, 0x70, 0x74,
10931        0x3d, 0x31, 0x20, 0x62, 0x5f, 0x62, 0x69, 0x61, 0x73, 0x3d, 0x30, 0x20, 0x64, 0x69, 0x72,
10932        0x65, 0x63, 0x74, 0x3d, 0x31, 0x20, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x62, 0x3d, 0x31,
10933        0x20, 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x67, 0x6f, 0x70, 0x3d, 0x31, 0x20, 0x77, 0x65, 0x69,
10934        0x67, 0x68, 0x74, 0x70, 0x3d, 0x32, 0x20, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, 0x3d, 0x31,
10935        0x32, 0x20, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, 0x5f, 0x6d, 0x69, 0x6e, 0x3d, 0x37, 0x20,
10936        0x73, 0x63, 0x65, 0x6e, 0x65, 0x63, 0x75, 0x74, 0x3d, 0x34, 0x30, 0x20, 0x69, 0x6e, 0x74,
10937        0x72, 0x61, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x3d, 0x30, 0x20, 0x72, 0x63,
10938        0x5f, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x3d, 0x31, 0x32, 0x20, 0x72,
10939        0x63, 0x3d, 0x63, 0x72, 0x66, 0x20, 0x6d, 0x62, 0x74, 0x72, 0x65, 0x65, 0x3d, 0x31, 0x20,
10940        0x63, 0x72, 0x66, 0x3d, 0x32, 0x33, 0x2e, 0x30, 0x20, 0x71, 0x63, 0x6f, 0x6d, 0x70, 0x3d,
10941        0x30, 0x2e, 0x36, 0x30, 0x20, 0x71, 0x70, 0x6d, 0x69, 0x6e, 0x3d, 0x30, 0x20, 0x71, 0x70,
10942        0x6d, 0x61, 0x78, 0x3d, 0x36, 0x39, 0x20, 0x71, 0x70, 0x73, 0x74, 0x65, 0x70, 0x3d, 0x34,
10943        0x20, 0x69, 0x70, 0x5f, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x3d, 0x31, 0x2e, 0x34, 0x30, 0x20,
10944        0x61, 0x71, 0x3d, 0x31, 0x3a, 0x31, 0x2e, 0x30, 0x30, 0x00, 0x80, 0x00, 0x00, 0x01, 0x65,
10945        0x88, 0x84, 0x00, 0x47, 0xda, 0xe3, 0x98, 0x5b, 0xd7, 0x57, 0xda, 0x42, 0x3e, 0x83, 0x89,
10946        0x96, 0xcb, 0xc5, 0x1b,
10947    ];
10948    const INTRA_AU: &[u8] = &[
10949        0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x0d, 0xac, 0xd9,
10950        0x41, 0x41, 0xfb, 0x01, 0x10, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x03, 0x03, 0xc0,
10951        0xf1, 0x42, 0x99, 0x60, 0x00, 0x00, 0x00, 0x01, 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, 0x00,
10952        0x00, 0x01, 0x06, 0x06, 0x01, 0xc4, 0x80, 0x00, 0x00, 0x01, 0x41, 0x88, 0xc3, 0x05, 0xff,
10953        0xd4, 0x57, 0x6d, 0x62, 0x78, 0xad, 0x3e, 0x89, 0xb4, 0xb5, 0x2a, 0xde, 0xcb, 0x0c, 0x64,
10954    ];
10955    const P_AU: &[u8] = &[
10956        0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x23, 0x6c, 0x45, 0x7f, 0xb7,
10957        0xe7, 0xf9, 0x23, 0x5b, 0xd9, 0xab, 0xc7, 0x7f, 0xcc, 0x03, 0xc4, 0xaa, 0x6f,
10958    ];
10959    const B_AU: &[u8] = &[
10960        0x00, 0x00, 0x01, 0x09, 0x50, 0x00, 0x00, 0x01, 0x41, 0x9e, 0x41, 0x78, 0x85, 0x3f, 0xfd,
10961        0xb1, 0x75, 0x99, 0x50, 0x2e, 0xe9, 0x5c, 0x1a, 0x0f, 0x32, 0x6a, 0xa7, 0x3d,
10962    ];
10963    const INTRA_VCL: &[u8] = &[
10964        0x00, 0x00, 0x01, 0x41, 0x88, 0xc3, 0x05, 0xff, 0xd4, 0x57, 0x6d, 0x62, 0x78, 0xad, 0x3e,
10965        0x89, 0xb4, 0xb5, 0x2a, 0xde, 0xcb, 0x0c, 0x64, 0x72, 0xe8, 0x3d, 0x54,
10966    ];
10967    const P_VCL: &[u8] = &[
10968        0x00, 0x00, 0x01, 0x41, 0x9a, 0x23, 0x6c, 0x45, 0x7f, 0xb7, 0xe7, 0xf9, 0x23, 0x5b, 0xd9,
10969        0xab, 0xc7, 0x7f, 0xcc, 0x03, 0xc4, 0xaa, 0x6f, 0x9a, 0xd8, 0xcf, 0x4a,
10970    ];
10971    /// An I slice whose huge `first_mb_in_slice` forces an emulation-prevention
10972    /// byte inside the header itself, so parsing only succeeds through the
10973    /// stripper.
10974    const EPB_INTRA_VCL: &[u8] = &[
10975        0x00, 0x00, 0x01, 0x41, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x08, 0x88,
10976    ];
10977
10978    #[test]
10979    fn idr_is_key() {
10980        assert_eq!(annexb_frame_type(IDR_AU), 0x01);
10981    }
10982
10983    #[test]
10984    fn open_gop_intra_is_intra_not_key() {
10985        assert_eq!(annexb_frame_type(INTRA_AU), 0x02);
10986    }
10987
10988    #[test]
10989    fn predicted_frames_are_delta() {
10990        assert_eq!(annexb_frame_type(P_AU), 0x00);
10991        assert_eq!(annexb_frame_type(B_AU), 0x00);
10992    }
10993
10994    #[test]
10995    fn one_predicted_slice_downgrades_a_mixed_unit() {
10996        let mixed = [INTRA_VCL, P_VCL].concat();
10997        assert_eq!(annexb_frame_type(&mixed), 0x00);
10998    }
10999
11000    #[test]
11001    fn unparsable_slice_header_is_delta() {
11002        assert_eq!(annexb_frame_type(&[0x00, 0x00, 0x01, 0x41]), 0x00);
11003        assert_eq!(annexb_frame_type(&[]), 0x00);
11004    }
11005
11006    #[test]
11007    fn emulation_prevention_is_stripped_before_parsing() {
11008        assert_eq!(annexb_frame_type(EPB_INTRA_VCL), 0x02);
11009    }
11010}
11011
11012#[cfg(test)]
11013mod encode_node_tests {
11014    //! How an unset encode node resolves: the index a render node path counts as, and no
11015    //! pick at all where auto-GPU selection is off.
11016    use super::{auto_render_node, render_node_index};
11017
11018    #[test]
11019    fn render_node_path_counts_from_128() {
11020        assert_eq!(render_node_index("/dev/dri/renderD128"), Some(0));
11021        assert_eq!(render_node_index("/dev/dri/renderD130"), Some(2));
11022        assert_eq!(render_node_index("/dev/dri/card0"), None);
11023    }
11024
11025    #[test]
11026    fn auto_gpu_off_picks_nothing() {
11027        assert_eq!(auto_render_node("false"), None);
11028        assert_eq!(auto_render_node(""), None);
11029    }
11030}
11031
11032#[cfg(test)]
11033mod shm_usage_tests {
11034    //! What the session reports as its /dev/shm usage: allocated blocks, so a sparse
11035    //! file is not counted as memory anyone holds.
11036    use super::shm_usage_in;
11037
11038    #[test]
11039    fn only_allocated_blocks_count() {
11040        let dir = std::env::temp_dir().join(format!("pixelflux-shm-{}", std::process::id()));
11041        std::fs::create_dir_all(&dir).unwrap();
11042        let path = dir.to_str().unwrap().to_string();
11043
11044        std::fs::File::create(dir.join("sparse"))
11045            .unwrap()
11046            .set_len(8 * 1024 * 1024 * 1024)
11047            .unwrap();
11048        let sparse = shm_usage_in(&path);
11049
11050        std::fs::write(dir.join("dense"), vec![0u8; 4 * 1024 * 1024]).unwrap();
11051        let dense = shm_usage_in(&path);
11052
11053        std::fs::remove_dir_all(&dir).ok();
11054        assert!(
11055            sparse < 1024 * 1024,
11056            "an 8 GiB sparse file counted {sparse} bytes"
11057        );
11058        assert!(
11059            dense - sparse >= 4 * 1024 * 1024,
11060            "a 4 MiB written file added only {} bytes",
11061            dense - sparse
11062        );
11063    }
11064}
11065
11066#[cfg(test)]
11067mod capture_state_tests {
11068    //! The `(state, last_error)` a Wayland display's capture reports: a live pipeline is
11069    //! "running" (with a caveat surfaced alongside), a recorded failure with no live
11070    //! pipeline is "failed", and a clean stop is "idle".
11071    use super::{set_wayland_capture_err, wayland_alive, wayland_capture_state};
11072
11073    #[test]
11074    fn state_tracks_liveness_and_recorded_error() {
11075        // A display id unlikely to collide with any concurrent test's use of the globals.
11076        let did = 987_654;
11077        set_wayland_capture_err(did, None);
11078        wayland_alive().lock().unwrap().remove(&did);
11079        assert_eq!(wayland_capture_state(did), ("idle".to_string(), None));
11080
11081        // Recorded failure, no live pipeline -> failed with the reason.
11082        set_wayland_capture_err(did, Some("no output".to_string()));
11083        assert_eq!(
11084            wayland_capture_state(did),
11085            ("failed".to_string(), Some("no output".to_string()))
11086        );
11087
11088        // A live pipeline reads running even with a caveat still recorded.
11089        wayland_alive().lock().unwrap().insert(did);
11090        set_wayland_capture_err(did, Some("using CPU encode".to_string()));
11091        assert_eq!(
11092            wayland_capture_state(did),
11093            ("running".to_string(), Some("using CPU encode".to_string()))
11094        );
11095
11096        // Clean stop clears both.
11097        wayland_alive().lock().unwrap().remove(&did);
11098        set_wayland_capture_err(did, None);
11099        assert_eq!(wayland_capture_state(did), ("idle".to_string(), None));
11100    }
11101}
11102
11103#[cfg(test)]
11104mod host_layout_tests {
11105    //! How the mode a host announced resolves for a capture configured at the requested
11106    //! size: that size, or no mode at all -> nothing to do; any other mode -> follow it,
11107    //! whatever the host answered about the request.
11108    use super::host_layout_resolution;
11109
11110    #[test]
11111    fn the_capture_follows_the_mode_the_host_runs() {
11112        let want = (1920, 1080);
11113        // Running the requested size: converged.
11114        assert_eq!(host_layout_resolution(want, Some(want)), None);
11115        // No mode announced: nothing to follow, the request stands.
11116        assert_eq!(host_layout_resolution(want, None), None);
11117        // Running another mode: the capture follows the host. This covers a host that
11118        // refused the request and one that acknowledged it without applying it, which
11119        // reads identically here and is what keeps a fixed-mode host from renegotiating
11120        // with a resizing client forever.
11121        assert_eq!(
11122            host_layout_resolution(want, Some((2560, 1440))),
11123            Some((2560, 1440))
11124        );
11125        assert_eq!(
11126            host_layout_resolution(want, Some((1280, 720))),
11127            Some((1280, 720))
11128        );
11129    }
11130}
11131
11132#[cfg(test)]
11133mod output_overlap_tests {
11134    //! Invariants of the output-placement intersection predicate: strict interior
11135    //! intersection (touching edges never overlap), containment and identity overlap,
11136    //! empty/negative rectangles never overlap, and extreme coordinates do not wrap.
11137    use super::rects_overlap;
11138
11139    #[test]
11140    fn disjoint_rects_do_not_overlap() {
11141        assert!(!rects_overlap((0, 0, 100, 100), (200, 0, 100, 100)));
11142        assert!(!rects_overlap((0, 0, 100, 100), (0, 200, 100, 100)));
11143    }
11144
11145    #[test]
11146    fn touching_edges_do_not_overlap() {
11147        // Right edge of a meets left edge of b, and bottom meets top.
11148        assert!(!rects_overlap((0, 0, 100, 100), (100, 0, 100, 100)));
11149        assert!(!rects_overlap((0, 0, 100, 100), (0, 100, 100, 100)));
11150        // Corner touch only.
11151        assert!(!rects_overlap((0, 0, 100, 100), (100, 100, 50, 50)));
11152    }
11153
11154    #[test]
11155    fn one_pixel_intrusion_overlaps() {
11156        assert!(rects_overlap((0, 0, 100, 100), (99, 0, 100, 100)));
11157        assert!(rects_overlap((0, 0, 100, 100), (0, 99, 100, 100)));
11158    }
11159
11160    #[test]
11161    fn containment_and_identity_overlap() {
11162        assert!(rects_overlap((0, 0, 100, 100), (25, 25, 10, 10)));
11163        assert!(rects_overlap((25, 25, 10, 10), (0, 0, 100, 100)));
11164        assert!(rects_overlap((5, 5, 50, 50), (5, 5, 50, 50)));
11165    }
11166
11167    #[test]
11168    fn empty_or_negative_rects_never_overlap() {
11169        assert!(!rects_overlap((10, 10, 0, 50), (0, 0, 100, 100)));
11170        assert!(!rects_overlap((10, 10, 50, 0), (0, 0, 100, 100)));
11171        assert!(!rects_overlap((10, 10, -5, 5), (0, 0, 100, 100)));
11172        assert!(!rects_overlap((0, 0, 100, 100), (10, 10, 0, 0)));
11173    }
11174
11175    #[test]
11176    fn negative_origins_overlap_correctly() {
11177        assert!(rects_overlap((-50, -50, 100, 100), (0, 0, 100, 100)));
11178        assert!(!rects_overlap((-100, -100, 100, 100), (0, 0, 100, 100)));
11179    }
11180
11181    #[test]
11182    fn extreme_coordinates_do_not_wrap() {
11183        assert!(!rects_overlap(
11184            (i32::MAX - 10, 0, 10, 10),
11185            (i32::MIN, 0, 10, 10)
11186        ));
11187        assert!(rects_overlap(
11188            (i32::MAX - 10, 0, 10, 10),
11189            (i32::MAX - 5, 0, 10, 10)
11190        ));
11191    }
11192}
11193
11194#[cfg(test)]
11195mod wl_frame_pool_tests {
11196    //! Invariants under test: try_begin is non-blocking and hands out a buffer ONLY while the
11197    //! publish slot is empty (so publish can never block the calloop); take blocks until a
11198    //! frame or shutdown; recycle/cancel return buffers for reuse; every published frame is
11199    //! observed exactly once and in order (the H.264 reference chain depends on it).
11200    use super::*;
11201
11202    fn frame(id: usize, buf: Vec<u8>, n: u16) -> WlFrame {
11203        WlFrame {
11204            id,
11205            buf,
11206            source: None,
11207            scanout: false,
11208            shm: None,
11209            frame_id: n,
11210            damage: Vec::new(),
11211            is_animated: false,
11212            captured_ns: 0,
11213        }
11214    }
11215
11216    #[test]
11217    fn begin_gated_on_slot_and_free_list() {
11218        let p = WlFramePool::new(2, 16);
11219        let (a, abuf) = p.try_begin().expect("first buffer");
11220        let (b, bbuf) = p.try_begin().expect("second buffer");
11221        assert_ne!(a, b);
11222        assert!(p.try_begin().is_none(), "free list exhausted");
11223        p.publish(frame(a, abuf, 0));
11224        p.cancel(b, bbuf);
11225        assert!(p.try_begin().is_none(), "slot occupied blocks begin");
11226        let f = p.take().expect("published frame");
11227        assert_eq!(f.frame_id, 0);
11228        p.recycle(f.id, f.buf);
11229        assert!(p.try_begin().is_some(), "drained slot re-enables begin");
11230    }
11231
11232    #[test]
11233    fn frames_flow_in_order_and_buffers_recycle() {
11234        let p = Arc::new(WlFramePool::new(2, 4));
11235        let p2 = p.clone();
11236        let consumer = thread::spawn(move || {
11237            let mut seen = Vec::new();
11238            while let Some(f) = p2.take() {
11239                seen.push(f.frame_id);
11240                p2.recycle(f.id, f.buf);
11241            }
11242            seen
11243        });
11244        let mut published = 0u16;
11245        while published < 50 {
11246            if let Some((id, buf)) = p.try_begin() {
11247                p.publish(frame(id, buf, published));
11248                published += 1;
11249            } else {
11250                thread::sleep(Duration::from_micros(50));
11251            }
11252        }
11253        thread::sleep(Duration::from_millis(50));
11254        p.shutdown();
11255        let seen = consumer.join().unwrap();
11256        assert_eq!(seen, (0..50).collect::<Vec<u16>>(), "every frame, in order");
11257    }
11258
11259    #[test]
11260    fn shutdown_unblocks_take() {
11261        let p = Arc::new(WlFramePool::new(1, 4));
11262        let p2 = p.clone();
11263        let t = thread::spawn(move || p2.take());
11264        thread::sleep(Duration::from_millis(30));
11265        p.shutdown();
11266        assert!(t.join().unwrap().is_none(), "take returns None on shutdown");
11267    }
11268
11269    #[test]
11270    fn cancel_returns_buffer_for_reuse() {
11271        let p = WlFramePool::new(1, 8);
11272        let (id, buf) = p.try_begin().expect("buffer");
11273        assert!(p.try_begin().is_none());
11274        p.cancel(id, buf);
11275        assert!(p.try_begin().is_some(), "canceled reservation reusable");
11276    }
11277}
11278
11279#[cfg(test)]
11280mod auto_gpu_token_tests {
11281    //! Invariant: AUTO_GPU tokens match a card's kernel-reported identity —
11282    //! driver name exactly (no table), raw PCI vendor ID, devicetree compatible
11283    //! prefix literally, or a human vendor name via the small embedded aliases —
11284    //! so users may pass either "amd" or "amdgpu" (etc.) interchangeably.
11285    use super::{CardIdentity, card_matches_token};
11286
11287    fn pci(driver: &str, vendor: u32) -> CardIdentity {
11288        CardIdentity {
11289            driver: driver.into(),
11290            pci_vendor: Some(vendor),
11291            compatibles: vec![],
11292        }
11293    }
11294
11295    fn dt(driver: &str, compatibles: &[&str]) -> CardIdentity {
11296        CardIdentity {
11297            driver: driver.into(),
11298            pci_vendor: None,
11299            compatibles: compatibles.iter().map(|c| c.to_string()).collect(),
11300        }
11301    }
11302
11303    #[test]
11304    fn driver_names_match_without_any_table() {
11305        assert!(card_matches_token("amdgpu", &pci("amdgpu", 0x1002)));
11306        assert!(card_matches_token(
11307            "panfrost",
11308            &dt("panfrost", &["rockchip,rk3399-mali"])
11309        ));
11310        assert!(card_matches_token("nouveau", &pci("nouveau", 0x10de)));
11311        assert!(!card_matches_token("i915", &pci("amdgpu", 0x1002)));
11312    }
11313
11314    #[test]
11315    fn vendor_names_and_raw_ids_match_pci_identity() {
11316        let nv = pci("nouveau", 0x10de);
11317        assert!(card_matches_token("nvidia", &nv));
11318        assert!(card_matches_token("0x10de", &nv));
11319        assert!(card_matches_token("10de", &nv));
11320        assert!(!card_matches_token("amd", &nv));
11321        assert!(card_matches_token("ati", &pci("radeon", 0x1002)));
11322    }
11323
11324    #[test]
11325    fn devicetree_prefixes_match_literally_and_via_aliases() {
11326        let mali = dt("panfrost", &["rockchip,rk3399-mali", "arm,mali-t860"]);
11327        assert!(card_matches_token("rockchip", &mali));
11328        assert!(card_matches_token("arm", &mali));
11329        assert!(card_matches_token("mali", &mali));
11330        let adreno = dt("msm", &["qcom,adreno-630", "qcom,adreno"]);
11331        assert!(card_matches_token("qcom", &adreno));
11332        assert!(card_matches_token("qualcomm", &adreno));
11333        assert!(card_matches_token("adreno", &adreno));
11334        assert!(!card_matches_token("brcm", &adreno));
11335        assert!(card_matches_token(
11336            "videocore",
11337            &dt("v3d", &["brcm,bcm2711-v3d"])
11338        ));
11339    }
11340
11341    #[test]
11342    fn missing_identity_fields_never_false_match() {
11343        let bare = CardIdentity {
11344            driver: String::new(),
11345            pci_vendor: None,
11346            compatibles: vec![],
11347        };
11348        for t in ["nvidia", "amdgpu", "0x10de", "qcom"] {
11349            assert!(!card_matches_token(t, &bare));
11350        }
11351    }
11352}