Skip to main content

wlr_capture/
wl.rs

1//! Native Wayland client: enumerate foreign toplevels and outputs, and capture
2//! them via `ext-image-copy-capture-v1`.
3//!
4//! The whole point of doing this natively (instead of shelling out to `grim -T`)
5//! is to create the shm buffer with the *correct* stride (`width * 4`), which is
6//! where grim 1.5 trips up ("Invalid stride") on some toplevels (Firefox, …).
7
8use anyhow::{Context, Result, bail};
9#[cfg(feature = "gpu")]
10use gbm::{BufferObject, BufferObjectFlags, Device as GbmDevice, Format as GbmFormat, Modifier};
11use rustix::event::{PollFd, PollFlags, Timespec};
12use std::collections::HashMap;
13use std::ffi::c_void;
14#[cfg(feature = "gpu")]
15use std::fs::File;
16use std::os::fd::{AsFd, OwnedFd};
17use std::time::{Duration, Instant};
18use wayland_client::{
19    Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum,
20    backend::ObjectId,
21    delegate_noop, event_created_child,
22    globals::{GlobalListContents, registry_queue_init},
23    protocol::{
24        wl_buffer::WlBuffer,
25        wl_output::{self, Transform, WlOutput},
26        wl_registry::WlRegistry,
27        wl_seat::WlSeat,
28        wl_shm::{self, WlShm},
29        wl_shm_pool::WlShmPool,
30    },
31};
32#[cfg(feature = "gpu")]
33use wayland_protocols::wp::linux_dmabuf::zv1::client::{
34    zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1},
35    zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1,
36};
37use wayland_protocols_wlr::foreign_toplevel::v1::client::{
38    zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1},
39    zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
40};
41
42/// DRM "invalid"/"let the driver choose" modifier sentinel — not a real layout.
43#[cfg(feature = "gpu")]
44const DRM_MOD_INVALID: u64 = 0x00ff_ffff_ffff_ffff;
45use wayland_protocols::ext::{
46    foreign_toplevel_list::v1::client::{
47        ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1},
48        ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1},
49    },
50    image_capture_source::v1::client::{
51        ext_foreign_toplevel_image_capture_source_manager_v1::ExtForeignToplevelImageCaptureSourceManagerV1,
52        ext_image_capture_source_v1::ExtImageCaptureSourceV1,
53        ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1,
54    },
55    image_copy_capture::v1::client::{
56        ext_image_copy_capture_frame_v1::{self, ExtImageCopyCaptureFrameV1, FailureReason},
57        ext_image_copy_capture_manager_v1::{ExtImageCopyCaptureManagerV1, Options},
58        ext_image_copy_capture_session_v1::{self, ExtImageCopyCaptureSessionV1},
59    },
60};
61use wayland_protocols::xdg::xdg_output::zv1::client::{
62    zxdg_output_manager_v1::ZxdgOutputManagerV1,
63    zxdg_output_v1::{self, ZxdgOutputV1},
64};
65
66/// A capturable window.
67#[derive(Clone)]
68pub struct Toplevel {
69    /// The protocol handle used to capture or act on this window.
70    pub handle: ExtForeignToplevelHandleV1,
71    /// Compositor-assigned stable identifier for the toplevel.
72    pub identifier: String,
73    /// The window title.
74    pub title: String,
75    /// The application id (used to match an icon / desktop entry).
76    pub app_id: String,
77}
78
79/// A capturable output, with its placement in the global logical space.
80///
81/// Logical position/size come from `xdg-output` (`zxdg_output_manager_v1`) when the
82/// compositor exposes it — the only reliable source for multi-monitor positions and
83/// fractional-scale logical sizes. Physical pixel size, integer scale and transform
84/// come from `wl_output`; if `xdg-output` is absent we fall back to computing the
85/// logical size from those.
86#[derive(Clone)]
87pub struct Output {
88    /// The underlying `wl_output` protocol object.
89    pub wl_output: WlOutput,
90    /// The output's connector name (e.g. `DP-1`).
91    pub name: String,
92    /// Left edge in the global logical coordinate space.
93    pub logical_x: i32,
94    /// Top edge in the global logical coordinate space.
95    pub logical_y: i32,
96    /// Logical width from xdg-output (0 until received; see [`Output::logical_size`]).
97    pub logical_w: i32,
98    /// Logical height from xdg-output (0 until received; see [`Output::logical_size`]).
99    pub logical_h: i32,
100    /// Width of the current mode, in physical pixels (pre-transform).
101    pub phys_width: i32,
102    /// Height of the current mode, in physical pixels (pre-transform).
103    pub phys_height: i32,
104    /// Integer buffer scale (wl_output; may be coarser than the real scale).
105    pub scale: i32,
106    /// Output transform (rotation/flip); swaps logical width/height for 90/270.
107    pub transform: Transform,
108    /// Whether xdg-output supplied authoritative logical geometry.
109    pub have_xdg: bool,
110}
111
112/// Logical dimensions from physical pixels: divide by `scale`, swapping
113/// width/height for 90°/270° transforms. Free function so it's unit-testable
114/// without a live `WlOutput`.
115fn logical_dims(phys_w: i32, phys_h: i32, scale: i32, transform: Transform) -> (i32, i32) {
116    let s = scale.max(1);
117    let (w, h) = (phys_w / s, phys_h / s);
118    if matches!(
119        transform,
120        Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270
121    ) {
122        (h, w)
123    } else {
124        (w, h)
125    }
126}
127
128impl Output {
129    /// Logical size (points). Prefers xdg-output's authoritative size (handles
130    /// fractional scale); otherwise physical pixels divided by the integer scale,
131    /// with width/height swapped for 90°/270° transforms.
132    pub fn logical_size(&self) -> (i32, i32) {
133        if self.have_xdg && self.logical_w > 0 && self.logical_h > 0 {
134            (self.logical_w, self.logical_h)
135        } else {
136            logical_dims(
137                self.phys_width,
138                self.phys_height,
139                self.scale,
140                self.transform,
141            )
142        }
143    }
144}
145
146/// An axis-aligned rectangle. Used both for capture cropping (in an image's pixel
147/// space) and for selection geometry (in the global logical space), so `x`/`y` may
148/// be negative; `w`/`h` are unsigned.
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub struct Region {
151    /// Left edge (may be negative in logical space).
152    pub x: i32,
153    /// Top edge (may be negative in logical space).
154    pub y: i32,
155    /// Width.
156    pub w: u32,
157    /// Height.
158    pub h: u32,
159}
160
161impl Region {
162    /// Whether the region has zero area (`w` or `h` is 0).
163    pub fn is_empty(&self) -> bool {
164        self.w == 0 || self.h == 0
165    }
166
167    /// The overlapping rectangle of two regions, or `None` if they don't overlap.
168    pub fn intersect(&self, o: &Region) -> Option<Region> {
169        let x0 = self.x.max(o.x);
170        let y0 = self.y.max(o.y);
171        let x1 = (self.x + self.w as i32).min(o.x + o.w as i32);
172        let y1 = (self.y + self.h as i32).min(o.y + o.h as i32);
173        (x1 > x0 && y1 > y0).then_some(Region {
174            x: x0,
175            y: y0,
176            w: (x1 - x0) as u32,
177            h: (y1 - y0) as u32,
178        })
179    }
180}
181
182impl Output {
183    /// The output's placement in the global logical space, as a [`Region`].
184    pub fn logical_rect(&self) -> Region {
185        let (w, h) = self.logical_size();
186        Region {
187            x: self.logical_x,
188            y: self.logical_y,
189            w: w.max(0) as u32,
190            h: h.max(0) as u32,
191        }
192    }
193}
194
195/// Decoded RGBA8 image.
196pub struct CapturedImage {
197    /// Width in pixels.
198    pub width: u32,
199    /// Height in pixels.
200    pub height: u32,
201    /// Tightly-packed RGBA8 pixels, row-major (`width * height * 4` bytes).
202    pub rgba: Vec<u8>,
203}
204
205impl CapturedImage {
206    /// The RGBA bytes of the pixel at `(x, y)`, or `None` if out of bounds.
207    pub fn pixel(&self, x: u32, y: u32) -> Option<[u8; 4]> {
208        if x >= self.width || y >= self.height {
209            return None;
210        }
211        let i = ((y * self.width + x) * 4) as usize;
212        self.rgba.get(i..i + 4).map(|s| [s[0], s[1], s[2], s[3]])
213    }
214
215    /// Crop to `rect` (in this image's pixel space), clamped to the bounds. Returns
216    /// the overlapping sub-image; empty (0×0) if there is no overlap.
217    pub fn crop(&self, rect: Region) -> CapturedImage {
218        let bounds = Region {
219            x: 0,
220            y: 0,
221            w: self.width,
222            h: self.height,
223        };
224        let Some(r) = rect.intersect(&bounds) else {
225            return CapturedImage {
226                width: 0,
227                height: 0,
228                rgba: Vec::new(),
229            };
230        };
231        let row_bytes = (r.w * 4) as usize;
232        let mut out = vec![0u8; row_bytes * r.h as usize];
233        for row in 0..r.h {
234            let sy = r.y as u32 + row;
235            let src = ((sy * self.width + r.x as u32) * 4) as usize;
236            let dst = row as usize * row_bytes;
237            out[dst..dst + row_bytes].copy_from_slice(&self.rgba[src..src + row_bytes]);
238        }
239        CapturedImage {
240            width: r.w,
241            height: r.h,
242            rgba: out,
243        }
244    }
245
246    /// Composite this image into a `dst_w × dst_h` RGBA8 buffer at `(at_x, at_y)`,
247    /// clipping to the destination. Used to stitch per-output captures into one
248    /// multi-output region.
249    pub fn blit_into(&self, dst: &mut [u8], dst_w: u32, dst_h: u32, at_x: i32, at_y: i32) {
250        let dst_rect = Region {
251            x: 0,
252            y: 0,
253            w: dst_w,
254            h: dst_h,
255        };
256        let src_rect = Region {
257            x: at_x,
258            y: at_y,
259            w: self.width,
260            h: self.height,
261        };
262        let Some(r) = src_rect.intersect(&dst_rect) else {
263            return;
264        };
265        let row_bytes = (r.w * 4) as usize;
266        for row in 0..r.h {
267            let dy = r.y as u32 + row;
268            let sy = (r.y - at_y) as u32 + row;
269            let sx = (r.x - at_x) as u32;
270            let src = ((sy * self.width + sx) * 4) as usize;
271            let dpos = ((dy * dst_w + r.x as u32) * 4) as usize;
272            dst[dpos..dpos + row_bytes].copy_from_slice(&self.rgba[src..src + row_bytes]);
273        }
274    }
275}
276
277/// Byte layout of a wl_shm pixel format (memory order, little-endian), so we can
278/// convert to RGBA8 and — crucially — compute the correct stride (`width * bpp`).
279struct PixelLayout {
280    bpp: usize,
281    r: usize,
282    g: usize,
283    b: usize,
284    a: Option<usize>,
285}
286
287impl PixelLayout {
288    fn of(f: wl_shm::Format) -> Option<Self> {
289        use wl_shm::Format::*;
290        Some(match f {
291            Argb8888 => Self {
292                bpp: 4,
293                r: 2,
294                g: 1,
295                b: 0,
296                a: Some(3),
297            },
298            Xrgb8888 => Self {
299                bpp: 4,
300                r: 2,
301                g: 1,
302                b: 0,
303                a: None,
304            },
305            Abgr8888 => Self {
306                bpp: 4,
307                r: 0,
308                g: 1,
309                b: 2,
310                a: Some(3),
311            },
312            Xbgr8888 => Self {
313                bpp: 4,
314                r: 0,
315                g: 1,
316                b: 2,
317                a: None,
318            },
319            Bgr888 => Self {
320                bpp: 3,
321                r: 0,
322                g: 1,
323                b: 2,
324                a: None,
325            },
326            Rgb888 => Self {
327                bpp: 3,
328                r: 2,
329                g: 1,
330                b: 0,
331                a: None,
332            },
333            _ => return None,
334        })
335    }
336
337    /// Same, keyed by DRM fourcc (for dma-buf). DRM 32-bit codes use the same
338    /// little-endian memory order as their wl_shm counterparts.
339    #[cfg(feature = "gpu")]
340    fn of_fourcc(f: u32) -> Option<Self> {
341        Some(match f {
342            // XR24 / AR24: little-endian B,G,R,(X|A)
343            f if f == fourcc(b'X', b'R', b'2', b'4') => Self::of(wl_shm::Format::Xrgb8888)?,
344            f if f == fourcc(b'A', b'R', b'2', b'4') => Self::of(wl_shm::Format::Argb8888)?,
345            // XB24 / AB24: little-endian R,G,B,(X|A)
346            f if f == fourcc(b'X', b'B', b'2', b'4') => Self::of(wl_shm::Format::Xbgr8888)?,
347            f if f == fourcc(b'A', b'B', b'2', b'4') => Self::of(wl_shm::Format::Abgr8888)?,
348            _ => return None,
349        })
350    }
351}
352
353#[derive(Default)]
354struct PendingToplevel {
355    identifier: String,
356    title: String,
357    app_id: String,
358}
359
360/// Opaque handle to a persistent capture session (the session object's id).
361pub type SessionId = ObjectId;
362
363/// Per-session bookkeeping, updated by the session/frame Dispatch impls and keyed
364/// by the session object id so multiple live sessions never clobber each other.
365#[derive(Default)]
366struct SessionData {
367    /// Latest buffer constraints advertised by the compositor.
368    width: u32,
369    height: u32,
370    format: Option<wl_shm::Format>,
371    /// dma-buf device the compositor wants buffers allocated on (raw dev_t).
372    #[cfg(feature = "gpu")]
373    dmabuf_dev: Option<u64>,
374    /// dma-buf formats advertised: (drm fourcc, supported modifiers).
375    #[cfg(feature = "gpu")]
376    dmabuf_formats: Vec<(u32, Vec<u64>)>,
377    /// Set once a constraints group (`done`) has been received.
378    constraints_done: bool,
379    /// Constraints changed since the buffer was last (re)allocated (e.g. resize).
380    dirty: bool,
381    /// Set when the current in-flight frame is ready to read.
382    ready: bool,
383    /// A transient per-frame failure (retry next round); `buffer_constraints`
384    /// additionally triggers a reallocation. Not terminal.
385    frame_failed: Option<FailureReason>,
386    /// Terminal: the session/source stopped and won't produce more frames.
387    stopped: bool,
388}
389
390/// A reusable buffer backing one session, kept alive between frames. Either a
391/// CPU shm buffer (fallback) or a GPU dma-buf swapchain allocated through gbm.
392enum Buf {
393    Shm(ShmBuf),
394    #[cfg(feature = "gpu")]
395    Dmabuf(DmaBuf),
396}
397
398impl Buf {
399    fn wl_buffer(&self) -> &WlBuffer {
400        match self {
401            Buf::Shm(b) => &b.buffer,
402            #[cfg(feature = "gpu")]
403            Buf::Dmabuf(b) => &b.buffer,
404        }
405    }
406    /// Did the advertised constraints (size) change vs this buffer?
407    fn matches(&self, w: u32, h: u32) -> bool {
408        match self {
409            Buf::Shm(b) => b.width == w && b.height == h,
410            #[cfg(feature = "gpu")]
411            Buf::Dmabuf(b) => b.width == w && b.height == h,
412        }
413    }
414}
415
416/// CPU shm buffer with a correct, format-specific stride.
417struct ShmBuf {
418    pool: WlShmPool,
419    buffer: WlBuffer,
420    _fd: OwnedFd,
421    map: *mut c_void,
422    size: usize,
423    width: u32,
424    height: u32,
425    stride: usize,
426    format: wl_shm::Format,
427}
428
429impl Drop for ShmBuf {
430    fn drop(&mut self) {
431        self.buffer.destroy();
432        self.pool.destroy();
433        unsafe {
434            let _ = rustix::mm::munmap(self.map, self.size);
435        }
436    }
437}
438
439/// One dma-buf allocated via gbm: the compositor captures into it, and it is
440/// imported zero-copy as a GL texture for display.
441///
442/// Single-buffered on purpose: `ext-image-copy-capture` captures *incrementally*
443/// by damage, assuming the buffer it's given already holds the previous frame.
444/// Reusing one buffer lets it accumulate the full image; alternating buffers
445/// would leave undamaged regions of the "other" buffer empty (black) for static
446/// windows. Sampling the buffer while the compositor updates a small damage
447/// region is imperceptible at thumbnail scale.
448#[cfg(feature = "gpu")]
449struct DmaBuf {
450    buffer: WlBuffer,
451    bo: BufferObject<()>,
452    width: u32,
453    height: u32,
454    fourcc: u32,
455    modifier: u64,
456    stride: u32,
457    offset: u32,
458}
459
460#[cfg(feature = "gpu")]
461impl Drop for DmaBuf {
462    fn drop(&mut self) {
463        self.buffer.destroy();
464        // `bo` drops here, releasing the underlying dma-buf.
465    }
466}
467
468/// A captured frame handed to the UI: either CPU pixels (shm) or a dma-buf
469/// descriptor to import as a GL texture (GPU, zero-copy).
470pub enum Frame {
471    /// CPU pixels read back into shared memory (the shm fallback path).
472    Shm(CapturedImage),
473    /// A dma-buf descriptor to import as a GL texture (zero-copy GPU path).
474    // Constructed only with the `gpu` feature; the display side (EGL import) is
475    // always built since it needs no gbm.
476    #[cfg_attr(not(feature = "gpu"), allow(dead_code))]
477    Dmabuf(DmabufFrame),
478}
479
480/// dma-buf descriptor for zero-copy GL import on the UI thread. `fd` is owned by
481/// the receiver; `buf_id` identifies the swapchain slot so the importer can cache
482/// one GL texture per slot (their backing memory is stable).
483pub struct DmabufFrame {
484    /// Owned file descriptor backing the buffer (closed when this is dropped).
485    pub fd: OwnedFd,
486    /// Width in pixels.
487    pub width: u32,
488    /// Height in pixels.
489    pub height: u32,
490    /// DRM `FourCC` pixel format code.
491    pub fourcc: u32,
492    /// DRM format modifier (tiling/compression layout).
493    pub modifier: u64,
494    /// Row stride in bytes.
495    pub stride: u32,
496    /// Byte offset of the plane within the buffer.
497    pub offset: u32,
498}
499
500/// A persistent capture session: source + session objects plus the reusable
501/// buffer. Re-armed each frame instead of being torn down (the one-shot model).
502/// `frame` holds the in-flight capture (a frame object captures exactly one
503/// frame), pending until the source produces new content (damage).
504struct OpenSession {
505    frame: Option<ExtImageCopyCaptureFrameV1>, // in-flight capture, if armed
506    buf: Option<Buf>,                          // dropped after the frame, before the session
507    session: ExtImageCopyCaptureSessionV1,
508    src: ExtImageCaptureSourceV1,
509}
510
511impl Drop for OpenSession {
512    fn drop(&mut self) {
513        if let Some(frame) = &self.frame {
514            frame.destroy();
515        }
516        self.session.destroy();
517        self.src.destroy();
518    }
519}
520
521#[derive(Default)]
522struct State {
523    toplevels: Vec<Toplevel>,
524    pending: Vec<(ExtForeignToplevelHandleV1, PendingToplevel)>,
525    outputs: Vec<Output>,
526    shm: Option<WlShm>,
527    tl_src: Option<ExtForeignToplevelImageCaptureSourceManagerV1>,
528    out_src: Option<ExtOutputImageCaptureSourceManagerV1>,
529    copy: Option<ExtImageCopyCaptureManagerV1>,
530    /// linux-dmabuf manager, if the compositor exposes it (enables the GPU path).
531    #[cfg(feature = "gpu")]
532    dmabuf: Option<ZwpLinuxDmabufV1>,
533    /// Event bookkeeping for every live session, keyed by session object id.
534    sessions: HashMap<ObjectId, SessionData>,
535}
536
537/// A Wayland client that enumerates capturable toplevels and outputs and drives
538/// their capture sessions over `ext-image-copy-capture`.
539pub struct Client {
540    queue: EventQueue<State>,
541    qh: QueueHandle<State>,
542    state: State,
543    /// Session-owned Wayland objects + buffers, keyed by session object id.
544    open: HashMap<ObjectId, OpenSession>,
545    /// gbm device for dma-buf allocation, opened lazily on the first dma-buf
546    /// session (matching the compositor's advertised device). `None` until then,
547    /// or if the GPU path is unavailable (we then fall back to shm).
548    #[cfg(feature = "gpu")]
549    gbm: Option<GbmDevice<File>>,
550}
551
552impl Client {
553    /// Connect, bind the capture managers, and enumerate windows + outputs.
554    pub fn connect() -> Result<Self> {
555        let conn = Connection::connect_to_env().context("Wayland connection")?;
556        let (globals, mut queue) =
557            registry_queue_init::<State>(&conn).context("registre Wayland")?;
558        let qh = queue.handle();
559
560        let shm = globals.bind(&qh, 1..=1, ()).context("wl_shm")?;
561        let copy = globals
562            .bind(&qh, 1..=1, ())
563            .context("ext_image_copy_capture_manager_v1 missing")?;
564        let tl_src = globals.bind(&qh, 1..=1, ()).context(
565            "ext_foreign_toplevel_image_capture_source_manager_v1 missing: \
566             this compositor cannot capture individual windows. The foreign-toplevel \
567             capture source requires wlroots >= 0.20 (Sway >= 1.12); wlroots 0.19 / \
568             Sway 1.11 only expose output capture. Run `wlr-peek doctor` to see what \
569             your compositor supports.",
570        )?;
571        let out_src = globals
572            .bind(&qh, 1..=1, ())
573            .context("ext_output_image_capture_source_manager_v1 missing")?;
574        let _list: ExtForeignToplevelListV1 = globals
575            .bind(&qh, 1..=1, ())
576            .context("ext_foreign_toplevel_list_v1 missing")?;
577
578        // Optional: authoritative logical geometry (multi-monitor positions,
579        // fractional scale). Absent on a few compositors — we then fall back to
580        // wl_output-derived sizes.
581        let xdg_mgr: Option<ZxdgOutputManagerV1> = globals.bind(&qh, 1..=3, ()).ok();
582
583        globals.contents().with_list(|list| {
584            for g in list {
585                if g.interface == WlOutput::interface().name {
586                    let out: WlOutput = globals.registry().bind(g.name, g.version.min(4), &qh, ());
587                    if let Some(mgr) = &xdg_mgr {
588                        // udata = the wl_output, so the xdg_output's logical-geometry
589                        // events update the matching Output.
590                        mgr.get_xdg_output(&out, &qh, out.clone());
591                    }
592                }
593            }
594        });
595
596        let mut state = State {
597            shm: Some(shm),
598            copy: Some(copy),
599            tl_src: Some(tl_src),
600            out_src: Some(out_src),
601            ..Default::default()
602        };
603        // Optional: enables the GPU dma-buf path. Absence just means shm-only.
604        #[cfg(feature = "gpu")]
605        {
606            state.dmabuf = globals.bind(&qh, 3..=4, ()).ok();
607        }
608        queue.roundtrip(&mut state)?;
609        queue.roundtrip(&mut state)?;
610
611        Ok(Self {
612            queue,
613            qh,
614            state,
615            open: HashMap::new(),
616            #[cfg(feature = "gpu")]
617            gbm: None,
618        })
619    }
620
621    /// The currently known capturable windows.
622    pub fn toplevels(&self) -> &[Toplevel] {
623        &self.state.toplevels
624    }
625    /// The currently known capturable outputs.
626    pub fn outputs(&self) -> &[Output] {
627        &self.state.outputs
628    }
629
630    /// Drain pending Wayland events (new/closed toplevels, etc.) without blocking
631    /// on a capture, so the source list stays current between capture rounds.
632    pub fn refresh(&mut self) -> Result<()> {
633        self.queue.roundtrip(&mut self.state)?;
634        Ok(())
635    }
636
637    /// Open a persistent capture session for a window. The session and its buffer
638    /// live until [`Client::close_session`] (or the source disappears); re-arm a
639    /// frame each cycle with [`Client::capture`].
640    pub fn open_toplevel_session(&mut self, t: &Toplevel) -> Result<SessionId> {
641        let src = self
642            .state
643            .tl_src
644            .as_ref()
645            .unwrap()
646            .create_source(&t.handle, &self.qh, ());
647        self.open_session(src)
648    }
649
650    /// Open a persistent capture session for an output. See [`Client::open_toplevel_session`].
651    pub fn open_output_session(&mut self, o: &Output) -> Result<SessionId> {
652        let src = self
653            .state
654            .out_src
655            .as_ref()
656            .unwrap()
657            .create_source(&o.wl_output, &self.qh, ());
658        self.open_session(src)
659    }
660
661    fn open_session(&mut self, src: ExtImageCaptureSourceV1) -> Result<SessionId> {
662        let session =
663            self.state
664                .copy
665                .as_ref()
666                .unwrap()
667                .create_session(&src, Options::empty(), &self.qh, ());
668        let id = session.id();
669        self.state
670            .sessions
671            .insert(id.clone(), SessionData::default());
672
673        // Wait for the first buffer-constraints group (buffer_size + shm_format + done).
674        loop {
675            self.queue.blocking_dispatch(&mut self.state)?;
676            let d = self.state.sessions.get(&id).unwrap();
677            if d.constraints_done || d.stopped {
678                break;
679            }
680        }
681        if self.state.sessions.get(&id).unwrap().stopped {
682            self.state.sessions.remove(&id);
683            session.destroy();
684            src.destroy();
685            bail!("capture session stopped before first frame");
686        }
687
688        self.open.insert(
689            id.clone(),
690            OpenSession {
691                frame: None,
692                buf: None,
693                session,
694                src,
695            },
696        );
697        Ok(id)
698    }
699
700    /// Tear down a session (e.g. its window closed).
701    pub fn close_session(&mut self, id: &SessionId) {
702        self.open.remove(id); // Drop releases frame + buffer + session + source
703        self.state.sessions.remove(id);
704    }
705
706    /// One-shot: capture a single frame of `output`, then tear the session down.
707    /// Blocks up to `budget`. For screenshots / timelapse ticks.
708    pub fn capture_output_once(&mut self, output: &Output, budget: Duration) -> Result<Frame> {
709        let id = self.open_output_session(output)?;
710        let r = self.poll_one(&id, budget);
711        self.close_session(&id);
712        r
713    }
714
715    /// One-shot: capture a single frame of `toplevel`, then tear the session down.
716    pub fn capture_toplevel_once(
717        &mut self,
718        toplevel: &Toplevel,
719        budget: Duration,
720    ) -> Result<Frame> {
721        let id = self.open_toplevel_session(toplevel)?;
722        let r = self.poll_one(&id, budget);
723        self.close_session(&id);
724        r
725    }
726
727    /// Poll until session `id` yields a frame, it stops, or `budget` elapses.
728    /// Frames from other open sessions in this round are discarded.
729    fn poll_one(&mut self, id: &SessionId, budget: Duration) -> Result<Frame> {
730        let deadline = Instant::now() + budget;
731        loop {
732            let now = Instant::now();
733            if now >= deadline {
734                bail!("capture: timed out");
735            }
736            let step = Duration::from_millis(50).min(deadline - now);
737            let (frames, stopped) = self.poll(step);
738            for (sid, frame) in frames {
739                if &sid == id {
740                    return Ok(frame);
741                }
742            }
743            if stopped.iter().any(|s| s == id) {
744                bail!("capture: session stopped before first frame");
745            }
746        }
747    }
748
749    /// Drive all open sessions for up to `budget`: arm a frame on every idle
750    /// session, wait for events, and return the frames that became ready (the
751    /// sources that produced new content). Sessions whose source is static simply
752    /// keep their frame armed and deliver nothing — which is exactly right, there
753    /// is nothing new to show.
754    ///
755    /// Also returns the ids of sessions the compositor stopped (e.g. their window
756    /// closed), so the caller can drop and (if still listed) reopen them.
757    pub fn poll(&mut self, budget: Duration) -> (Vec<(SessionId, Frame)>, Vec<SessionId>) {
758        // 1. Arm every session that has no frame in flight.
759        let ids: Vec<ObjectId> = self.open.keys().cloned().collect();
760        for id in &ids {
761            let armed = self.open.get(id).is_some_and(|o| o.frame.is_some());
762            let dead = self.state.sessions.get(id).is_some_and(|d| d.stopped);
763            if armed || dead {
764                continue;
765            }
766            if self.ensure_buffer(id).is_err() {
767                continue;
768            }
769            if let Some(d) = self.state.sessions.get_mut(id) {
770                d.ready = false;
771            }
772            let os = self.open.get_mut(id).unwrap();
773            let wl_buffer = os.buf.as_ref().unwrap().wl_buffer().clone();
774            let frame = os.session.create_frame(&self.qh, id.clone());
775            frame.attach_buffer(&wl_buffer);
776            frame.capture();
777            os.frame = Some(frame);
778        }
779
780        // 2. Wait for frame events, but never longer than the budget.
781        let _ = self.dispatch_timeout(budget);
782
783        // 3. Harvest ready frames; retry transient frame failures; surface stops.
784        let mut frames = Vec::new();
785        let mut stopped = Vec::new();
786        for id in self.open.keys().cloned().collect::<Vec<_>>() {
787            let (ready, is_stopped, frame_failed) = self
788                .state
789                .sessions
790                .get(&id)
791                .map(|d| (d.ready, d.stopped, d.frame_failed))
792                .unwrap_or((false, false, None));
793
794            // Terminal: source gone. Drop the in-flight frame and report it.
795            if is_stopped {
796                if let Some(os) = self.open.get_mut(&id)
797                    && let Some(frame) = os.frame.take()
798                {
799                    frame.destroy();
800                }
801                stopped.push(id);
802                continue;
803            }
804
805            if ready {
806                let frame = harvest(self.open[&id].buf.as_ref().unwrap());
807                if let Some(os) = self.open.get_mut(&id)
808                    && let Some(f) = os.frame.take()
809                {
810                    f.destroy();
811                }
812                if let Some(d) = self.state.sessions.get_mut(&id) {
813                    d.ready = false;
814                    d.frame_failed = None;
815                }
816                if let Some(frame) = frame {
817                    frames.push((id, frame));
818                }
819            } else if let Some(reason) = frame_failed {
820                // Transient: drop the failed frame and re-arm next round. A
821                // buffer_constraints failure also means our buffer is stale.
822                if let Some(os) = self.open.get_mut(&id)
823                    && let Some(f) = os.frame.take()
824                {
825                    f.destroy();
826                }
827                if let Some(d) = self.state.sessions.get_mut(&id) {
828                    d.frame_failed = None;
829                    if matches!(reason, FailureReason::BufferConstraints) {
830                        d.dirty = true; // size/format changed → reallocate
831                    }
832                }
833            }
834        }
835        (frames, stopped)
836    }
837
838    /// Dispatch Wayland events for at most `budget`, returning early once the fd
839    /// goes quiet. Mirrors the crate's `blocking_read` but with a `poll` timeout
840    /// so a desktop with no damage doesn't block us forever.
841    fn dispatch_timeout(&mut self, budget: Duration) -> Result<()> {
842        self.queue.dispatch_pending(&mut self.state)?;
843        self.queue.flush()?;
844        let deadline = Instant::now() + budget;
845        loop {
846            let remaining = deadline.saturating_duration_since(Instant::now());
847            if remaining.is_zero() {
848                break;
849            }
850            let Some(guard) = self.queue.prepare_read() else {
851                // Events already queued: dispatch them and re-check.
852                self.queue.dispatch_pending(&mut self.state)?;
853                continue;
854            };
855            let ts = Timespec {
856                tv_sec: remaining.as_secs() as _,
857                tv_nsec: remaining.subsec_nanos() as _,
858            };
859            // Scope the borrowed fd so it is released before `guard.read()` (which
860            // consumes the guard).
861            let poll_res = {
862                let fd = guard.connection_fd();
863                let mut fds = [PollFd::new(&fd, PollFlags::IN | PollFlags::ERR)];
864                rustix::event::poll(&mut fds, Some(&ts))
865            };
866            match poll_res {
867                Ok(0) => break, // timeout: no events within the budget
868                Ok(_) => {
869                    guard.read().context("reading Wayland events")?;
870                    self.queue.dispatch_pending(&mut self.state)?;
871                }
872                Err(rustix::io::Errno::INTR) => continue,
873                Err(e) => return Err(anyhow::anyhow!("poll: {e}")),
874            }
875        }
876        Ok(())
877    }
878
879    /// Ensure the session has a usable buffer, (re)allocating only when it is
880    /// missing or the size changed (window resized). Prefers the GPU dma-buf path
881    /// and falls back to shm. The buffer is reused across frames otherwise.
882    fn ensure_buffer(&mut self, id: &SessionId) -> Result<()> {
883        let (w, h, dirty) = {
884            let d = self.state.sessions.get(id).context("session inconnue")?;
885            (d.width, d.height, d.dirty)
886        };
887        let fits = self
888            .open
889            .get(id)
890            .and_then(|o| o.buf.as_ref())
891            .is_some_and(|b| b.matches(w, h));
892        // Reuse unless the size changed or a buffer_constraints failure marked it
893        // dirty (then reallocate even at the same size).
894        if fits && !dirty {
895            return Ok(());
896        }
897        if w == 0 || h == 0 {
898            bail!("dimensions de capture nulles");
899        }
900
901        // Prefer dma-buf (GPU); fall back to shm if it isn't available/usable.
902        #[cfg(feature = "gpu")]
903        let buf = match self.alloc_dmabuf(id, w, h) {
904            Some(b) => b,
905            None => self.alloc_shm(id, w, h)?,
906        };
907        #[cfg(not(feature = "gpu"))]
908        let buf = self.alloc_shm(id, w, h)?;
909        // Install the new buffer; the old one (if any) drops here, releasing it.
910        self.open.get_mut(id).context("session non ouverte")?.buf = Some(buf);
911        self.state.sessions.get_mut(id).unwrap().dirty = false;
912        Ok(())
913    }
914
915    /// Allocate a CPU shm buffer with the format-correct stride.
916    fn alloc_shm(&mut self, id: &SessionId, w: u32, h: u32) -> Result<Buf> {
917        let format = self
918            .state
919            .sessions
920            .get(id)
921            .and_then(|d| d.format)
922            .context("compositor offered no shm format")?;
923        let layout = PixelLayout::of(format)
924            .with_context(|| format!("unsupported shm format: {format:?}"))?;
925        let stride = w as usize * layout.bpp; // stride from the format's actual bpp
926        let size = stride * h as usize;
927
928        let fd = rustix::fs::memfd_create("wlr-chooser-shm", rustix::fs::MemfdFlags::CLOEXEC)
929            .context("memfd_create")?;
930        rustix::fs::ftruncate(&fd, size as u64).context("ftruncate")?;
931        let map = unsafe {
932            rustix::mm::mmap(
933                std::ptr::null_mut(),
934                size,
935                rustix::mm::ProtFlags::READ | rustix::mm::ProtFlags::WRITE,
936                rustix::mm::MapFlags::SHARED,
937                &fd,
938                0,
939            )
940            .context("mmap")?
941        };
942        let pool =
943            self.state
944                .shm
945                .as_ref()
946                .unwrap()
947                .create_pool(fd.as_fd(), size as i32, &self.qh, ());
948        let buffer = pool.create_buffer(0, w as i32, h as i32, stride as i32, format, &self.qh, ());
949        Ok(Buf::Shm(ShmBuf {
950            pool,
951            buffer,
952            _fd: fd,
953            map,
954            size,
955            width: w,
956            height: h,
957            stride,
958            format,
959        }))
960    }
961
962    /// Try to allocate a dma-buf (via gbm) and wrap it as a wl_buffer. Returns
963    /// `None` whenever the GPU path isn't usable (no manager, no suitable format,
964    /// gbm/allocation failure) so the caller falls back to shm.
965    #[cfg(feature = "gpu")]
966    fn alloc_dmabuf(&mut self, id: &SessionId, w: u32, h: u32) -> Option<Buf> {
967        let dmabuf_mgr = self.state.dmabuf.as_ref().cloned()?;
968        let (formats, dev) = {
969            let d = self.state.sessions.get(id)?;
970            (d.dmabuf_formats.clone(), d.dmabuf_dev)
971        };
972        let Some((fourcc, mods)) = pick_dmabuf_format(&formats) else {
973            if debug() {
974                eprintln!("wlr-capture: no usable dma-buf format");
975            }
976            return None;
977        };
978        self.ensure_gbm(dev)?;
979        let gbm = self.gbm.as_ref()?;
980        let gfmt = GbmFormat::try_from(fourcc).ok()?;
981        let qh = &self.qh;
982
983        // Allocate one swapchain slot: a gbm bo wrapped as a dma-buf wl_buffer.
984        let alloc_slot = || -> Option<DmaBuf> {
985            let bo = gbm
986                .create_buffer_object_with_modifiers2::<()>(
987                    w,
988                    h,
989                    gfmt,
990                    mods.iter().map(|&m| Modifier::from(m)),
991                    BufferObjectFlags::RENDERING,
992                )
993                .ok()?;
994            let stride = bo.stride();
995            let offset = bo.offset(0);
996            let modifier: u64 = bo.modifier().into();
997            let fd = bo.fd().ok()?;
998
999            let params = dmabuf_mgr.create_params(qh, ());
1000            params.add(
1001                fd.as_fd(),
1002                0,
1003                offset,
1004                stride,
1005                (modifier >> 32) as u32,
1006                (modifier & 0xffff_ffff) as u32,
1007            );
1008            let buffer = params.create_immed(
1009                w as i32,
1010                h as i32,
1011                fourcc,
1012                zwp_linux_buffer_params_v1::Flags::empty(),
1013                qh,
1014                (),
1015            );
1016            params.destroy();
1017            Some(DmaBuf {
1018                buffer,
1019                bo,
1020                width: w,
1021                height: h,
1022                fourcc,
1023                modifier,
1024                stride,
1025                offset,
1026            })
1027        };
1028
1029        let buf = alloc_slot()?;
1030        if debug() {
1031            eprintln!(
1032                "wlr-chooser: dma-buf {w}x{h} fourcc={fourcc:#010x} modifier={}",
1033                buf.modifier
1034            );
1035        }
1036        Some(Buf::Dmabuf(buf))
1037    }
1038
1039    /// Open the gbm device for the compositor's advertised dma-buf device, once.
1040    /// Returns `None` (so callers fall back to shm) if it can't be opened.
1041    #[cfg(feature = "gpu")]
1042    fn ensure_gbm(&mut self, dev: Option<u64>) -> Option<()> {
1043        if self.gbm.is_some() {
1044            return Some(());
1045        }
1046        let path = render_node_for(dev);
1047        let file = std::fs::OpenOptions::new()
1048            .read(true)
1049            .write(true)
1050            .open(&path)
1051            .ok()?;
1052        let device = GbmDevice::new(file).ok()?;
1053        if debug() {
1054            eprintln!("wlr-chooser: gbm device {}", path.display());
1055        }
1056        self.gbm = Some(device);
1057        Some(())
1058    }
1059}
1060
1061/// Build a DRM fourcc code from its four ASCII bytes.
1062#[cfg(feature = "gpu")]
1063const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 {
1064    (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24)
1065}
1066
1067/// Pick a dma-buf format we can both allocate and decode, plus its usable
1068/// modifiers (dropping `INVALID`). Prefers the common 32-bit RGB layouts.
1069#[cfg(feature = "gpu")]
1070fn pick_dmabuf_format(formats: &[(u32, Vec<u64>)]) -> Option<(u32, Vec<u64>)> {
1071    let preferred = [
1072        fourcc(b'X', b'R', b'2', b'4'), // XRGB8888
1073        fourcc(b'A', b'R', b'2', b'4'), // ARGB8888
1074        fourcc(b'X', b'B', b'2', b'4'), // XBGR8888
1075        fourcc(b'A', b'B', b'2', b'4'), // ABGR8888
1076    ];
1077    for want in preferred {
1078        if PixelLayout::of_fourcc(want).is_none() {
1079            continue;
1080        }
1081        if let Some((_, mods)) = formats.iter().find(|(f, _)| *f == want) {
1082            let usable: Vec<u64> = mods
1083                .iter()
1084                .copied()
1085                .filter(|&m| m != DRM_MOD_INVALID)
1086                .collect();
1087            if !usable.is_empty() {
1088                return Some((want, usable));
1089            }
1090        }
1091    }
1092    None
1093}
1094
1095/// Resolve the DRM render node to allocate on. Best effort: match the advertised
1096/// dev_t against `/dev/dri/renderD*`, else the first render node, else renderD128.
1097#[cfg(feature = "gpu")]
1098fn render_node_for(dev: Option<u64>) -> std::path::PathBuf {
1099    use std::path::PathBuf;
1100    let render_nodes = || -> Vec<PathBuf> {
1101        let mut v: Vec<PathBuf> = std::fs::read_dir("/dev/dri")
1102            .into_iter()
1103            .flatten()
1104            .flatten()
1105            .map(|e| e.path())
1106            .filter(|p| {
1107                p.file_name()
1108                    .and_then(|n| n.to_str())
1109                    .is_some_and(|n| n.starts_with("renderD"))
1110            })
1111            .collect();
1112        v.sort();
1113        v
1114    };
1115    let nodes = render_nodes();
1116    if let Some(dev) = dev {
1117        for p in &nodes {
1118            if rustix::fs::stat(p).is_ok_and(|st| st.st_rdev == dev) {
1119                return p.clone();
1120            }
1121        }
1122    }
1123    nodes
1124        .into_iter()
1125        .next()
1126        .unwrap_or_else(|| PathBuf::from("/dev/dri/renderD128"))
1127}
1128
1129/// Whether verbose capture diagnostics are enabled (`WLR_CHOOSER_DEBUG`).
1130#[cfg(feature = "gpu")]
1131fn debug() -> bool {
1132    std::env::var_os("WLR_CHOOSER_DEBUG").is_some()
1133}
1134
1135/// Turn a ready capture into a [`Frame`] for the UI. shm is read back + converted
1136/// to RGBA on the CPU; dma-buf is handed off zero-copy as an fd to import as a GL
1137/// texture (re-exporting an fd for the buffer the compositor just wrote).
1138fn harvest(buf: &Buf) -> Option<Frame> {
1139    match buf {
1140        Buf::Shm(b) => {
1141            let layout = PixelLayout::of(b.format).expect("format validated at alloc time");
1142            let raw = unsafe { std::slice::from_raw_parts(b.map as *const u8, b.size) };
1143            Some(Frame::Shm(convert(
1144                raw, b.width, b.height, b.stride, &layout,
1145            )))
1146        }
1147        #[cfg(feature = "gpu")]
1148        Buf::Dmabuf(b) => {
1149            let fd = b.bo.fd().ok()?;
1150            Some(Frame::Dmabuf(DmabufFrame {
1151                fd,
1152                width: b.width,
1153                height: b.height,
1154                fourcc: b.fourcc,
1155                modifier: b.modifier,
1156                stride: b.stride,
1157                offset: b.offset,
1158            }))
1159        }
1160    }
1161}
1162
1163/// Pixel-format conversion to RGBA8 shared by the shm and dma-buf paths.
1164fn convert(raw: &[u8], w: u32, h: u32, stride: usize, layout: &PixelLayout) -> CapturedImage {
1165    let (w, h) = (w as usize, h as usize);
1166    let mut rgba = vec![0u8; w * h * 4];
1167    for y in 0..h {
1168        for x in 0..w {
1169            let s = y * stride + x * layout.bpp;
1170            let d = (y * w + x) * 4;
1171            rgba[d] = raw[s + layout.r];
1172            rgba[d + 1] = raw[s + layout.g];
1173            rgba[d + 2] = raw[s + layout.b];
1174            rgba[d + 3] = match layout.a {
1175                Some(a) => raw[s + a],
1176                None => 255,
1177            };
1178        }
1179    }
1180    CapturedImage {
1181        width: w as u32,
1182        height: h as u32,
1183        rgba,
1184    }
1185}
1186
1187// --- Window activation (zwlr-foreign-toplevel-management) ---
1188//
1189// Capture uses ext-foreign-toplevel-list (stable `identifier`), but activation
1190// needs zwlr handles, a separate object namespace. We correlate the two by
1191// app_id + title — the only key both expose. This is a self-contained, one-shot
1192// path on its own connection, run after the picker closes (so our overlay's
1193// keyboard grab is already gone and focus can move to the target).
1194
1195/// Enumeration state for [`activate_window`].
1196#[derive(Default)]
1197struct ActState {
1198    /// (handle, app_id, title) for every advertised toplevel.
1199    toplevels: Vec<(ZwlrForeignToplevelHandleV1, String, String)>,
1200}
1201
1202/// Focus the window matching `app_id` + `title` via zwlr-foreign-toplevel-manager.
1203/// `dup_index` selects among identical (app_id, title) windows by creation order
1204/// (both ext-foreign-toplevel-list and zwlr enumerate in that order on wlroots),
1205/// so the right one is focused even with duplicates.
1206pub fn activate_window(app_id: &str, title: &str, dup_index: usize) -> Result<()> {
1207    let conn = Connection::connect_to_env().context("Wayland connection")?;
1208    let (globals, mut queue) =
1209        registry_queue_init::<ActState>(&conn).context("registre Wayland")?;
1210    let qh = queue.handle();
1211    let _mgr: ZwlrForeignToplevelManagerV1 = globals
1212        .bind(&qh, 1..=3, ())
1213        .context("zwlr_foreign_toplevel_manager_v1 missing (unsupported compositor)")?;
1214    let seat: WlSeat = globals.bind(&qh, 1..=8, ()).context("wl_seat missing")?;
1215
1216    // Binding the manager makes the compositor advertise current toplevels.
1217    let mut st = ActState::default();
1218    queue.roundtrip(&mut st)?;
1219    queue.roundtrip(&mut st)?;
1220
1221    let handle = st
1222        .toplevels
1223        .iter()
1224        .filter(|(_, a, t)| a == app_id && t == title)
1225        .nth(dup_index)
1226        .or_else(|| {
1227            st.toplevels
1228                .iter()
1229                .find(|(_, a, t)| a == app_id && t == title)
1230        })
1231        .map(|(h, _, _)| h.clone())
1232        .with_context(|| format!("window to activate not found: {app_id} / {title}"))?;
1233    handle.activate(&seat);
1234    queue.roundtrip(&mut st)?; // flush the activate request
1235    Ok(())
1236}
1237
1238/// List the Wayland globals the current compositor advertises, as
1239/// `(interface, version)`. Used by `wlr-peek doctor` to report which capture
1240/// protocols (and therefore which features) are available.
1241pub fn advertised_globals() -> Result<Vec<(String, u32)>> {
1242    let conn = Connection::connect_to_env().context("Wayland connection")?;
1243    let (globals, _queue) = registry_queue_init::<ActState>(&conn).context("registre Wayland")?;
1244    let mut list = Vec::new();
1245    globals.contents().with_list(|globals| {
1246        for g in globals {
1247            list.push((g.interface.clone(), g.version));
1248        }
1249    });
1250    Ok(list)
1251}
1252
1253impl Dispatch<WlRegistry, GlobalListContents> for ActState {
1254    fn event(
1255        _: &mut Self,
1256        _: &WlRegistry,
1257        _: <WlRegistry as Proxy>::Event,
1258        _: &GlobalListContents,
1259        _: &Connection,
1260        _: &QueueHandle<Self>,
1261    ) {
1262    }
1263}
1264
1265impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for ActState {
1266    fn event(
1267        state: &mut Self,
1268        _: &ZwlrForeignToplevelManagerV1,
1269        event: zwlr_foreign_toplevel_manager_v1::Event,
1270        _: &(),
1271        _: &Connection,
1272        _: &QueueHandle<Self>,
1273    ) {
1274        if let zwlr_foreign_toplevel_manager_v1::Event::Toplevel { toplevel } = event {
1275            state
1276                .toplevels
1277                .push((toplevel, String::new(), String::new()));
1278        }
1279    }
1280
1281    event_created_child!(ActState, ZwlrForeignToplevelManagerV1, [
1282        zwlr_foreign_toplevel_manager_v1::EVT_TOPLEVEL_OPCODE => (ZwlrForeignToplevelHandleV1, ()),
1283    ]);
1284}
1285
1286impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for ActState {
1287    fn event(
1288        state: &mut Self,
1289        handle: &ZwlrForeignToplevelHandleV1,
1290        event: zwlr_foreign_toplevel_handle_v1::Event,
1291        _: &(),
1292        _: &Connection,
1293        _: &QueueHandle<Self>,
1294    ) {
1295        use zwlr_foreign_toplevel_handle_v1::Event;
1296        let Some(e) = state.toplevels.iter_mut().find(|(h, _, _)| h == handle) else {
1297            return;
1298        };
1299        match event {
1300            Event::AppId { app_id } => e.1 = app_id,
1301            Event::Title { title } => e.2 = title,
1302            _ => {}
1303        }
1304    }
1305}
1306
1307delegate_noop!(ActState: ignore WlSeat);
1308
1309// --- Dispatch ---
1310
1311impl Dispatch<WlRegistry, GlobalListContents> for State {
1312    fn event(
1313        _: &mut Self,
1314        _: &WlRegistry,
1315        _: <WlRegistry as Proxy>::Event,
1316        _: &GlobalListContents,
1317        _: &Connection,
1318        _: &QueueHandle<Self>,
1319    ) {
1320    }
1321}
1322
1323impl Dispatch<ExtForeignToplevelListV1, ()> for State {
1324    fn event(
1325        state: &mut Self,
1326        _: &ExtForeignToplevelListV1,
1327        event: ext_foreign_toplevel_list_v1::Event,
1328        _: &(),
1329        _: &Connection,
1330        _: &QueueHandle<Self>,
1331    ) {
1332        if let ext_foreign_toplevel_list_v1::Event::Toplevel { toplevel } = event {
1333            state.pending.push((toplevel, PendingToplevel::default()));
1334        }
1335    }
1336
1337    event_created_child!(State, ExtForeignToplevelListV1, [
1338        ext_foreign_toplevel_list_v1::EVT_TOPLEVEL_OPCODE => (ExtForeignToplevelHandleV1, ()),
1339    ]);
1340}
1341
1342impl Dispatch<ExtForeignToplevelHandleV1, ()> for State {
1343    fn event(
1344        state: &mut Self,
1345        handle: &ExtForeignToplevelHandleV1,
1346        event: ext_foreign_toplevel_handle_v1::Event,
1347        _: &(),
1348        _: &Connection,
1349        _: &QueueHandle<Self>,
1350    ) {
1351        use ext_foreign_toplevel_handle_v1::Event;
1352        let Some((_, p)) = state.pending.iter_mut().find(|(h, _)| h == handle) else {
1353            return;
1354        };
1355        match event {
1356            Event::Identifier { identifier } => p.identifier = identifier,
1357            Event::Title { title } => p.title = title,
1358            Event::AppId { app_id } => p.app_id = app_id,
1359            Event::Done => {
1360                if let Some(pos) = state.pending.iter().position(|(h, _)| h == handle) {
1361                    let (h, p) = state.pending.remove(pos);
1362                    state.toplevels.push(Toplevel {
1363                        handle: h,
1364                        identifier: p.identifier,
1365                        title: p.title,
1366                        app_id: p.app_id,
1367                    });
1368                }
1369            }
1370            Event::Closed => {
1371                state.pending.retain(|(h, _)| h != handle);
1372                state.toplevels.retain(|t| &t.handle != handle);
1373            }
1374            _ => {}
1375        }
1376    }
1377}
1378
1379impl State {
1380    /// The `Output` for `wl_output`, created (with neutral geometry) on first sight
1381    /// so `geometry`/`mode`/`scale` can land before `name`.
1382    fn output_entry(&mut self, output: &WlOutput) -> &mut Output {
1383        if let Some(i) = self.outputs.iter().position(|o| &o.wl_output == output) {
1384            return &mut self.outputs[i];
1385        }
1386        self.outputs.push(Output {
1387            wl_output: output.clone(),
1388            name: String::new(),
1389            logical_x: 0,
1390            logical_y: 0,
1391            logical_w: 0,
1392            logical_h: 0,
1393            phys_width: 0,
1394            phys_height: 0,
1395            scale: 1,
1396            transform: Transform::Normal,
1397            have_xdg: false,
1398        });
1399        self.outputs.last_mut().unwrap()
1400    }
1401}
1402
1403impl Dispatch<WlOutput, ()> for State {
1404    fn event(
1405        state: &mut Self,
1406        output: &WlOutput,
1407        event: <WlOutput as Proxy>::Event,
1408        _: &(),
1409        _: &Connection,
1410        _: &QueueHandle<Self>,
1411    ) {
1412        use wayland_client::protocol::wl_output::Event;
1413        match event {
1414            Event::Geometry {
1415                x, y, transform, ..
1416            } => {
1417                let o = state.output_entry(output);
1418                o.transform = transform.into_result().unwrap_or(Transform::Normal);
1419                // wl_output position is only a fallback; xdg-output is authoritative.
1420                if !o.have_xdg {
1421                    o.logical_x = x;
1422                    o.logical_y = y;
1423                }
1424            }
1425            // Keep only the active mode's resolution (physical pixels).
1426            Event::Mode {
1427                flags,
1428                width,
1429                height,
1430                ..
1431            } => {
1432                if flags
1433                    .into_result()
1434                    .is_ok_and(|f| f.contains(wl_output::Mode::Current))
1435                {
1436                    let o = state.output_entry(output);
1437                    o.phys_width = width;
1438                    o.phys_height = height;
1439                }
1440            }
1441            Event::Scale { factor } => {
1442                state.output_entry(output).scale = factor.max(1);
1443            }
1444            Event::Name { name } => {
1445                state.output_entry(output).name = name;
1446            }
1447            _ => {}
1448        }
1449    }
1450}
1451
1452impl Dispatch<ZxdgOutputV1, WlOutput> for State {
1453    fn event(
1454        state: &mut Self,
1455        _: &ZxdgOutputV1,
1456        event: <ZxdgOutputV1 as Proxy>::Event,
1457        wl_output: &WlOutput,
1458        _: &Connection,
1459        _: &QueueHandle<Self>,
1460    ) {
1461        use zxdg_output_v1::Event;
1462        match event {
1463            Event::LogicalPosition { x, y } => {
1464                let o = state.output_entry(wl_output);
1465                o.logical_x = x;
1466                o.logical_y = y;
1467                o.have_xdg = true;
1468            }
1469            Event::LogicalSize { width, height } => {
1470                let o = state.output_entry(wl_output);
1471                o.logical_w = width;
1472                o.logical_h = height;
1473                o.have_xdg = true;
1474            }
1475            _ => {}
1476        }
1477    }
1478}
1479
1480impl Dispatch<ExtImageCopyCaptureSessionV1, ()> for State {
1481    fn event(
1482        state: &mut Self,
1483        session: &ExtImageCopyCaptureSessionV1,
1484        event: ext_image_copy_capture_session_v1::Event,
1485        _: &(),
1486        _: &Connection,
1487        _: &QueueHandle<Self>,
1488    ) {
1489        use ext_image_copy_capture_session_v1::Event;
1490        let Some(d) = state.sessions.get_mut(&session.id()) else {
1491            return;
1492        };
1493        match event {
1494            Event::BufferSize { width, height } => {
1495                d.width = width;
1496                d.height = height;
1497            }
1498            Event::ShmFormat {
1499                format: WEnum::Value(f),
1500            } => d.format = Some(f),
1501            #[cfg(feature = "gpu")]
1502            Event::DmabufDevice { device } => {
1503                // dev_t as a native-endian byte array.
1504                if device.len() == 8 {
1505                    let mut b = [0u8; 8];
1506                    b.copy_from_slice(&device);
1507                    d.dmabuf_dev = Some(u64::from_ne_bytes(b));
1508                }
1509            }
1510            #[cfg(feature = "gpu")]
1511            Event::DmabufFormat { format, modifiers } => {
1512                // modifiers: array of native-endian u64.
1513                let mods = modifiers
1514                    .chunks_exact(8)
1515                    .map(|c| u64::from_ne_bytes(c.try_into().unwrap()))
1516                    .collect();
1517                d.dmabuf_formats.push((format, mods));
1518            }
1519            // A constraints group ends with `done`; flag a (re)allocation so a
1520            // resize between frames grows the buffer.
1521            Event::Done => {
1522                d.constraints_done = true;
1523                d.dirty = true;
1524            }
1525            Event::Stopped => d.stopped = true,
1526            _ => {}
1527        }
1528    }
1529}
1530
1531impl Dispatch<ExtImageCopyCaptureFrameV1, ObjectId> for State {
1532    fn event(
1533        state: &mut Self,
1534        _: &ExtImageCopyCaptureFrameV1,
1535        event: ext_image_copy_capture_frame_v1::Event,
1536        session_id: &ObjectId,
1537        _: &Connection,
1538        _: &QueueHandle<Self>,
1539    ) {
1540        use ext_image_copy_capture_frame_v1::Event;
1541        let Some(d) = state.sessions.get_mut(session_id) else {
1542            return;
1543        };
1544        match event {
1545            Event::Ready => d.ready = true,
1546            Event::Failed { reason } => {
1547                let reason = match reason {
1548                    WEnum::Value(r) => r,
1549                    _ => FailureReason::Unknown,
1550                };
1551                // Per the protocol, a frame failure means destroy the frame, not
1552                // the session. Only `stopped` is terminal; the rest are transient.
1553                if matches!(reason, FailureReason::Stopped) {
1554                    d.stopped = true;
1555                } else {
1556                    d.frame_failed = Some(reason);
1557                }
1558            }
1559            _ => {}
1560        }
1561    }
1562}
1563
1564#[cfg(test)]
1565mod tests {
1566    use super::*;
1567    use wayland_client::protocol::wl_shm::Format;
1568
1569    /// The heart of the grim-vs-wlr-chooser fix: bytes-per-pixel (hence stride) must
1570    /// match the advertised format. Bgr888 is 24-bit, so stride = width*3, not *4.
1571    #[test]
1572    fn pixel_layout_stride_and_alpha() {
1573        assert_eq!(PixelLayout::of(Format::Bgr888).unwrap().bpp, 3);
1574        assert_eq!(PixelLayout::of(Format::Rgb888).unwrap().bpp, 3);
1575        assert_eq!(PixelLayout::of(Format::Xrgb8888).unwrap().bpp, 4);
1576        assert_eq!(PixelLayout::of(Format::Argb8888).unwrap().bpp, 4);
1577
1578        assert!(PixelLayout::of(Format::Bgr888).unwrap().a.is_none());
1579        assert!(PixelLayout::of(Format::Xrgb8888).unwrap().a.is_none());
1580        assert_eq!(PixelLayout::of(Format::Argb8888).unwrap().a, Some(3));
1581        assert_eq!(PixelLayout::of(Format::Abgr8888).unwrap().a, Some(3));
1582    }
1583
1584    #[test]
1585    fn pixel_layout_unknown_format_is_none() {
1586        // A format we don't decode should be reported, not silently mishandled.
1587        assert!(PixelLayout::of(Format::C8).is_none());
1588    }
1589
1590    #[test]
1591    fn region_intersect() {
1592        let a = Region {
1593            x: 0,
1594            y: 0,
1595            w: 10,
1596            h: 10,
1597        };
1598        let b = Region {
1599            x: 5,
1600            y: 5,
1601            w: 10,
1602            h: 10,
1603        };
1604        assert_eq!(
1605            a.intersect(&b),
1606            Some(Region {
1607                x: 5,
1608                y: 5,
1609                w: 5,
1610                h: 5
1611            })
1612        );
1613        // Negative origin (selection partly off the image) clamps correctly.
1614        let c = Region {
1615            x: -3,
1616            y: -3,
1617            w: 6,
1618            h: 6,
1619        };
1620        assert_eq!(
1621            a.intersect(&c),
1622            Some(Region {
1623                x: 0,
1624                y: 0,
1625                w: 3,
1626                h: 3
1627            })
1628        );
1629        // Disjoint → None.
1630        let d = Region {
1631            x: 100,
1632            y: 100,
1633            w: 1,
1634            h: 1,
1635        };
1636        assert_eq!(a.intersect(&d), None);
1637    }
1638
1639    /// A 2×2 RGBA image: four distinct pixels, to verify pixel/crop addressing.
1640    fn img_2x2() -> CapturedImage {
1641        CapturedImage {
1642            width: 2,
1643            height: 2,
1644            rgba: vec![
1645                1, 1, 1, 255, 2, 2, 2, 255, // row 0: (0,0)=1, (1,0)=2
1646                3, 3, 3, 255, 4, 4, 4, 255, // row 1: (0,1)=3, (1,1)=4
1647            ],
1648        }
1649    }
1650
1651    #[test]
1652    fn captured_pixel_and_crop() {
1653        let img = img_2x2();
1654        assert_eq!(img.pixel(0, 0), Some([1, 1, 1, 255]));
1655        assert_eq!(img.pixel(1, 1), Some([4, 4, 4, 255]));
1656        assert_eq!(img.pixel(2, 0), None); // out of bounds
1657
1658        // Crop the bottom-right 1×1 pixel.
1659        let c = img.crop(Region {
1660            x: 1,
1661            y: 1,
1662            w: 1,
1663            h: 1,
1664        });
1665        assert_eq!((c.width, c.height), (1, 1));
1666        assert_eq!(c.rgba, vec![4, 4, 4, 255]);
1667
1668        // Crop overrunning the bounds clamps to the overlap.
1669        let c2 = img.crop(Region {
1670            x: 1,
1671            y: 0,
1672            w: 5,
1673            h: 5,
1674        });
1675        assert_eq!((c2.width, c2.height), (1, 2));
1676        assert_eq!(c2.rgba, vec![2, 2, 2, 255, 4, 4, 4, 255]);
1677    }
1678
1679    #[test]
1680    fn captured_blit_into() {
1681        // Blit the 2×2 image into a 3×2 black canvas at x=1, clipping the overflow.
1682        let img = img_2x2();
1683        let (dw, dh) = (3u32, 2u32);
1684        let mut dst = vec![0u8; (dw * dh * 4) as usize];
1685        img.blit_into(&mut dst, dw, dh, 1, 0);
1686        // Column 0 stays black; columns 1..3 get the image's two columns.
1687        assert_eq!(&dst[0..4], &[0, 0, 0, 0]); // (0,0)
1688        assert_eq!(&dst[4..8], &[1, 1, 1, 255]); // (1,0) = img (0,0)
1689        assert_eq!(&dst[8..12], &[2, 2, 2, 255]); // (2,0) = img (1,0)
1690        assert_eq!(&dst[12..16], &[0, 0, 0, 0]); // (0,1)
1691        assert_eq!(&dst[16..20], &[3, 3, 3, 255]); // (1,1) = img (0,1)
1692    }
1693
1694    #[test]
1695    fn output_logical_dims_transform() {
1696        // 4K at scale 2 → 1920×1080 logical.
1697        assert_eq!(logical_dims(3840, 2160, 2, Transform::Normal), (1920, 1080));
1698        // 90°/270° swap width and height.
1699        assert_eq!(logical_dims(3840, 2160, 2, Transform::_90), (1080, 1920));
1700        assert_eq!(
1701            logical_dims(3840, 2160, 2, Transform::Flipped270),
1702            (1080, 1920)
1703        );
1704        // 180° keeps orientation; scale 0 is treated as 1.
1705        assert_eq!(logical_dims(1000, 500, 0, Transform::_180), (1000, 500));
1706    }
1707}
1708
1709// Objects whose events we don't need.
1710delegate_noop!(State: ignore ZxdgOutputManagerV1);
1711delegate_noop!(State: ignore WlShm);
1712delegate_noop!(State: ignore WlShmPool);
1713delegate_noop!(State: ignore WlBuffer);
1714delegate_noop!(State: ignore ExtImageCaptureSourceV1);
1715delegate_noop!(State: ignore ExtForeignToplevelImageCaptureSourceManagerV1);
1716delegate_noop!(State: ignore ExtOutputImageCaptureSourceManagerV1);
1717delegate_noop!(State: ignore ExtImageCopyCaptureManagerV1);
1718// dma-buf: we drive allocation ourselves (gbm) and create buffers with
1719// `create_immed`, so the manager's format/modifier and the params' created/failed
1720// events carry nothing we need.
1721#[cfg(feature = "gpu")]
1722delegate_noop!(State: ignore ZwpLinuxDmabufV1);
1723#[cfg(feature = "gpu")]
1724delegate_noop!(State: ignore ZwpLinuxBufferParamsV1);