Skip to main content

pixelflux/wayland/
host.rs

1//! Host-capture mode: pixelflux as a CLIENT of an external Wayland compositor
2//! (e.g. labwc running `WLR_BACKENDS=headless`), inverting the nested topology.
3//!
4//! The host compositor owns the session — one seat, one selection, one screen
5//! model — and pixelflux captures and injects as a privileged-protocol client:
6//! frames via `ext_image_copy_capture_v1` where the host offers it (wlroots
7//! 0.19+, KWin 6.2+, cosmic) and `zwlr_screencopy_v1` (v3 `copy_with_damage`)
8//! everywhere else — both damage-gated, so idle screens cost nothing
9//! (`PIXELFLUX_HOST_CAPTURE=zwlr` forces the fallback); keyboard via a
10//! persistent `zwp_virtual_keyboard_v1` device carrying selkies' own keymap
11//! text, pointer via `zwlr_virtual_pointer_v1`. Zero-copy is preserved by
12//! allocating the capture buffers from pixelflux's OWN GBM device (render node —
13//! no privileges): the compositor blits straight into the dmabufs the encoder
14//! imports, so no CPU ever touches a frame. A host that cannot import our
15//! dmabufs (other GPU, software renderer) and any run without a GPU degrade to
16//! wl_shm buffers feeding the CPU encode pool.
17//!
18//! Every host `wl_output` maps to one selkies display id, in registry order: a
19//! control thread on the primary connection owns wlr-output-management and
20//! applies the whole layout (per-output custom modes plus row positions) as one
21//! atomic configuration, while each output gets its own capture connection and
22//! thread. Input proxies are called from the calloop thread directly
23//! (wayland-client proxies are thread-safe); pointer coordinates arrive in the
24//! same union-layout space selkies uses, so the virtual-pointer extent is the
25//! layout's bounding box. All capture-side waits poll a wake pipe next to the
26//! Wayland socket, so a resize or teardown lands immediately even while the
27//! thread is parked in a `copy_with_damage` wait on a static screen.
28
29use std::fs::File;
30use std::os::fd::{AsFd, AsRawFd, OwnedFd, RawFd};
31use std::os::unix::net::UnixStream;
32use std::sync::atomic::{AtomicBool, Ordering};
33use std::sync::mpsc::{Receiver, Sender, TryRecvError};
34use std::sync::{Arc, Mutex};
35use std::time::{Duration, Instant};
36
37use gbm::{BufferObjectFlags, Device as GbmDevice, Format as GbmFormat};
38use smithay::backend::allocator::dmabuf::Dmabuf;
39use smithay::backend::allocator::Buffer as _;
40use smithay::utils::{Physical, Rectangle};
41use wayland_client::protocol::{wl_output, wl_pointer, wl_registry, wl_seat, wl_shm, wl_shm_pool};
42use wayland_client::{delegate_noop, Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum};
43use wayland_protocols::ext::image_capture_source::v1::client::{
44    ext_image_capture_source_v1::ExtImageCaptureSourceV1,
45    ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1,
46};
47use wayland_protocols::ext::image_copy_capture::v1::client::{
48    ext_image_copy_capture_frame_v1::{self, ExtImageCopyCaptureFrameV1},
49    ext_image_copy_capture_manager_v1::{self, ExtImageCopyCaptureManagerV1},
50    ext_image_copy_capture_session_v1::{self, ExtImageCopyCaptureSessionV1},
51};
52use wayland_protocols::wp::linux_dmabuf::zv1::client::{
53    zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1},
54    zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1,
55};
56use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
57    zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1,
58    zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1,
59};
60use wayland_protocols_wlr::screencopy::v1::client::{
61    zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1},
62    zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
63};
64use wayland_protocols_wlr::output_management::v1::client::{
65    zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1,
66    zwlr_output_configuration_v1::{self, ZwlrOutputConfigurationV1},
67    zwlr_output_head_v1::{self, ZwlrOutputHeadV1},
68    zwlr_output_manager_v1::{self, ZwlrOutputManagerV1},
69    zwlr_output_mode_v1::ZwlrOutputModeV1,
70};
71use wayland_protocols_wlr::virtual_pointer::v1::client::{
72    zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
73    zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
74};
75
76use crate::wayland::wlclient::{
77    bounded_roundtrip, drain_pipe, impl_sync_callback, memfd_with, socket_path, wait_readable2,
78    wake_pipe, wake_write, SyncState,
79};
80
81const KEYMAP_FORMAT_XKB_V1: u32 = 1;
82/// Frames in flight between a capture thread and the encode tick. Two slots:
83/// one being blitted by the compositor while the previous one is encoded.
84const SLOTS: usize = 2;
85
86/// A frame the compositor finished blitting, ready for the encoder.
87pub struct HostFrame {
88    generation: u64,
89    slot: usize,
90    /// GPU path: the filled dmabuf (Arc-backed; the buffer itself stays owned by
91    /// the capture thread and is reused once the slot is released).
92    pub dmabuf: Option<Dmabuf>,
93    /// Software path: the shm mapping itself — the consumer converts straight
94    /// out of it (one copy total); the slot is not reused until release.
95    pub cpu: Option<HostCpuFrame>,
96    pub width: i32,
97    pub height: i32,
98    pub damage: Vec<Rectangle<i32, Physical>>,
99}
100
101/// Borrow-by-Arc view of a software frame still sitting in its shm mapping.
102pub struct HostCpuFrame {
103    map: Arc<memmap2::MmapMut>,
104    stride: usize,
105    format: u32,
106}
107
108/// Source bytes-per-pixel and whether R and B swap to reach BGRA, for one announced wl_shm
109/// format. The consumers downstream all read BGRA (byte order B,G,R,A), so a format is mapped by
110/// where its red and blue land in memory under the little-endian byte order wayland.xml specifies:
111///
112/// - `Xrgb8888` / `Argb8888` (`[31:0] x:R:G:B` LE) are B,G,R,x in memory: already BGRA, no swap.
113/// - `Xbgr8888` / `Abgr8888` (`[31:0] x:B:G:R` LE) are R,G,B,x in memory: swap R/B.
114/// - `Bgr888` (`[23:0] B:G:R` LE) is R,G,B in memory: swap R/B (the 24-bit NVIDIA-GLES read path).
115/// - `Rgb888` (`[23:0] R:G:B` LE) is B,G,R in memory: already BGR order, no swap.
116///
117/// The empirical NVIDIA-GLES host could not be exercised here, so `Bgr888` follows the wl_shm
118/// definition literally: `[23:0] B:G:R little endian` places R in the low byte, so byte 0 is red and
119/// reaching BGRA swaps R and B — the opposite of a straight copy.
120fn shm_src_layout(format: u32) -> (usize, bool) {
121    match format {
122        f if f == wl_shm::Format::Xbgr8888 as u32 || f == wl_shm::Format::Abgr8888 as u32 => {
123            (4, true)
124        }
125        f if f == wl_shm::Format::Bgr888 as u32 => (3, true),
126        f if f == wl_shm::Format::Rgb888 as u32 => (3, false),
127        _ => (4, false),
128    }
129}
130
131/// Convert one source row to a BGRA destination row per `(src_bpp, swap_rb)` from
132/// `shm_src_layout`: a 4-byte source either copies straight or swaps R/B keeping alpha, a 3-byte
133/// source fills opaque alpha and swaps R/B only when the layout calls for it.
134fn convert_shm_row(src: &[u8], dst: &mut [u8], src_bpp: usize, swap_rb: bool) {
135    match (src_bpp, swap_rb) {
136        (4, false) => {
137            let n = dst.len().min(src.len());
138            dst[..n].copy_from_slice(&src[..n]);
139        }
140        (4, true) => {
141            for (d, s) in dst.chunks_exact_mut(4).zip(src.chunks_exact(4)) {
142                d[0] = s[2];
143                d[1] = s[1];
144                d[2] = s[0];
145                d[3] = s[3];
146            }
147        }
148        (_, true) => {
149            for (d, s) in dst.chunks_exact_mut(4).zip(src.chunks_exact(3)) {
150                d[0] = s[2];
151                d[1] = s[1];
152                d[2] = s[0];
153                d[3] = 0xff;
154            }
155        }
156        (_, false) => {
157            for (d, s) in dst.chunks_exact_mut(4).zip(src.chunks_exact(3)) {
158                d[0] = s[0];
159                d[1] = s[1];
160                d[2] = s[2];
161                d[3] = 0xff;
162            }
163        }
164    }
165}
166
167impl HostCpuFrame {
168    /// Convert the frame into tight BGRA rows in `dst` (sized `w*4*h`); the announced shm
169    /// format decides the per-pixel conversion (compositors pick their renderer's preferred read
170    /// format, e.g. 24-bit BGR on NVIDIA GLES).
171    pub fn write_bgra(&self, w: i32, h: i32, dst: &mut [u8]) {
172        let row = (w * 4) as usize;
173        let (src_bpp, swap_rb) = shm_src_layout(self.format);
174        for y in 0..h as usize {
175            let src_start = y * self.stride;
176            let src_end = (src_start + src_bpp * w as usize).min(self.map.len());
177            if src_start >= src_end || (y + 1) * row > dst.len() {
178                break;
179            }
180            let src_row = &self.map[src_start..src_end];
181            let dst_row = &mut dst[y * row..(y + 1) * row];
182            convert_shm_row(src_row, dst_row, src_bpp, swap_rb);
183        }
184    }
185}
186
187/// One display's slice of the union layout, as selkies configured it. Slots are
188/// keyed by selkies display id ('display2' is id 2 — ids are sparse); active ids
189/// map onto host outputs by ascending rank.
190#[derive(Clone, Copy, Default)]
191struct LayoutSlot {
192    want: (i32, i32),
193    pos: (i32, i32),
194    active: bool,
195    /// The consumer imports dmabufs directly, so the compositor blits into GPU
196    /// buffers. A CPU encode path clears this and takes shm frames instead.
197    zero_copy: bool,
198    /// The host paints its cursor into the frames (the consumer's native cursor
199    /// rendering); off, the consumer draws the cursor itself from the sprite callback.
200    paint_cursor: bool,
201}
202
203/// What a capture thread is currently aiming at: the encoder's dimensions, the buffer
204/// type its consumer can take, and whether the host paints the cursor in. A size or
205/// buffer-type change forces a slot renegotiation; a cursor change reopens the ext
206/// session, whose option is fixed at creation.
207#[derive(Clone, Copy, PartialEq, Eq)]
208struct Want {
209    size: (i32, i32),
210    zero_copy: bool,
211    paint_cursor: bool,
212}
213
214enum ToHost {
215    Start { width: i32, height: i32, zero_copy: bool, paint_cursor: bool },
216    /// Slot return; `generation` guards against slots recycled by a renegotiation
217    /// while the consumer still held the frame.
218    Release { generation: u64, slot: usize },
219    /// Park the capture (display stopped) without ending the thread.
220    Idle,
221}
222
223enum CtrlMsg {
224    Apply { epoch: u64, slots: Vec<LayoutSlot> },
225}
226
227/// Outcome ledger for layout requests: every Apply carries an epoch, and the
228/// control thread records whether the newest one it processed was realized by
229/// the host. A request is answered once `decided` has caught up to its epoch;
230/// a newer request subsumes an older one (the layout map is cumulative), so
231/// the newest verdict stands for every epoch at or below it. Polled, never
232/// waited on: the session side reads it from its own loop.
233#[derive(Default)]
234struct LayoutLedger {
235    epoch: u64,
236    decided: u64,
237    realized: bool,
238}
239
240impl LayoutLedger {
241    /// Number the next request.
242    fn issue(&mut self) -> u64 {
243        self.epoch += 1;
244        self.epoch
245    }
246
247    /// Record the host's verdict on `epoch` (ignored if a newer one is already in).
248    fn decide(&mut self, epoch: u64, realized: bool) {
249        if epoch > self.decided {
250            self.decided = epoch;
251            self.realized = realized;
252        }
253    }
254
255    /// The verdict covering `epoch`: `None` while the host has not answered it yet.
256    fn outcome(&self, epoch: u64) -> Option<bool> {
257        (self.decided >= epoch).then_some(self.realized)
258    }
259}
260
261/// Bound for one layout application on the control thread: a host that has not
262/// answered by then counts as having kept its own mode, which the session then
263/// follows, the way a refusal is handled.
264pub const LAYOUT_DEADLINE: Duration = Duration::from_secs(5);
265
266// ---------------------------------------------------------------------------
267// Control connection: seat + virtual input devices + wlr-output-management.
268// ---------------------------------------------------------------------------
269
270#[derive(Default)]
271struct CtrlState {
272    seat: Option<wl_seat::WlSeat>,
273    vk_mgr: Option<ZwpVirtualKeyboardManagerV1>,
274    vptr_mgr: Option<ZwlrVirtualPointerManagerV1>,
275    has_screencopy: bool,
276    has_ext_capture: bool,
277    has_ext_source: bool,
278    outputs: Vec<(wl_output::WlOutput, Option<String>)>,
279    /// Current mode per output, registry-indexed and shared with the session:
280    /// when the host keeps its own size (refused or unmanageable layout), the
281    /// capture follows this instead of gating on the size it asked for.
282    sizes: Arc<Mutex<Vec<(i32, i32)>>>,
283    /// Registry indices in display order (natural name sort — registry
284    /// announcement order is not guaranteed to match output creation order).
285    order: Vec<usize>,
286    output_mgr: Option<ZwlrOutputManagerV1>,
287    heads: Vec<(ZwlrOutputHeadV1, Option<String>)>,
288    om_serial: Option<u32>,
289    cfg_result: Option<bool>,
290    cfg_cancelled: bool,
291    sync_done: bool,
292}
293
294
295/// Natural sort key for an output name: text prefix plus trailing number, so
296/// HEADLESS-2 orders before HEADLESS-10. Unnamed outputs keep registry order
297/// after every named one.
298fn output_order_key(name: Option<&String>, registry_idx: usize) -> (bool, String, u64, usize) {
299    match name {
300        Some(n) => {
301            let digits_at = n.rfind(|c: char| !c.is_ascii_digit()).map(|i| i + 1).unwrap_or(0);
302            let num = n[digits_at..].parse::<u64>().unwrap_or(0);
303            (false, n[..digits_at].to_string(), num, registry_idx)
304        }
305        None => (true, String::new(), 0, registry_idx),
306    }
307}
308
309impl SyncState for CtrlState {
310    fn sync_done_mut(&mut self) -> &mut bool {
311        &mut self.sync_done
312    }
313}
314impl_sync_callback!(CtrlState);
315
316impl Dispatch<wl_registry::WlRegistry, ()> for CtrlState {
317    fn event(
318        state: &mut Self,
319        registry: &wl_registry::WlRegistry,
320        event: wl_registry::Event,
321        _: &(),
322        _: &Connection,
323        qh: &QueueHandle<Self>,
324    ) {
325        if let wl_registry::Event::Global { name, interface, version } = event {
326            match interface.as_str() {
327                "wl_seat" if state.seat.is_none() => {
328                    state.seat = Some(registry.bind(name, 1, qh, ()))
329                }
330                "zwp_virtual_keyboard_manager_v1" => {
331                    state.vk_mgr = Some(registry.bind(name, 1, qh, ()))
332                }
333                "zwlr_virtual_pointer_manager_v1" => {
334                    state.vptr_mgr = Some(registry.bind(name, version.min(2), qh, ()))
335                }
336                "zwlr_screencopy_manager_v1" if version >= 3 => state.has_screencopy = true,
337                "ext_image_copy_capture_manager_v1" => state.has_ext_capture = true,
338                "ext_output_image_capture_source_manager_v1" => state.has_ext_source = true,
339                "wl_output" => {
340                    let idx = state.outputs.len();
341                    let out = registry.bind(name, version.min(4), qh, idx);
342                    state.outputs.push((out, None));
343                }
344                "zwlr_output_manager_v1" => {
345                    state.output_mgr = Some(registry.bind(name, 1, qh, ()))
346                }
347                _ => {}
348            }
349        }
350    }
351}
352
353macro_rules! impl_output_name {
354    ($t:ty) => {
355        impl Dispatch<wl_output::WlOutput, usize> for $t {
356            fn event(
357                state: &mut Self,
358                _: &wl_output::WlOutput,
359                event: wl_output::Event,
360                idx: &usize,
361                _: &Connection,
362                _: &QueueHandle<Self>,
363            ) {
364                if let wl_output::Event::Name { name } = event {
365                    if let Some(o) = state.outputs.get_mut(*idx) {
366                        o.1 = Some(name);
367                    }
368                }
369            }
370        }
371    };
372}
373impl Dispatch<wl_output::WlOutput, usize> for CtrlState {
374    fn event(
375        state: &mut Self,
376        _: &wl_output::WlOutput,
377        event: wl_output::Event,
378        idx: &usize,
379        _: &Connection,
380        _: &QueueHandle<Self>,
381    ) {
382        match event {
383            wl_output::Event::Name { name } => {
384                if let Some(o) = state.outputs.get_mut(*idx) {
385                    o.1 = Some(name);
386                }
387            }
388            wl_output::Event::Mode { flags, width, height, .. } => {
389                if flags
390                    .into_result()
391                    .is_ok_and(|f| f.contains(wl_output::Mode::Current))
392                {
393                    let mut sizes = state.sizes.lock().unwrap();
394                    if sizes.len() <= *idx {
395                        sizes.resize(*idx + 1, (0, 0));
396                    }
397                    sizes[*idx] = (width, height);
398                }
399            }
400            _ => {}
401        }
402    }
403}
404
405delegate_noop!(CtrlState: ignore wl_seat::WlSeat);
406delegate_noop!(CtrlState: ZwpVirtualKeyboardManagerV1);
407delegate_noop!(CtrlState: ZwpVirtualKeyboardV1);
408delegate_noop!(CtrlState: ZwlrVirtualPointerManagerV1);
409delegate_noop!(CtrlState: ZwlrVirtualPointerV1);
410delegate_noop!(CtrlState: ignore ZwlrOutputModeV1);
411delegate_noop!(CtrlState: ZwlrOutputConfigurationHeadV1);
412
413impl Dispatch<ZwlrOutputManagerV1, ()> for CtrlState {
414    fn event(
415        state: &mut Self,
416        _: &ZwlrOutputManagerV1,
417        event: zwlr_output_manager_v1::Event,
418        _: &(),
419        _: &Connection,
420        _: &QueueHandle<Self>,
421    ) {
422        match event {
423            zwlr_output_manager_v1::Event::Head { head } => state.heads.push((head, None)),
424            zwlr_output_manager_v1::Event::Done { serial } => state.om_serial = Some(serial),
425            _ => {}
426        }
427    }
428
429    wayland_client::event_created_child!(CtrlState, ZwlrOutputManagerV1, [
430        zwlr_output_manager_v1::EVT_HEAD_OPCODE => (ZwlrOutputHeadV1, ()),
431    ]);
432}
433
434impl Dispatch<ZwlrOutputHeadV1, ()> for CtrlState {
435    fn event(
436        state: &mut Self,
437        head: &ZwlrOutputHeadV1,
438        event: zwlr_output_head_v1::Event,
439        _: &(),
440        _: &Connection,
441        _: &QueueHandle<Self>,
442    ) {
443        match event {
444            zwlr_output_head_v1::Event::Name { name } => {
445                if let Some(h) = state.heads.iter_mut().find(|(h, _)| h.id() == head.id()) {
446                    h.1 = Some(name);
447                }
448            }
449            zwlr_output_head_v1::Event::Finished => {
450                state.heads.retain(|(h, _)| h.id() != head.id());
451            }
452            _ => {}
453        }
454    }
455
456    wayland_client::event_created_child!(CtrlState, ZwlrOutputHeadV1, [
457        zwlr_output_head_v1::EVT_MODE_OPCODE => (ZwlrOutputModeV1, ()),
458    ]);
459}
460
461impl Dispatch<ZwlrOutputConfigurationV1, ()> for CtrlState {
462    fn event(
463        state: &mut Self,
464        _: &ZwlrOutputConfigurationV1,
465        event: zwlr_output_configuration_v1::Event,
466        _: &(),
467        _: &Connection,
468        _: &QueueHandle<Self>,
469    ) {
470        match event {
471            zwlr_output_configuration_v1::Event::Succeeded => state.cfg_result = Some(true),
472            zwlr_output_configuration_v1::Event::Failed => state.cfg_result = Some(false),
473            zwlr_output_configuration_v1::Event::Cancelled => state.cfg_cancelled = true,
474            _ => {}
475        }
476    }
477}
478
479// ---------------------------------------------------------------------------
480// Capture connections: one per host output (screencopy + buffer allocation).
481// ---------------------------------------------------------------------------
482
483#[derive(Default)]
484struct CaptureState {
485    shm: Option<wl_shm::WlShm>,
486    dmabuf: Option<ZwpLinuxDmabufV1>,
487    screencopy: Option<ZwlrScreencopyManagerV1>,
488    ext_capture: Option<ExtImageCopyCaptureManagerV1>,
489    ext_source_mgr: Option<ExtOutputImageCaptureSourceManagerV1>,
490    outputs: Vec<(wl_output::WlOutput, Option<String>)>,
491    // Per-frame capture negotiation/results: the buffer types screencopy offered, as
492    // (fourcc, width, height) for dmabuf and (format, width, height, stride) for shm.
493    announce_dmabuf: Option<(u32, i32, i32)>,
494    announce_shm: Option<(u32, i32, i32, i32)>,
495    buffer_done: bool,
496    damage: Vec<Rectangle<i32, Physical>>,
497    ready: bool,
498    failed: bool,
499    sync_done: bool,
500    // Non-immed zwp_linux_buffer_params result: the host either delivered a
501    // wl_buffer for our dmabuf or refused the import.
502    params_created: Option<wayland_client::protocol::wl_buffer::WlBuffer>,
503    params_failed: bool,
504    // ext session constraints stream in flight; `done` seals it into the committed
505    // set and bumps the serial (a new set arrives whenever the output changes).
506    ext_pending_size: Option<(i32, i32)>,
507    ext_pending_shm: Vec<u32>,
508    ext_pending_dma: Vec<(u32, Vec<u64>)>,
509    ext_size: Option<(i32, i32)>,
510    ext_shm_formats: Vec<u32>,
511    ext_dma_formats: Vec<(u32, Vec<u64>)>,
512    ext_serial: u64,
513    ext_stopped: bool,
514    ext_fail_reason: Option<ext_image_copy_capture_frame_v1::FailureReason>,
515}
516
517
518impl CaptureState {
519    fn reset_frame(&mut self) {
520        self.announce_dmabuf = None;
521        self.announce_shm = None;
522        self.buffer_done = false;
523        self.damage.clear();
524        self.ready = false;
525        self.failed = false;
526        self.ext_fail_reason = None;
527    }
528}
529
530impl SyncState for CaptureState {
531    fn sync_done_mut(&mut self) -> &mut bool {
532        &mut self.sync_done
533    }
534}
535impl_sync_callback!(CaptureState);
536impl_output_name!(CaptureState);
537
538impl Dispatch<wl_registry::WlRegistry, ()> for CaptureState {
539    fn event(
540        state: &mut Self,
541        registry: &wl_registry::WlRegistry,
542        event: wl_registry::Event,
543        _: &(),
544        _: &Connection,
545        qh: &QueueHandle<Self>,
546    ) {
547        if let wl_registry::Event::Global { name, interface, version } = event {
548            match interface.as_str() {
549                "wl_shm" => state.shm = Some(registry.bind(name, 1, qh, ())),
550                "zwp_linux_dmabuf_v1" if version >= 3 => {
551                    state.dmabuf = Some(registry.bind(name, 3, qh, ()))
552                }
553                "zwlr_screencopy_manager_v1" if version >= 3 => {
554                    state.screencopy = Some(registry.bind(name, 3, qh, ()))
555                }
556                "ext_image_copy_capture_manager_v1" => {
557                    state.ext_capture = Some(registry.bind(name, 1, qh, ()))
558                }
559                "ext_output_image_capture_source_manager_v1" => {
560                    state.ext_source_mgr = Some(registry.bind(name, 1, qh, ()))
561                }
562                "wl_output" => {
563                    let idx = state.outputs.len();
564                    let out = registry.bind(name, version.min(4), qh, idx);
565                    state.outputs.push((out, None));
566                }
567                _ => {}
568            }
569        }
570    }
571}
572
573delegate_noop!(CaptureState: ignore wl_shm::WlShm);
574delegate_noop!(CaptureState: ignore wl_shm_pool::WlShmPool);
575delegate_noop!(CaptureState: ignore wayland_client::protocol::wl_buffer::WlBuffer);
576delegate_noop!(CaptureState: ZwlrScreencopyManagerV1);
577delegate_noop!(CaptureState: ExtImageCopyCaptureManagerV1);
578delegate_noop!(CaptureState: ExtOutputImageCaptureSourceManagerV1);
579delegate_noop!(CaptureState: ExtImageCaptureSourceV1);
580
581impl Dispatch<ExtImageCopyCaptureSessionV1, ()> for CaptureState {
582    fn event(
583        state: &mut Self,
584        _: &ExtImageCopyCaptureSessionV1,
585        event: ext_image_copy_capture_session_v1::Event,
586        _: &(),
587        _: &Connection,
588        _: &QueueHandle<Self>,
589    ) {
590        use ext_image_copy_capture_session_v1::Event;
591        match event {
592            Event::BufferSize { width, height } => {
593                state.ext_pending_size = Some((width as i32, height as i32));
594            }
595            Event::ShmFormat { format } => {
596                let raw = match format {
597                    WEnum::Value(f) => f as u32,
598                    WEnum::Unknown(u) => u,
599                };
600                state.ext_pending_shm.push(raw);
601            }
602            Event::DmabufDevice { .. } => {
603                // Buffers are allocated from pixelflux's own render node, like the
604                // wlr-screencopy path; a cross-device host fails the dmabuf import
605                // and the shm formats below still apply.
606            }
607            Event::DmabufFormat { format, modifiers } => {
608                let mods = modifiers
609                    .chunks_exact(8)
610                    .map(|c| u64::from_ne_bytes(c.try_into().unwrap()))
611                    .collect();
612                state.ext_pending_dma.push((format, mods));
613            }
614            Event::Done => {
615                state.ext_size = state.ext_pending_size.take();
616                state.ext_shm_formats = std::mem::take(&mut state.ext_pending_shm);
617                state.ext_dma_formats = std::mem::take(&mut state.ext_pending_dma);
618                state.ext_serial += 1;
619            }
620            Event::Stopped => state.ext_stopped = true,
621            _ => {}
622        }
623    }
624}
625
626impl Dispatch<ExtImageCopyCaptureFrameV1, ()> for CaptureState {
627    fn event(
628        state: &mut Self,
629        _: &ExtImageCopyCaptureFrameV1,
630        event: ext_image_copy_capture_frame_v1::Event,
631        _: &(),
632        _: &Connection,
633        _: &QueueHandle<Self>,
634    ) {
635        use ext_image_copy_capture_frame_v1::Event;
636        match event {
637            Event::Damage { x, y, width, height } => {
638                state.damage.push(Rectangle::new(
639                    (x, y).into(),
640                    (width, height).into(),
641                ));
642            }
643            Event::Ready => state.ready = true,
644            Event::Failed { reason } => {
645                state.failed = true;
646                if let WEnum::Value(r) = reason {
647                    state.ext_fail_reason = Some(r);
648                }
649            }
650            _ => {}
651        }
652    }
653}
654
655impl Dispatch<ZwpLinuxDmabufV1, ()> for CaptureState {
656    fn event(
657        _: &mut Self,
658        _: &ZwpLinuxDmabufV1,
659        _: <ZwpLinuxDmabufV1 as Proxy>::Event,
660        _: &(),
661        _: &Connection,
662        _: &QueueHandle<Self>,
663    ) {
664        // format/modifier advertisements: the allocation follows the screencopy
665        // frame's announcement instead.
666    }
667}
668
669impl Dispatch<ZwpLinuxBufferParamsV1, ()> for CaptureState {
670    fn event(
671        state: &mut Self,
672        _: &ZwpLinuxBufferParamsV1,
673        event: zwp_linux_buffer_params_v1::Event,
674        _: &(),
675        _: &Connection,
676        _: &QueueHandle<Self>,
677    ) {
678        match event {
679            zwp_linux_buffer_params_v1::Event::Created { buffer } => {
680                state.params_created = Some(buffer);
681            }
682            zwp_linux_buffer_params_v1::Event::Failed => state.params_failed = true,
683            _ => {}
684        }
685    }
686
687    fn event_created_child(
688        _opcode: u16,
689        qhandle: &QueueHandle<Self>,
690    ) -> std::sync::Arc<dyn wayland_client::backend::ObjectData> {
691        qhandle.make_data::<wayland_client::protocol::wl_buffer::WlBuffer, _>(())
692    }
693}
694
695impl Dispatch<ZwlrScreencopyFrameV1, ()> for CaptureState {
696    fn event(
697        state: &mut Self,
698        _: &ZwlrScreencopyFrameV1,
699        event: zwlr_screencopy_frame_v1::Event,
700        _: &(),
701        _: &Connection,
702        _: &QueueHandle<Self>,
703    ) {
704        match event {
705            zwlr_screencopy_frame_v1::Event::Buffer {
706                format: WEnum::Value(f), width, height, stride,
707            } => {
708                state.announce_shm = Some((f as u32, width as i32, height as i32, stride as i32));
709            }
710            zwlr_screencopy_frame_v1::Event::LinuxDmabuf { format, width, height } => {
711                state.announce_dmabuf = Some((format, width as i32, height as i32));
712            }
713            zwlr_screencopy_frame_v1::Event::BufferDone => state.buffer_done = true,
714            zwlr_screencopy_frame_v1::Event::Damage { x, y, width, height } => {
715                state.damage.push(Rectangle::new(
716                    (x as i32, y as i32).into(),
717                    (width as i32, height as i32).into(),
718                ));
719            }
720            zwlr_screencopy_frame_v1::Event::Ready { .. } => state.ready = true,
721            zwlr_screencopy_frame_v1::Event::Failed => state.failed = true,
722            _ => {}
723        }
724    }
725}
726
727enum SlotBuffer {
728    Gpu {
729        _bo: gbm::BufferObject<()>,
730        dmabuf: Dmabuf,
731        wl: wayland_client::protocol::wl_buffer::WlBuffer,
732    },
733    Cpu {
734        _pool: wl_shm_pool::WlShmPool,
735        map: Arc<memmap2::MmapMut>,
736        stride: i32,
737        format: u32,
738        wl: wayland_client::protocol::wl_buffer::WlBuffer,
739    },
740}
741
742/// Release the host proxies a slot created. Dropping a wayland-client proxy sends no request,
743/// so without this every resize or buffer-type switch strands the slot's `wl_buffer` — and, on the
744/// shm path, its `wl_shm_pool` — in the host compositor for the connection's lifetime. The buffer is
745/// destroyed before the pool it was created from.
746impl Drop for SlotBuffer {
747    fn drop(&mut self) {
748        match self {
749            SlotBuffer::Gpu { wl, .. } => wl.destroy(),
750            SlotBuffer::Cpu { _pool, wl, .. } => {
751                wl.destroy();
752                _pool.destroy();
753            }
754        }
755    }
756}
757
758/// One host output's calloop-side handle: control channel, wake pipe, frames.
759struct OutputHandle {
760    to_thread: Sender<ToHost>,
761    wake: OwnedFd,
762    frames: Receiver<HostFrame>,
763    /// Newest frame, kept (slot and all) until replaced so an IDR request on a
764    /// static screen can re-encode current content like compositor mode does.
765    retained: Mutex<Option<HostFrame>>,
766    name: Option<String>,
767}
768
769impl OutputHandle {
770    fn send(&self, msg: ToHost) {
771        let _ = self.to_thread.send(msg);
772        wake_write(self.wake.as_raw_fd());
773    }
774}
775
776/// The calloop-side session: input proxies plus one capture handle per output.
777pub struct HostSession {
778    conn: Connection,
779    vk: Option<ZwpVirtualKeyboardV1>,
780    vptr: Option<ZwlrVirtualPointerV1>,
781    ctrl_tx: Sender<CtrlMsg>,
782    ctrl_wake: OwnedFd,
783    outputs: Vec<OutputHandle>,
784    layout: Mutex<std::collections::BTreeMap<u32, LayoutSlot>>,
785    /// Rank -> registry index, for looking a display's host output up in `sizes`.
786    order: Vec<usize>,
787    sizes: Arc<Mutex<Vec<(i32, i32)>>>,
788    layouts: Arc<Mutex<LayoutLedger>>,
789    alive: Arc<AtomicBool>,
790}
791
792impl HostSession {
793    /// Connect to `display`, bring up input devices, enumerate the host's
794    /// outputs and spawn one capture thread per output (idle until
795    /// [`start_capture`]) plus the layout-control thread. `gbm_path` (this
796    /// process's render node) enables the zero-copy path.
797    pub fn connect(display: &str, gbm_path: Option<std::path::PathBuf>) -> Result<Self, String> {
798        let path = socket_path(display).ok_or("XDG_RUNTIME_DIR is unset")?;
799        let stream = UnixStream::connect(&path).map_err(|e| format!("connect {path}: {e}"))?;
800        let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
801        let mut queue = conn.new_event_queue();
802        let qh = queue.handle();
803        let _registry = conn.display().get_registry(&qh, ());
804        let mut state = CtrlState::default();
805        bounded_roundtrip(&conn, &mut queue, &mut state)?;
806
807        let seat = state.seat.clone().ok_or("host compositor advertises no wl_seat")?;
808        if !state.has_screencopy && !(state.has_ext_capture && state.has_ext_source) {
809            return Err(
810                "host compositor offers neither ext-image-copy-capture nor zwlr_screencopy_manager_v1 (v3)"
811                    .into(),
812            );
813        }
814        if state.outputs.is_empty() {
815            return Err("host compositor has no wl_output".into());
816        }
817
818        let vk = match &state.vk_mgr {
819            Some(mgr) => {
820                let vk = mgr.create_virtual_keyboard(&seat, &qh, ());
821                // A keymap must precede any key event; selkies replaces this with
822                // its managed keymap through the ABI as soon as it starts.
823                if let Some(text) = crate::wayland::vkclient::us_base_text() {
824                    let mut data = text.as_bytes().to_vec();
825                    data.push(0);
826                    let fd = memfd_with(&data)?;
827                    vk.keymap(KEYMAP_FORMAT_XKB_V1, fd.as_fd(), data.len() as u32);
828                }
829                Some(vk)
830            }
831            None => {
832                eprintln!("[HostCapture] no zwp_virtual_keyboard_manager_v1: keyboard injection disabled.");
833                None
834            }
835        };
836        let vptr = match &state.vptr_mgr {
837            Some(mgr) => Some(mgr.create_virtual_pointer(Some(&seat), &qh, ())),
838            None => {
839                eprintln!("[HostCapture] no zwlr_virtual_pointer_manager_v1: pointer injection disabled.");
840                None
841            }
842        };
843        // Second round-trip: wl_output v4 name events and output-management heads.
844        bounded_roundtrip(&conn, &mut queue, &mut state)?;
845
846        let alive = Arc::new(AtomicBool::new(true));
847        let mut order: Vec<usize> = (0..state.outputs.len()).collect();
848        order.sort_by_key(|&i| output_order_key(state.outputs[i].1.as_ref(), i));
849        let names: Vec<Option<String>> =
850            order.iter().map(|&i| state.outputs[i].1.clone()).collect();
851        state.order = order;
852        println!(
853            "[HostCapture] host outputs: {}.",
854            names
855                .iter()
856                .enumerate()
857                .map(|(i, n)| format!("{i}={}", n.as_deref().unwrap_or("?")))
858                .collect::<Vec<_>>()
859                .join(", ")
860        );
861
862        let mut outputs = Vec::new();
863        for (i, name) in names.iter().cloned().enumerate() {
864            let (wake_rd, wake_wr) = wake_pipe()?;
865            let (to_thread, from_main) = std::sync::mpsc::channel::<ToHost>();
866            let (frame_tx, frames) = std::sync::mpsc::channel::<HostFrame>();
867            let display = display.to_string();
868            let expect = name.clone();
869            let gbm_path = gbm_path.clone();
870            let alive = alive.clone();
871            std::thread::Builder::new()
872                .name(format!("pf-host-cap{i}"))
873                .spawn(move || {
874                    if let Err(e) =
875                        capture_loop(&display, i, expect, gbm_path, from_main, wake_rd, frame_tx)
876                    {
877                        eprintln!("[HostCapture] output {i} capture ended: {e}");
878                    }
879                    if i == 0 {
880                        alive.store(false, Ordering::Relaxed);
881                    }
882                })
883                .map_err(|e| format!("spawn: {e}"))?;
884            outputs.push(OutputHandle {
885                to_thread,
886                wake: wake_wr,
887                frames,
888                retained: Mutex::new(None),
889                name,
890            });
891        }
892
893        let (ctrl_tx, ctrl_rx) = std::sync::mpsc::channel::<CtrlMsg>();
894        let (ctrl_wake_rd, ctrl_wake) = wake_pipe()?;
895        let order_for_session = state.order.clone();
896        let sizes = state.sizes.clone();
897        let layouts = Arc::new(Mutex::new(LayoutLedger::default()));
898        {
899            let conn = conn.clone();
900            let ledger = layouts.clone();
901            let alive = alive.clone();
902            std::thread::Builder::new()
903                .name("pf-host-ctrl".into())
904                .spawn(move || {
905                    control_loop(conn, queue, state, ctrl_rx, ctrl_wake_rd, ledger);
906                    // The primary connection carries the virtual keyboard and pointer: its
907                    // end is the session's end, whatever the capture threads still do.
908                    alive.store(false, Ordering::Relaxed);
909                })
910                .map_err(|e| format!("spawn: {e}"))?;
911        }
912
913        let layout = Mutex::new(std::collections::BTreeMap::new());
914        Ok(Self {
915            conn,
916            vk,
917            vptr,
918            ctrl_tx,
919            ctrl_wake,
920            outputs,
921            layout,
922            order: order_for_session,
923            sizes,
924            layouts,
925            alive,
926        })
927    }
928
929    pub fn output_count(&self) -> usize {
930        self.outputs.len()
931    }
932
933    /// The host output backing `display_id`: active display ids map onto host
934    /// outputs by ascending rank (selkies ids are sparse — 'display2' is id 2).
935    fn output_index_for(&self, display_id: u32) -> Option<usize> {
936        let layout = self.layout.lock().unwrap();
937        let rank = layout
938            .iter()
939            .filter(|(_, s)| s.active)
940            .position(|(id, _)| *id == display_id)?;
941        (rank < self.outputs.len()).then_some(rank)
942    }
943
944    /// True when an active capture on `display_id` has a host output behind it.
945    pub fn has_output_for(&self, display_id: u32) -> bool {
946        self.output_index_for(display_id).is_some()
947    }
948
949    /// Record where selkies laid out `display_id` (union coordinates); pushed
950    /// to the host with the next capture (re)start.
951    pub fn set_layout(&self, display_id: u32, x: i32, y: i32) {
952        let mut layout = self.layout.lock().unwrap();
953        layout.entry(display_id).or_default().pos = (x, y);
954    }
955
956    /// The current mode of the host output backing `display_id` (physical
957    /// pixels), as the host last announced it — what a capture follows when the
958    /// host keeps its own mode instead of taking the requested one.
959    pub fn current_output_size(&self, display_id: u32) -> Option<(i32, i32)> {
960        let rank = self.output_index_for(display_id)?;
961        let registry_idx = *self.order.get(rank)?;
962        let size = *self.sizes.lock().unwrap().get(registry_idx)?;
963        (size.0 > 0 && size.1 > 0).then_some(size)
964    }
965
966    /// Number a layout request and hand it to the control thread.
967    fn send_apply(&self, slots: Vec<LayoutSlot>) -> u64 {
968        let epoch = self.layouts.lock().unwrap().issue();
969        let _ = self.ctrl_tx.send(CtrlMsg::Apply { epoch, slots });
970        wake_write(self.ctrl_wake.as_raw_fd());
971        epoch
972    }
973
974    /// The host's verdict on the layout request `epoch` (as returned by
975    /// [`start_capture`]): `Some(true)` once the host applied it, `Some(false)`
976    /// once it kept its own modes (refusal, no layout management at all — KWin —
977    /// or no answer within `LAYOUT_DEADLINE`), `None` while still unanswered.
978    /// Never blocks; the caller polls it from its own loop and, on `Some(false)`,
979    /// compares [`current_output_size`] with what it asked for.
980    pub fn layout_outcome(&self, epoch: u64) -> Option<bool> {
981        self.layouts.lock().unwrap().outcome(epoch)
982    }
983
984    /// Aim `display_id` at `width`x`height`: assigns every active display its
985    /// host output (by rank), asks the host for all modes and positions in one
986    /// atomic configuration, and points the capture threads at their sizes and
987    /// buffer types; frames gate until the host applies them. `zero_copy` states
988    /// whether this display's encoder imports dmabufs — a CPU consumer needs shm
989    /// frames it can read, and gets them even on a GPU-capable host. Returns the
990    /// layout request's epoch for [`layout_outcome`]; nothing here waits on the host.
991    pub fn start_capture(
992        &self,
993        display_id: u32,
994        width: i32,
995        height: i32,
996        zero_copy: bool,
997        paint_cursor: bool,
998    ) -> u64 {
999        let assignments = {
1000            let mut layout = self.layout.lock().unwrap();
1001            let slot = layout.entry(display_id).or_default();
1002            slot.want = (width, height);
1003            slot.zero_copy = zero_copy;
1004            slot.paint_cursor = paint_cursor;
1005            slot.active = true;
1006            let active: Vec<(u32, LayoutSlot)> = layout
1007                .iter()
1008                .filter(|(_, s)| s.active)
1009                .map(|(id, s)| (*id, *s))
1010                .collect();
1011            if active.iter().position(|(id, _)| *id == display_id).unwrap_or(usize::MAX)
1012                >= self.outputs.len()
1013            {
1014                eprintln!(
1015                    "[HostCapture] display {display_id} has no host output (host has {}); not captured.",
1016                    self.outputs.len()
1017                );
1018            }
1019            active
1020        };
1021        // Ranks may have shifted (a lower id joined): repoint every assigned
1022        // capture thread; a same-size Start is a no-op for an unaffected one.
1023        let mut by_output: Vec<LayoutSlot> = Vec::new();
1024        for (rank, (_, slot)) in assignments.iter().enumerate() {
1025            if rank >= self.outputs.len() {
1026                break;
1027            }
1028            // A mode change invalidates everything buffered for this output:
1029            // those frames belong to the old geometry and would otherwise be
1030            // consumed at the new one.
1031            let size_mismatch = self.outputs[rank]
1032                .retained
1033                .lock()
1034                .unwrap()
1035                .as_ref()
1036                .is_some_and(|f| f.width != slot.want.0 || f.height != slot.want.1);
1037            if size_mismatch {
1038                self.drop_buffered_frames(rank);
1039            }
1040            self.outputs[rank].send(ToHost::Start {
1041                width: slot.want.0,
1042                height: slot.want.1,
1043                zero_copy: slot.zero_copy,
1044                paint_cursor: slot.paint_cursor,
1045            });
1046            by_output.push(*slot);
1047        }
1048        self.send_apply(by_output)
1049    }
1050
1051    /// Point `display_id`'s capture at dmabufs (`zero_copy`) or shm frames without touching its
1052    /// mode or position, which is what an encoder demoted or rebuilt mid-capture needs: the
1053    /// layout is already what the host applied, and re-running it would mode-set the host
1054    /// display again. Idempotent, so a consumer that keeps seeing the wrong buffer type until
1055    /// the capture thread reallocates can call this on every frame.
1056    pub fn set_buffer_type(&self, display_id: u32, zero_copy: bool) {
1057        let target = {
1058            let mut layout = self.layout.lock().unwrap();
1059            let slot = layout.entry(display_id).or_default();
1060            if slot.zero_copy == zero_copy {
1061                return;
1062            }
1063            slot.zero_copy = zero_copy;
1064            let (want, active, paint_cursor) = (slot.want, slot.active, slot.paint_cursor);
1065            let rank = layout
1066                .iter()
1067                .filter(|(_, s)| s.active)
1068                .position(|(id, _)| *id == display_id);
1069            match rank {
1070                Some(r) if active && r < self.outputs.len() => (r, want, paint_cursor),
1071                _ => return,
1072            }
1073        };
1074        let (idx, want, paint_cursor) = target;
1075        // Same geometry, new buffer type: a retained dmabuf cannot be consumed
1076        // by the shm path (nor vice versa), so everything buffered goes back.
1077        self.drop_buffered_frames(idx);
1078        self.outputs[idx].send(ToHost::Start {
1079            width: want.0,
1080            height: want.1,
1081            zero_copy,
1082            paint_cursor,
1083        });
1084    }
1085
1086    /// Switch every active capture between host-painted and consumer-drawn cursors
1087    /// (the consumer's native cursor rendering toggle), keeping geometry and buffer type.
1088    /// Frames already buffered stay valid either way. Idempotent.
1089    pub fn set_cursor_painting(&self, paint_cursor: bool) {
1090        let targets: Vec<(usize, LayoutSlot)> = {
1091            let mut layout = self.layout.lock().unwrap();
1092            let mut out = Vec::new();
1093            for (rank, (_, slot)) in layout.iter_mut().filter(|(_, s)| s.active).enumerate() {
1094                if rank >= self.outputs.len() {
1095                    break;
1096                }
1097                if slot.paint_cursor != paint_cursor {
1098                    slot.paint_cursor = paint_cursor;
1099                    out.push((rank, *slot));
1100                }
1101            }
1102            out
1103        };
1104        for (idx, slot) in targets {
1105            self.outputs[idx].send(ToHost::Start {
1106                width: slot.want.0,
1107                height: slot.want.1,
1108                zero_copy: slot.zero_copy,
1109                paint_cursor,
1110            });
1111        }
1112    }
1113
1114    /// Drop every buffered frame for an output: a mode or buffer-type change
1115    /// makes the cached pixels unusable — consumed at the new geometry they
1116    /// smear garbage rows or fault an encoder built for the new size. The
1117    /// slots go straight back to the pool; dropping them silently would wedge
1118    /// a slot until the next renegotiation.
1119    fn drop_buffered_frames(&self, idx: usize) {
1120        let handle = &self.outputs[idx];
1121        if let Some(old) = handle.retained.lock().unwrap().take() {
1122            handle.send(ToHost::Release { generation: old.generation, slot: old.slot });
1123        }
1124        while let Ok(frame) = handle.frames.try_recv() {
1125            handle.send(ToHost::Release { generation: frame.generation, slot: frame.slot });
1126        }
1127    }
1128
1129    /// Park `display_id`'s capture (its slot leaves the pointer extent). The
1130    /// retained frame and anything still queued go back to the capture pool —
1131    /// an idle output has no consumer for them, and dropping them silently
1132    /// would wedge slots until the next renegotiation.
1133    pub fn idle_output(&self, display_id: u32) {
1134        let Some(idx) = self.output_index_for(display_id) else {
1135            self.layout.lock().unwrap().entry(display_id).or_default().active = false;
1136            return;
1137        };
1138        self.layout.lock().unwrap().entry(display_id).or_default().active = false;
1139        self.outputs[idx].send(ToHost::Idle);
1140        self.drop_buffered_frames(idx);
1141    }
1142
1143    /// False once the host connection is gone (the primary connection or the first
1144    /// output's capture ended): the owner drops the session and reports its captures as
1145    /// stopped, so they are rebuilt against a new connection instead of feeding a dead one.
1146    pub fn alive(&self) -> bool {
1147        self.alive.load(Ordering::Relaxed)
1148    }
1149
1150    /// Hand one frame back to the pool (same path the drain/idle releases take).
1151    pub fn release_frame(&self, display_id: u32, frame: HostFrame) {
1152        if let Some(idx) = self.output_index_for(display_id) {
1153            self.outputs[idx].send(ToHost::Release {
1154                generation: frame.generation,
1155                slot: frame.slot,
1156            });
1157        }
1158    }
1159
1160    /// Newest ready frame for `display_id`, releasing any staler ones straight
1161    /// back to the pool.
1162    pub fn try_take_frame(&self, display_id: u32) -> Option<HostFrame> {
1163        let handle = self.outputs.get(self.output_index_for(display_id)?)?;
1164        let mut newest: Option<HostFrame> = None;
1165        while let Ok(frame) = handle.frames.try_recv() {
1166            if let Some(stale) = newest.replace(frame) {
1167                handle.send(ToHost::Release { generation: stale.generation, slot: stale.slot });
1168            }
1169        }
1170        newest
1171    }
1172
1173    /// Keep `frame` as `display_id`'s current content (releasing the one it
1174    /// replaces).
1175    pub fn retain_frame(&self, display_id: u32, frame: HostFrame) {
1176        let Some(handle) = self.output_index_for(display_id).and_then(|i| self.outputs.get(i))
1177        else {
1178            return;
1179        };
1180        let old = handle.retained.lock().unwrap().replace(frame);
1181        if let Some(old) = old {
1182            handle.send(ToHost::Release { generation: old.generation, slot: old.slot });
1183        }
1184    }
1185
1186    /// Run `f` with `display_id`'s retained (current-content) frame, if any.
1187    pub fn with_retained<R>(
1188        &self,
1189        display_id: u32,
1190        f: impl FnOnce(Option<&HostFrame>) -> R,
1191    ) -> R {
1192        match self.output_index_for(display_id).and_then(|i| self.outputs.get(i)) {
1193            Some(handle) => f(handle.retained.lock().unwrap().as_ref()),
1194            None => f(None),
1195        }
1196    }
1197
1198    /// Upload selkies' managed keymap to the virtual keyboard verbatim.
1199    pub fn set_keymap(&self, text: &str) {
1200        let Some(vk) = &self.vk else { return };
1201        let mut data = text.as_bytes().to_vec();
1202        data.push(0);
1203        match memfd_with(&data) {
1204            Ok(fd) => {
1205                vk.keymap(KEYMAP_FORMAT_XKB_V1, fd.as_fd(), data.len() as u32);
1206                let _ = self.conn.flush();
1207            }
1208            Err(e) => eprintln!("[HostCapture] keymap upload failed: {e}"),
1209        }
1210    }
1211
1212    /// Key event in xkb numbering (evdev + 8), matching the seat injectors.
1213    pub fn key(&self, xkb_keycode: u32, pressed: bool) {
1214        let Some(vk) = &self.vk else { return };
1215        if xkb_keycode < 8 {
1216            return;
1217        }
1218        vk.key(0, xkb_keycode - 8, if pressed { 1 } else { 0 });
1219        let _ = self.conn.flush();
1220    }
1221
1222    /// Union-layout bounding box of every active, output-backed display (the
1223    /// virtual-pointer extent, matching the space selkies injects in).
1224    fn extent(&self) -> (i32, i32) {
1225        let layout = self.layout.lock().unwrap();
1226        let mut w = 0;
1227        let mut h = 0;
1228        for (rank, (_, slot)) in layout.iter().filter(|(_, s)| s.active).enumerate() {
1229            if rank >= self.outputs.len() {
1230                break;
1231            }
1232            w = w.max(slot.pos.0 + slot.want.0);
1233            h = h.max(slot.pos.1 + slot.want.1);
1234        }
1235        (w, h)
1236    }
1237
1238    pub fn pointer_motion_abs(&self, x: f64, y: f64) {
1239        let Some(vp) = &self.vptr else { return };
1240        let (w, h) = self.extent();
1241        if w <= 0 || h <= 0 {
1242            return;
1243        }
1244        let cx = x.clamp(0.0, (w - 1) as f64) as u32;
1245        let cy = y.clamp(0.0, (h - 1) as f64) as u32;
1246        vp.motion_absolute(0, cx, cy, w as u32, h as u32);
1247        vp.frame();
1248        let _ = self.conn.flush();
1249    }
1250
1251    /// Relative motion in union-layout units, sent as the host's own relative motion: the
1252    /// host moves its pointer by the delta and clamps it to its outputs, or, for a
1253    /// pointer-locked client, passes the delta on without moving it. An absolute warp
1254    /// against a position tracked here could not do the second: the locked host pointer
1255    /// stays put, so every warp would read as the whole distance from the lock point.
1256    pub fn pointer_motion_rel(&self, dx: f64, dy: f64) {
1257        let Some(vp) = &self.vptr else { return };
1258        if dx == 0.0 && dy == 0.0 {
1259            return;
1260        }
1261        vp.motion(0, dx, dy);
1262        vp.frame();
1263        let _ = self.conn.flush();
1264    }
1265
1266    pub fn pointer_button(&self, btn: u32, pressed: bool) {
1267        let Some(vp) = &self.vptr else { return };
1268        vp.button(
1269            0,
1270            btn,
1271            if pressed { wl_pointer::ButtonState::Pressed } else { wl_pointer::ButtonState::Released },
1272        );
1273        vp.frame();
1274        let _ = self.conn.flush();
1275    }
1276
1277    /// Wheel scroll: the continuous value plus the discrete step count the seat path reports
1278    /// as v120, so host clients that scroll by notches (terminals, games, GTK list views)
1279    /// see the same steps as in nested mode; a value below one notch stays continuous. The
1280    /// source follows each axis request, which is the axis it applies to on wlroots.
1281    pub fn pointer_axis(&self, dx: f64, dy: f64) {
1282        let Some(vp) = &self.vptr else { return };
1283        if dx == 0.0 && dy == 0.0 {
1284            return;
1285        }
1286        for (axis, value) in [
1287            (wl_pointer::Axis::VerticalScroll, dy),
1288            (wl_pointer::Axis::HorizontalScroll, dx),
1289        ] {
1290            if value == 0.0 {
1291                continue;
1292            }
1293            let steps = (value * crate::SCROLL_V120_PER_UNIT / 120.0).round() as i32;
1294            if steps != 0 {
1295                vp.axis_discrete(0, axis, value, steps);
1296            } else {
1297                vp.axis(0, axis, value);
1298            }
1299            vp.axis_source(wl_pointer::AxisSource::Wheel);
1300        }
1301        vp.frame();
1302        let _ = self.conn.flush();
1303    }
1304}
1305
1306// Dropping the session drops every channel sender and wake-pipe write end; the
1307// capture and control threads observe the disconnect (poll sees POLLHUP) and
1308// exit on their own.
1309
1310// ---------------------------------------------------------------------------
1311// Control thread: pumps the primary connection and applies layout requests.
1312// ---------------------------------------------------------------------------
1313
1314fn control_loop(
1315    conn: Connection,
1316    mut queue: EventQueue<CtrlState>,
1317    mut state: CtrlState,
1318    ctrl_rx: Receiver<CtrlMsg>,
1319    wake_rd: OwnedFd,
1320    ledger: Arc<Mutex<LayoutLedger>>,
1321) {
1322    let qh = queue.handle();
1323    loop {
1324        let mut pending: Option<(u64, Vec<LayoutSlot>)> = None;
1325        loop {
1326            match ctrl_rx.try_recv() {
1327                Ok(CtrlMsg::Apply { epoch, slots }) => pending = Some((epoch, slots)),
1328                Err(TryRecvError::Empty) => break,
1329                Err(TryRecvError::Disconnected) => return,
1330            }
1331        }
1332        if let Some((epoch, slots)) = pending {
1333            let realized = apply_layout(&conn, &mut queue, &mut state, &qh, &slots);
1334            ledger.lock().unwrap().decide(epoch, realized);
1335        }
1336        if queue.dispatch_pending(&mut state).is_err() {
1337            return;
1338        }
1339        let _ = queue.flush();
1340        if let Some(guard) = conn.prepare_read() {
1341            match wait_readable2(
1342                guard.connection_fd().as_raw_fd(),
1343                wake_rd.as_raw_fd(),
1344                Some(Duration::from_secs(1)),
1345            ) {
1346                Ok((wl, wake)) => {
1347                    if wake {
1348                        drain_pipe(wake_rd.as_raw_fd());
1349                    }
1350                    if wl {
1351                        let _ = guard.read();
1352                    } else {
1353                        drop(guard);
1354                    }
1355                }
1356                Err(_) => return,
1357            }
1358        }
1359    }
1360}
1361
1362/// Ask the host (wlr-output-management) to give every active output its wanted
1363/// mode and layout position in one atomic configuration. Retries across a
1364/// `cancelled` (stale serial). Returns whether the host realized the layout:
1365/// on `false` (refusal, no answer by `LAYOUT_DEADLINE`, or no manager at all —
1366/// KWin offers only its own kde_output_management protocol) the session owner
1367/// re-sizes its capture to the host's actual mode so capture follows the host
1368/// instead of gating forever on a size it will never produce.
1369fn apply_layout(
1370    conn: &Connection,
1371    queue: &mut EventQueue<CtrlState>,
1372    state: &mut CtrlState,
1373    qh: &QueueHandle<CtrlState>,
1374    slots: &[LayoutSlot],
1375) -> bool {
1376    let Some(mgr) = state.output_mgr.clone() else {
1377        eprintln!("[HostCapture] host lacks zwlr_output_manager_v1; capture follows the host's own size.");
1378        return false;
1379    };
1380    let deadline = Instant::now() + LAYOUT_DEADLINE;
1381    for _ in 0..3 {
1382        while state.om_serial.is_none() && Instant::now() < deadline {
1383            if !pump_ctrl(conn, queue, state) {
1384                return false;
1385            }
1386        }
1387        let Some(serial) = state.om_serial else {
1388            eprintln!("[HostCapture] output-management serial never arrived; resize skipped.");
1389            return false;
1390        };
1391        state.cfg_result = None;
1392        state.cfg_cancelled = false;
1393        let cfg = mgr.create_configuration(serial, qh, ());
1394        let mut any = false;
1395        for (i, slot) in slots.iter().enumerate() {
1396            if !slot.active {
1397                continue;
1398            }
1399            let want_name = state
1400                .order
1401                .get(i)
1402                .and_then(|&oi| state.outputs.get(oi))
1403                .and_then(|(_, n)| n.clone());
1404            let head = match want_name
1405                .as_ref()
1406                .and_then(|n| state.heads.iter().find(|(_, hn)| hn.as_ref() == Some(n)))
1407                .or_else(|| state.heads.get(i))
1408            {
1409                Some((h, _)) => h.clone(),
1410                None => {
1411                    eprintln!("[HostCapture] no output-management head for output {i}; not resized.");
1412                    continue;
1413                }
1414            };
1415            let cfg_head = cfg.enable_head(&head, qh, ());
1416            cfg_head.set_custom_mode(slot.want.0, slot.want.1, 0);
1417            cfg_head.set_position(slot.pos.0, slot.pos.1);
1418            any = true;
1419        }
1420        if !any {
1421            cfg.destroy();
1422            return false;
1423        }
1424        cfg.apply();
1425        let _ = queue.flush();
1426        while state.cfg_result.is_none() && !state.cfg_cancelled && Instant::now() < deadline {
1427            if !pump_ctrl(conn, queue, state) {
1428                cfg.destroy();
1429                return false;
1430            }
1431        }
1432        cfg.destroy();
1433        if state.cfg_cancelled {
1434            // Stale serial: the compositor re-announces its state with a fresh one.
1435            state.om_serial = None;
1436            continue;
1437        }
1438        if state.cfg_result != Some(true) {
1439            eprintln!("[HostCapture] host refused the layout; capture follows the host's own size.");
1440            return false;
1441        }
1442        return true;
1443    }
1444    false
1445}
1446
1447/// One bounded dispatch step for the control connection (false = connection died).
1448fn pump_ctrl(conn: &Connection, queue: &mut EventQueue<CtrlState>, state: &mut CtrlState) -> bool {
1449    if queue.dispatch_pending(state).is_err() {
1450        return false;
1451    }
1452    let _ = queue.flush();
1453    let Some(guard) = conn.prepare_read() else { return true };
1454    match crate::wayland::wlclient::wait_readable(
1455        guard.connection_fd().as_raw_fd(),
1456        Duration::from_millis(200),
1457    ) {
1458        Ok(readable) => {
1459            if readable {
1460                let _ = guard.read();
1461            } else {
1462                drop(guard);
1463            }
1464            true
1465        }
1466        Err(_) => false,
1467    }
1468}
1469
1470// ---------------------------------------------------------------------------
1471// Capture threads: one connection + screencopy loop per host output.
1472// ---------------------------------------------------------------------------
1473
1474fn fourcc_to_gbm(fourcc: u32) -> GbmFormat {
1475    // XR24 / AR24; anything else falls back to ARGB (the encoder reads BGRA bytes
1476    // and ignores alpha).
1477    match fourcc {
1478        0x34325258 => GbmFormat::Xrgb8888,
1479        _ => GbmFormat::Argb8888,
1480    }
1481}
1482
1483/// A dmabuf-backed capture slot: a GBM buffer object from pixelflux's render node,
1484/// exported and wrapped in a host `wl_buffer`. `modifiers` narrows the allocation to
1485/// what the host advertised; empty leaves the driver's choice, which the host learns
1486/// through the params anyway. `Ok(None)` means the host refused the import (wrong
1487/// device, unsupported modifier): the caller degrades to shm slots instead of dying
1488/// to the protocol error `create_immed` would raise.
1489#[allow(clippy::too_many_arguments)]
1490fn alloc_gpu_slot(
1491    conn: &Connection,
1492    queue: &mut EventQueue<CaptureState>,
1493    state: &mut CaptureState,
1494    wake: RawFd,
1495    dev: &GbmDevice<File>,
1496    dmabuf_global: &ZwpLinuxDmabufV1,
1497    fourcc: u32,
1498    w: i32,
1499    h: i32,
1500    modifiers: &[u64],
1501) -> Result<Option<SlotBuffer>, String> {
1502    let qh = queue.handle();
1503    let format = fourcc_to_gbm(fourcc);
1504    let bo = if modifiers.is_empty() {
1505        dev.create_buffer_object::<()>(w as u32, h as u32, format, BufferObjectFlags::RENDERING)
1506    } else {
1507        dev.create_buffer_object_with_modifiers2::<()>(
1508            w as u32,
1509            h as u32,
1510            format,
1511            modifiers.iter().map(|&m| gbm::Modifier::from(m)),
1512            BufferObjectFlags::RENDERING,
1513        )
1514        .or_else(|_| {
1515            dev.create_buffer_object::<()>(w as u32, h as u32, format, BufferObjectFlags::RENDERING)
1516        })
1517    }
1518    .map_err(|e| format!("GBM allocation {w}x{h}: {e:?}"))?;
1519    let dmabuf = crate::create_dmabuf_from_bo(&bo);
1520    let params = dmabuf_global.create_params(&qh, ());
1521    let modifier: u64 = dmabuf.format().modifier.into();
1522    for (i, handle) in dmabuf.handles().enumerate() {
1523        params.add(
1524            handle,
1525            i as u32,
1526            dmabuf.offsets().nth(i).unwrap_or(0),
1527            dmabuf.strides().nth(i).unwrap_or(0),
1528            (modifier >> 32) as u32,
1529            (modifier & 0xffff_ffff) as u32,
1530        );
1531    }
1532    state.params_created = None;
1533    state.params_failed = false;
1534    params.create(w, h, fourcc, zwp_linux_buffer_params_v1::Flags::empty());
1535    loop {
1536        match pump_until(conn, queue, state, wake, Some(Duration::from_secs(2)), |s| {
1537            s.params_created.is_some() || s.params_failed
1538        })? {
1539            Pump::Done => break,
1540            Pump::Control => continue,
1541            Pump::Timeout => {
1542                params.destroy();
1543                return Err("host answered neither created nor failed for the dmabuf".into());
1544            }
1545        }
1546    }
1547    params.destroy();
1548    match state.params_created.take() {
1549        Some(wl) => Ok(Some(SlotBuffer::Gpu { _bo: bo, dmabuf, wl })),
1550        None => Ok(None),
1551    }
1552}
1553
1554/// An shm-backed capture slot in a memfd pool the CPU consumer reads in place.
1555fn alloc_cpu_slot(
1556    shm: &wl_shm::WlShm,
1557    qh: &QueueHandle<CaptureState>,
1558    format: u32,
1559    w: i32,
1560    h: i32,
1561    stride: i32,
1562) -> Result<SlotBuffer, String> {
1563    let size = (stride * h) as usize;
1564    let fd = memfd_with(&vec![0u8; size])?;
1565    let pool = shm.create_pool(fd.as_fd(), size as i32, qh, ());
1566    let wl = pool.create_buffer(
1567        0,
1568        w,
1569        h,
1570        stride,
1571        WEnum::<wl_shm::Format>::from(format).into_result().unwrap_or(wl_shm::Format::Xrgb8888),
1572        qh,
1573        (),
1574    );
1575    let file = File::from(fd);
1576    let map = unsafe { memmap2::MmapMut::map_mut(&file) }.map_err(|e| format!("shm map: {e}"))?;
1577    Ok(SlotBuffer::Cpu { _pool: pool, map: Arc::new(map), stride, format, wl })
1578}
1579
1580/// What a control-channel drain decided while a capture wait was in progress.
1581enum Ctl {
1582    None,
1583    Renegotiate,
1584    Idle,
1585    Dead,
1586}
1587
1588enum Pump {
1589    Done,
1590    Control,
1591    Timeout,
1592}
1593
1594/// Dispatch events until `done(state)` holds, waiting on the Wayland socket and
1595/// the wake pipe together so control messages interrupt any capture wait. The
1596/// predicate is re-checked after every dispatch, so a wait whose condition was
1597/// satisfied by already-queued events returns without ever blocking.
1598fn pump_until(
1599    conn: &Connection,
1600    queue: &mut EventQueue<CaptureState>,
1601    state: &mut CaptureState,
1602    wake_rd: RawFd,
1603    timeout: Option<Duration>,
1604    done: impl Fn(&CaptureState) -> bool,
1605) -> Result<Pump, String> {
1606    let deadline = timeout.map(|t| Instant::now() + t);
1607    loop {
1608        queue.dispatch_pending(state).map_err(|e| format!("dispatch: {e}"))?;
1609        if done(state) {
1610            return Ok(Pump::Done);
1611        }
1612        queue.flush().map_err(|e| format!("flush: {e}"))?;
1613        let remaining = match deadline {
1614            Some(d) => match d.checked_duration_since(Instant::now()) {
1615                Some(r) => Some(r),
1616                None => return Ok(Pump::Timeout),
1617            },
1618            None => None,
1619        };
1620        let Some(guard) = conn.prepare_read() else { continue };
1621        let (wl, wake) = wait_readable2(guard.connection_fd().as_raw_fd(), wake_rd, remaining)?;
1622        if wake {
1623            drop(guard);
1624            drain_pipe(wake_rd);
1625            return Ok(Pump::Control);
1626        }
1627        if wl {
1628            let _ = guard.read();
1629        } else {
1630            drop(guard);
1631            if deadline.is_some_and(|d| Instant::now() >= d) {
1632                return Ok(Pump::Timeout);
1633            }
1634        }
1635    }
1636}
1637
1638fn capture_loop(
1639    display: &str,
1640    index: usize,
1641    expect_name: Option<String>,
1642    gbm_path: Option<std::path::PathBuf>,
1643    from_main: Receiver<ToHost>,
1644    wake_rd: OwnedFd,
1645    frame_tx: Sender<HostFrame>,
1646) -> Result<(), String> {
1647    let path = socket_path(display).ok_or("XDG_RUNTIME_DIR is unset")?;
1648    let stream = UnixStream::connect(&path).map_err(|e| format!("connect {path}: {e}"))?;
1649    let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
1650    let mut queue = conn.new_event_queue();
1651    let qh = queue.handle();
1652    let _registry = conn.display().get_registry(&qh, ());
1653    let mut state = CaptureState::default();
1654    bounded_roundtrip(&conn, &mut queue, &mut state)?;
1655    // Names (wl_output v4) arrive after the binds from the first round-trip.
1656    bounded_roundtrip(&conn, &mut queue, &mut state)?;
1657
1658    // Select this thread's output by the name the control connection assigned
1659    // it; ordering fallback only when the host names nothing.
1660    let by_name = expect_name.as_ref().and_then(|expect| {
1661        state.outputs.iter().find(|(_, n)| n.as_ref() == Some(expect)).cloned()
1662    });
1663    let (output, _name) = match by_name {
1664        Some(o) => o,
1665        None => {
1666            let mut order: Vec<usize> = (0..state.outputs.len()).collect();
1667            order.sort_by_key(|&i| output_order_key(state.outputs[i].1.as_ref(), i));
1668            let oi = *order
1669                .get(index)
1670                .ok_or_else(|| format!("host has no wl_output {index}"))?;
1671            state.outputs[oi].clone()
1672        }
1673    };
1674    let gbm = gbm_path
1675        .as_ref()
1676        .and_then(|p| File::options().read(true).write(true).open(p).ok())
1677        .and_then(|f| GbmDevice::new(f).ok());
1678    let wake = wake_rd.as_raw_fd();
1679
1680    // Hosts speaking ext-image-copy-capture (wlroots 0.19+, KWin 6.2+, cosmic) get a
1681    // persistent session with compositor-side damage gating; everything else takes
1682    // wlr-screencopy v3 below, unchanged. PIXELFLUX_HOST_CAPTURE=zwlr forces the
1683    // fallback for triage.
1684    // The current aim outlives the ext attempt: a Start consumed while that session was
1685    // being opened still drives the wlr-screencopy fallback.
1686    let mut want: Option<Want> = None;
1687    let force = std::env::var("PIXELFLUX_HOST_CAPTURE").unwrap_or_default();
1688    if force != "zwlr" && state.ext_capture.is_some() && state.ext_source_mgr.is_some() {
1689        match capture_loop_ext(
1690            &conn, &mut queue, &mut state, &output, gbm.as_ref(), wake, index, &from_main,
1691            &frame_tx, &mut want,
1692        ) {
1693            ExtOutcome::Finished => return Ok(()),
1694            ExtOutcome::Unavailable(e) => {
1695                eprintln!("[HostCapture] output {index}: ext capture unavailable ({e}); using wlr-screencopy.");
1696            }
1697        }
1698    }
1699
1700    let screencopy = state.screencopy.clone().ok_or("no zwlr_screencopy_manager_v1")?;
1701
1702    let mut slots: Vec<Option<SlotBuffer>> = (0..SLOTS).map(|_| None).collect();
1703    let mut free: Vec<usize> = (0..SLOTS).collect();
1704    let mut generation: u64 = 0;
1705    let mut announced: Option<(i32, i32)> = None;
1706    let mut warned_mismatch = false;
1707    let mut consecutive_failures = 0u32;
1708    // Buffer type the currently allocated slots hold; None while none exist.
1709    let mut slot_zero_copy: Option<bool> = None;
1710    let mut gpu_refused = false;
1711
1712    'main: loop {
1713        // Drain control; block while idle or out of slots.
1714        loop {
1715            let blocking = want.is_none() || free.is_empty();
1716            let msg = if blocking {
1717                match from_main.recv() {
1718                    Ok(m) => m,
1719                    Err(_) => return Ok(()),
1720                }
1721            } else {
1722                match from_main.try_recv() {
1723                    Ok(m) => m,
1724                    Err(TryRecvError::Empty) => break,
1725                    Err(TryRecvError::Disconnected) => return Ok(()),
1726                }
1727            };
1728            match msg {
1729                ToHost::Release { generation: g, slot } => {
1730                    if g == generation {
1731                        free.push(slot);
1732                    }
1733                }
1734                ToHost::Idle => want = None,
1735                ToHost::Start { width, height, zero_copy, paint_cursor } => {
1736                    let next = Want { size: (width, height), zero_copy, paint_cursor };
1737                    if want != Some(next) {
1738                        warned_mismatch = false;
1739                    }
1740                    want = Some(next);
1741                }
1742            }
1743        }
1744        let (want_w, want_h, want_zero_copy, want_paint) = match want {
1745            Some(w) => (w.size.0, w.size.1, w.zero_copy, w.paint_cursor),
1746            None => continue,
1747        };
1748
1749        // One frame: negotiate, attach our buffer, wait for damage. Control
1750        // messages (resize, idle, teardown) interrupt any of the waits.
1751        state.reset_frame();
1752        let frame = screencopy.capture_output(i32::from(want_paint), &output, &qh, ());
1753        loop {
1754            match pump_until(&conn, &mut queue, &mut state, wake, None, |s| {
1755                s.buffer_done || s.failed
1756            })? {
1757                Pump::Done => break,
1758                Pump::Control => match drain_ctl(&from_main, generation, &mut free, &mut want) {
1759                    Ctl::None => {}
1760                    Ctl::Renegotiate | Ctl::Idle => {
1761                        frame.destroy();
1762                        continue 'main;
1763                    }
1764                    Ctl::Dead => {
1765                        frame.destroy();
1766                        return Ok(());
1767                    }
1768                },
1769                Pump::Timeout => {}
1770            }
1771        }
1772        if state.failed {
1773            frame.destroy();
1774            consecutive_failures += 1;
1775            if consecutive_failures == 3 {
1776                eprintln!("[HostCapture] output {index}: repeated screencopy failures; is the output alive?");
1777            }
1778            let _ = pump_until(
1779                &conn,
1780                &mut queue,
1781                &mut state,
1782                wake,
1783                Some(Duration::from_millis(100)),
1784                |_| false,
1785            );
1786            continue;
1787        }
1788
1789        let (fw, fh) = state
1790            .announce_dmabuf
1791            .map(|(_, w, h)| (w, h))
1792            .or(state.announce_shm.map(|(_, w, h, _)| (w, h)))
1793            .ok_or("screencopy announced no buffer type")?;
1794        let mut renegotiate = false;
1795        if announced != Some((fw, fh)) {
1796            announced = Some((fw, fh));
1797            eprintln!(
1798                "[HostCapture] output {index} negotiated: {fw}x{fh} gbm={} dmabuf_global={} dmabuf_announce={:?} shm={:?}",
1799                gbm.is_some(),
1800                state.dmabuf.is_some(),
1801                state.announce_dmabuf,
1802                state.announce_shm,
1803            );
1804            renegotiate = true;
1805        }
1806        if slot_zero_copy.is_some_and(|z| z != want_zero_copy) {
1807            // The consumer switched between importing dmabufs and reading pixels (an
1808            // encoder demoted, or a restart with different encode settings), so the
1809            // allocated buffers are the wrong kind.
1810            renegotiate = true;
1811        }
1812        if renegotiate {
1813            // Drop the stale buffers. The generation bump makes any release still in
1814            // flight for them a no-op.
1815            for s in slots.iter_mut() {
1816                *s = None;
1817            }
1818            free = (0..SLOTS).collect();
1819            slot_zero_copy = None;
1820            gpu_refused = false;
1821            generation += 1;
1822        }
1823        if (fw, fh) != (want_w, want_h) {
1824            // The encoder is configured for (want_w, want_h): hold frames until
1825            // the host applies the resize rather than encode mismatched buffers.
1826            frame.destroy();
1827            if !warned_mismatch {
1828                warned_mismatch = true;
1829                eprintln!(
1830                    "[HostCapture] output {index}: waiting for host {want_w}x{want_h} (currently {fw}x{fh})."
1831                );
1832            }
1833            let _ = pump_until(
1834                &conn,
1835                &mut queue,
1836                &mut state,
1837                wake,
1838                Some(Duration::from_millis(150)),
1839                |_| false,
1840            );
1841            continue;
1842        }
1843        warned_mismatch = false;
1844
1845        let slot_idx = *free.last().unwrap();
1846        if slots[slot_idx].is_none() {
1847            // Only a consumer that imports dmabufs gets GPU slots; anything reading the
1848            // pixels on the CPU takes the shm path even where the host offers dmabuf.
1849            slot_zero_copy = Some(want_zero_copy);
1850            let mut built: Option<SlotBuffer> = None;
1851            if want_zero_copy && !gpu_refused {
1852                let dmabuf_global = state.dmabuf.clone();
1853                if let (Some(dev), Some(dmabuf_global), Some((fourcc, w, h))) =
1854                    (gbm.as_ref(), dmabuf_global.as_ref(), state.announce_dmabuf)
1855                {
1856                    match alloc_gpu_slot(
1857                        &conn, &mut queue, &mut state, wake, dev, dmabuf_global, fourcc, w, h, &[],
1858                    )? {
1859                        Some(slot) => built = Some(slot),
1860                        None => {
1861                            gpu_refused = true;
1862                            eprintln!(
1863                                "[HostCapture] output {index}: host refused the dmabuf import; capturing via shm."
1864                            );
1865                        }
1866                    }
1867                }
1868            }
1869            slots[slot_idx] = Some(match built {
1870                Some(slot) => slot,
1871                None => {
1872                    let (format, w, h, stride) =
1873                        state.announce_shm.ok_or("no shm fallback announced")?;
1874                    let shm = state.shm.clone().ok_or("host compositor lacks wl_shm")?;
1875                    alloc_cpu_slot(&shm, &qh, format, w, h, stride)?
1876                }
1877            });
1878        }
1879        free.pop();
1880
1881        {
1882            let slot = slots[slot_idx].as_ref().unwrap();
1883            let wl = match slot {
1884                SlotBuffer::Gpu { wl, .. } => wl,
1885                SlotBuffer::Cpu { wl, .. } => wl,
1886            };
1887            frame.copy_with_damage(wl);
1888        }
1889        queue.flush().map_err(|e| format!("flush: {e}"))?;
1890        let mut aborted = false;
1891        loop {
1892            match pump_until(&conn, &mut queue, &mut state, wake, None, |s| s.ready || s.failed)? {
1893                Pump::Done => break,
1894                Pump::Control => match drain_ctl(&from_main, generation, &mut free, &mut want) {
1895                    Ctl::None => {}
1896                    Ctl::Renegotiate | Ctl::Idle => {
1897                        aborted = true;
1898                        break;
1899                    }
1900                    Ctl::Dead => {
1901                        frame.destroy();
1902                        return Ok(());
1903                    }
1904                },
1905                Pump::Timeout => {}
1906            }
1907        }
1908        frame.destroy();
1909        if aborted {
1910            free.push(slot_idx);
1911            continue 'main;
1912        }
1913        if state.failed {
1914            free.push(slot_idx);
1915            consecutive_failures += 1;
1916            if consecutive_failures == 3 {
1917                eprintln!("[HostCapture] output {index}: repeated screencopy failures; is the output alive?");
1918            }
1919            let _ = pump_until(
1920                &conn,
1921                &mut queue,
1922                &mut state,
1923                wake,
1924                Some(Duration::from_millis(50)),
1925                |_| false,
1926            );
1927            continue;
1928        }
1929        consecutive_failures = 0;
1930
1931        let damage = std::mem::take(&mut state.damage);
1932        let out = match slots[slot_idx].as_ref().unwrap() {
1933            SlotBuffer::Gpu { dmabuf, .. } => HostFrame {
1934                generation,
1935                slot: slot_idx,
1936                dmabuf: Some(dmabuf.clone()),
1937                cpu: None,
1938                width: fw,
1939                height: fh,
1940                damage,
1941            },
1942            SlotBuffer::Cpu { map, stride, format, .. } => HostFrame {
1943                generation,
1944                slot: slot_idx,
1945                dmabuf: None,
1946                cpu: Some(HostCpuFrame {
1947                    map: map.clone(),
1948                    stride: *stride as usize,
1949                    format: *format,
1950                }),
1951                width: fw,
1952                height: fh,
1953                damage,
1954            },
1955        };
1956        if frame_tx.send(out).is_err() {
1957            return Ok(());
1958        }
1959    }
1960}
1961
1962/// How the ext capture loop ended: `Finished` is a normal teardown (the thread is
1963/// done); `Unavailable` means it could not get going, so wlr-screencopy should run.
1964enum ExtOutcome {
1965    Finished,
1966    Unavailable(String),
1967}
1968
1969/// ext-image-copy-capture capture loop: one persistent session per output. The
1970/// compositor holds each capture until the content actually changes, so a static
1971/// screen costs nothing — the property `copy_with_damage` provides on the wlr path.
1972/// Frames land in the same slot machinery and `HostFrame` shape as that path.
1973#[allow(clippy::too_many_arguments)]
1974fn capture_loop_ext(
1975    conn: &Connection,
1976    queue: &mut EventQueue<CaptureState>,
1977    state: &mut CaptureState,
1978    output: &wl_output::WlOutput,
1979    gbm: Option<&GbmDevice<File>>,
1980    wake: RawFd,
1981    index: usize,
1982    from_main: &Receiver<ToHost>,
1983    frame_tx: &Sender<HostFrame>,
1984    want: &mut Option<Want>,
1985) -> ExtOutcome {
1986    let qh = queue.handle();
1987    let mut slots: Vec<Option<SlotBuffer>> = (0..SLOTS).map(|_| None).collect();
1988    let mut free: Vec<usize> = (0..SLOTS).collect();
1989    let mut generation: u64 = 0;
1990    let mut slot_zero_copy: Option<bool> = None;
1991    let mut gpu_refused = false;
1992    let mut seen_serial: u64 = 0;
1993    let mut warned_mismatch = false;
1994    let mut consecutive_failures = 0u32;
1995
1996    let teardown = |session: &ExtImageCopyCaptureSessionV1, source: &ExtImageCaptureSourceV1| {
1997        session.destroy();
1998        source.destroy();
1999    };
2000
2001    // The session opens with the cursor option of the first Start, which may already be
2002    // queued; before any Start the consumer's default (drawing the cursor itself) applies.
2003    let mut session_paints = false;
2004    let (mut source, mut session) = match open_ext_session(
2005        conn, queue, state, output, wake, index, from_main, session_paints, generation,
2006        &mut free, want,
2007    ) {
2008        Ok(opened) => opened,
2009        Err(outcome) => return outcome,
2010    };
2011
2012    'main: loop {
2013        // Drain control; block while idle or out of slots.
2014        loop {
2015            let blocking = want.is_none() || free.is_empty();
2016            let msg = if blocking {
2017                match from_main.recv() {
2018                    Ok(m) => m,
2019                    Err(_) => {
2020                        teardown(&session, &source);
2021                        return ExtOutcome::Finished;
2022                    }
2023                }
2024            } else {
2025                match from_main.try_recv() {
2026                    Ok(m) => m,
2027                    Err(TryRecvError::Empty) => break,
2028                    Err(TryRecvError::Disconnected) => {
2029                        teardown(&session, &source);
2030                        return ExtOutcome::Finished;
2031                    }
2032                }
2033            };
2034            match msg {
2035                ToHost::Release { generation: g, slot } => {
2036                    if g == generation {
2037                        free.push(slot);
2038                    }
2039                }
2040                ToHost::Idle => *want = None,
2041                ToHost::Start { width, height, zero_copy, paint_cursor } => {
2042                    let next = Want { size: (width, height), zero_copy, paint_cursor };
2043                    if *want != Some(next) {
2044                        warned_mismatch = false;
2045                    }
2046                    *want = Some(next);
2047                }
2048            }
2049        }
2050        let (want_w, want_h, want_zero_copy, want_paint) = match *want {
2051            Some(w) => (w.size.0, w.size.1, w.zero_copy, w.paint_cursor),
2052            None => continue,
2053        };
2054        if state.ext_stopped {
2055            // The output is going away; nothing else on this connection will revive it.
2056            teardown(&session, &source);
2057            return ExtOutcome::Finished;
2058        }
2059        if want_paint != session_paints {
2060            // The cursor option is fixed per session: reopen with the wanted one. Its
2061            // constraints arrive under a new serial, which reallocates the slots below.
2062            teardown(&session, &source);
2063            (source, session) = match open_ext_session(
2064                conn, queue, state, output, wake, index, from_main, want_paint, generation,
2065                &mut free, want,
2066            ) {
2067                Ok(opened) => opened,
2068                Err(outcome) => return outcome,
2069            };
2070            session_paints = want_paint;
2071            continue;
2072        }
2073
2074        // Fresh constraints obsolete every allocated buffer.
2075        if seen_serial != state.ext_serial || slot_zero_copy.is_some_and(|z| z != want_zero_copy) {
2076            seen_serial = state.ext_serial;
2077            for s in slots.iter_mut() {
2078                *s = None;
2079            }
2080            free = (0..SLOTS).collect();
2081            slot_zero_copy = None;
2082            gpu_refused = false;
2083            generation += 1;
2084        }
2085
2086        let Some((cw, ch)) = state.ext_size else {
2087            teardown(&session, &source);
2088            return ExtOutcome::Unavailable("constraints carried no size".into());
2089        };
2090        if (cw, ch) != (want_w, want_h) {
2091            // The encoder is configured for (want_w, want_h): hold frames until the
2092            // host applies the resize rather than capture mismatched buffers.
2093            if !warned_mismatch {
2094                warned_mismatch = true;
2095                eprintln!(
2096                    "[HostCapture] output {index}: waiting for host {want_w}x{want_h} (currently {cw}x{ch})."
2097                );
2098            }
2099            let _ = pump_until(conn, queue, state, wake, Some(Duration::from_millis(150)), |s| {
2100                s.ext_serial != seen_serial
2101            });
2102            continue;
2103        }
2104        warned_mismatch = false;
2105
2106        let slot_idx = *free.last().unwrap();
2107        if slots[slot_idx].is_none() {
2108            // Only a consumer that imports dmabufs gets GPU slots; anything reading
2109            // the pixels on the CPU takes the shm path even where the host offers
2110            // dmabuf. XR24/AR24 keep the bytes BGRA, which is what every consumer
2111            // downstream assumes.
2112            let dma_choice = [0x3432_5258u32, 0x3432_5241]
2113                .iter()
2114                .find_map(|f| state.ext_dma_formats.iter().find(|(code, _)| code == f))
2115                .cloned();
2116            slot_zero_copy = Some(want_zero_copy);
2117            let mut slot: Option<SlotBuffer> = None;
2118            if want_zero_copy && !gpu_refused {
2119                let dmabuf_global = state.dmabuf.clone();
2120                if let (Some(dev), Some(dmabuf_global), Some((fourcc, modifiers))) =
2121                    (gbm, dmabuf_global.as_ref(), dma_choice)
2122                {
2123                    match alloc_gpu_slot(
2124                        conn, queue, state, wake, dev, dmabuf_global, fourcc, cw, ch, &modifiers,
2125                    ) {
2126                        Ok(Some(built)) => slot = Some(built),
2127                        Ok(None) => {
2128                            gpu_refused = true;
2129                            eprintln!(
2130                                "[HostCapture] output {index}: host refused the dmabuf import; capturing via shm."
2131                            );
2132                        }
2133                        Err(e) => {
2134                            teardown(&session, &source);
2135                            return ExtOutcome::Unavailable(e);
2136                        }
2137                    }
2138                }
2139            }
2140            let built = match slot {
2141                Some(slot) => Ok(slot),
2142                None => {
2143                    let format = [1u32, 0]
2144                        .iter()
2145                        .find(|f| state.ext_shm_formats.contains(f))
2146                        .copied()
2147                        .or_else(|| state.ext_shm_formats.first().copied());
2148                    match (state.shm.clone(), format) {
2149                        (Some(shm), Some(format)) => {
2150                            alloc_cpu_slot(&shm, &qh, format, cw, ch, cw * 4)
2151                        }
2152                        _ => Err("host offers no usable shm format".into()),
2153                    }
2154                }
2155            };
2156            match built {
2157                Ok(slot) => slots[slot_idx] = Some(slot),
2158                Err(e) => {
2159                    teardown(&session, &source);
2160                    return ExtOutcome::Unavailable(e);
2161                }
2162            }
2163        }
2164        free.pop();
2165
2166        state.reset_frame();
2167        let frame = {
2168            let slot = slots[slot_idx].as_ref().unwrap();
2169            let wl = match slot {
2170                SlotBuffer::Gpu { wl, .. } => wl,
2171                SlotBuffer::Cpu { wl, .. } => wl,
2172            };
2173            let frame = session.create_frame(&qh, ());
2174            frame.attach_buffer(wl);
2175            // Slots rotate, so the attached buffer holds a frame from SLOTS captures
2176            // ago: everything in it is stale from the host's point of view.
2177            frame.damage_buffer(0, 0, cw, ch);
2178            frame.capture();
2179            frame
2180        };
2181        if let Err(e) = queue.flush() {
2182            frame.destroy();
2183            teardown(&session, &source);
2184            return ExtOutcome::Unavailable(format!("flush: {e}"));
2185        }
2186
2187        let mut aborted = false;
2188        loop {
2189            match pump_until(conn, queue, state, wake, None, |s| {
2190                s.ready || s.failed || s.ext_stopped || s.ext_serial != seen_serial
2191            }) {
2192                Ok(Pump::Done) => break,
2193                Ok(Pump::Control) => match drain_ctl(from_main, generation, &mut free, want) {
2194                    Ctl::None => {}
2195                    Ctl::Renegotiate | Ctl::Idle => {
2196                        aborted = true;
2197                        break;
2198                    }
2199                    Ctl::Dead => {
2200                        frame.destroy();
2201                        teardown(&session, &source);
2202                        return ExtOutcome::Finished;
2203                    }
2204                },
2205                Ok(Pump::Timeout) => {}
2206                Err(e) => {
2207                    frame.destroy();
2208                    teardown(&session, &source);
2209                    return ExtOutcome::Unavailable(e);
2210                }
2211            }
2212        }
2213        frame.destroy();
2214        if aborted || state.ext_serial != seen_serial {
2215            free.push(slot_idx);
2216            continue 'main;
2217        }
2218        if state.ext_stopped {
2219            teardown(&session, &source);
2220            return ExtOutcome::Finished;
2221        }
2222        if state.failed {
2223            free.push(slot_idx);
2224            if state.ext_fail_reason
2225                == Some(ext_image_copy_capture_frame_v1::FailureReason::BufferConstraints)
2226            {
2227                // Constraints changed under us; the serial check above rebuilds the
2228                // buffers on the next pass.
2229                continue;
2230            }
2231            consecutive_failures += 1;
2232            if consecutive_failures == 3 {
2233                eprintln!("[HostCapture] output {index}: repeated capture failures; is the output alive?");
2234            }
2235            let _ = pump_until(conn, queue, state, wake, Some(Duration::from_millis(50)), |_| false);
2236            continue;
2237        }
2238        consecutive_failures = 0;
2239
2240        let damage = std::mem::take(&mut state.damage);
2241        let out = match slots[slot_idx].as_ref().unwrap() {
2242            SlotBuffer::Gpu { dmabuf, .. } => HostFrame {
2243                generation,
2244                slot: slot_idx,
2245                dmabuf: Some(dmabuf.clone()),
2246                cpu: None,
2247                width: cw,
2248                height: ch,
2249                damage,
2250            },
2251            SlotBuffer::Cpu { map, stride, format, .. } => HostFrame {
2252                generation,
2253                slot: slot_idx,
2254                dmabuf: None,
2255                cpu: Some(HostCpuFrame {
2256                    map: map.clone(),
2257                    stride: *stride as usize,
2258                    format: *format,
2259                }),
2260                width: cw,
2261                height: ch,
2262                damage,
2263            },
2264        };
2265        if frame_tx.send(out).is_err() {
2266            teardown(&session, &source);
2267            return ExtOutcome::Finished;
2268        }
2269    }
2270}
2271
2272/// Open an ext capture session on `output`, with or without the host painting its cursor
2273/// into the frames, and wait for its first buffer constraints: the point that decides
2274/// whether this protocol works here at all. A session's cursor option is fixed at
2275/// creation, so a toggle closes the session and opens another through here.
2276#[allow(clippy::too_many_arguments)]
2277fn open_ext_session(
2278    conn: &Connection,
2279    queue: &mut EventQueue<CaptureState>,
2280    state: &mut CaptureState,
2281    output: &wl_output::WlOutput,
2282    wake: RawFd,
2283    index: usize,
2284    from_main: &Receiver<ToHost>,
2285    paint_cursor: bool,
2286    generation: u64,
2287    free: &mut Vec<usize>,
2288    want: &mut Option<Want>,
2289) -> Result<(ExtImageCaptureSourceV1, ExtImageCopyCaptureSessionV1), ExtOutcome> {
2290    let qh = queue.handle();
2291    let src_mgr = state.ext_source_mgr.clone().expect("checked by caller");
2292    let mgr = state.ext_capture.clone().expect("checked by caller");
2293    let source = src_mgr.create_source(output, &qh, ());
2294    let options = if paint_cursor {
2295        ext_image_copy_capture_manager_v1::Options::PaintCursors
2296    } else {
2297        ext_image_copy_capture_manager_v1::Options::empty()
2298    };
2299    let session = mgr.create_session(&source, options, &qh, ());
2300    let teardown = |session: &ExtImageCopyCaptureSessionV1, source: &ExtImageCaptureSourceV1| {
2301        session.destroy();
2302        source.destroy();
2303    };
2304    // A previous session's stop or half-streamed constraints must not pass for this one's.
2305    state.ext_stopped = false;
2306    state.ext_pending_size = None;
2307    state.ext_pending_shm.clear();
2308    state.ext_pending_dma.clear();
2309    let before = state.ext_serial;
2310    loop {
2311        match pump_until(conn, queue, state, wake, Some(Duration::from_secs(5)), |s| {
2312            s.ext_serial > before || s.ext_stopped
2313        }) {
2314            Ok(Pump::Done) => break,
2315            Ok(Pump::Control) => match drain_ctl(from_main, generation, free, want) {
2316                Ctl::Dead => {
2317                    teardown(&session, &source);
2318                    return Err(ExtOutcome::Finished);
2319                }
2320                _ => continue,
2321            },
2322            Ok(Pump::Timeout) => {
2323                teardown(&session, &source);
2324                return Err(ExtOutcome::Unavailable("no buffer constraints within 5s".into()));
2325            }
2326            Err(e) => {
2327                teardown(&session, &source);
2328                return Err(ExtOutcome::Unavailable(e));
2329            }
2330        }
2331    }
2332    if state.ext_stopped {
2333        teardown(&session, &source);
2334        return Err(ExtOutcome::Unavailable("session stopped before constraints".into()));
2335    }
2336    eprintln!(
2337        "[HostCapture] output {index} ext session: {:?} dma_formats={} shm_formats={} cursor={}",
2338        state.ext_size,
2339        state.ext_dma_formats.len(),
2340        state.ext_shm_formats.len(),
2341        if paint_cursor { "painted" } else { "consumer" },
2342    );
2343    Ok((source, session))
2344}
2345
2346/// Drain the control channel from inside a capture wait: releases are applied
2347/// (generation-checked), and the newest Start/Idle decides the wait's fate.
2348fn drain_ctl(
2349    rx: &Receiver<ToHost>,
2350    generation: u64,
2351    free: &mut Vec<usize>,
2352    want: &mut Option<Want>,
2353) -> Ctl {
2354    let mut out = Ctl::None;
2355    loop {
2356        match rx.try_recv() {
2357            Ok(ToHost::Release { generation: g, slot }) => {
2358                if g == generation {
2359                    free.push(slot);
2360                }
2361            }
2362            Ok(ToHost::Start { width, height, zero_copy, paint_cursor }) => {
2363                let next = Want { size: (width, height), zero_copy, paint_cursor };
2364                if *want != Some(next) {
2365                    *want = Some(next);
2366                    out = Ctl::Renegotiate;
2367                }
2368            }
2369            Ok(ToHost::Idle) => {
2370                *want = None;
2371                out = Ctl::Idle;
2372            }
2373            Err(TryRecvError::Empty) => return out,
2374            Err(TryRecvError::Disconnected) => return Ctl::Dead,
2375        }
2376    }
2377}
2378
2379#[cfg(test)]
2380mod tests {
2381    use super::*;
2382
2383    /// The shm-format table maps each announced format to the right BGRA conversion. The
2384    /// wl_shm definitions are little-endian, so `Bgr888` (`[23:0] B:G:R`) is red-first in memory and
2385    /// must swap R/B, while `Rgb888` (`[23:0] R:G:B`) is already blue-first and copies straight.
2386    #[test]
2387    fn shm_layout_matches_wl_shm_definitions() {
2388        assert_eq!(shm_src_layout(wl_shm::Format::Bgr888 as u32), (3, true));
2389        assert_eq!(shm_src_layout(wl_shm::Format::Rgb888 as u32), (3, false));
2390        assert_eq!(shm_src_layout(wl_shm::Format::Xbgr8888 as u32), (4, true));
2391        assert_eq!(shm_src_layout(wl_shm::Format::Abgr8888 as u32), (4, true));
2392        assert_eq!(shm_src_layout(wl_shm::Format::Xrgb8888 as u32), (4, false));
2393        assert_eq!(shm_src_layout(wl_shm::Format::Argb8888 as u32), (4, false));
2394    }
2395
2396    /// A known pixel round-trips to BGRA. The logical colour is R=0x11, G=0x22, B=0x33; in a
2397    /// `Bgr888` buffer its little-endian bytes are R,G,B (0x11,0x22,0x33) and the destination must be
2398    /// B,G,R,A (0x33,0x22,0x11,0xff). Without the R/B swap this path produced 0x11,0x22,0x33 —
2399    /// red and blue transposed.
2400    #[test]
2401    fn bgr888_known_pixel_swaps_red_and_blue() {
2402        let (bpp, swap) = shm_src_layout(wl_shm::Format::Bgr888 as u32);
2403        let src = [0x11u8, 0x22, 0x33];
2404        let mut dst = [0u8; 4];
2405        convert_shm_row(&src, &mut dst, bpp, swap);
2406        assert_eq!(dst, [0x33, 0x22, 0x11, 0xff]);
2407    }
2408
2409    /// The same logical colour in an `Rgb888` buffer is already B,G,R in memory
2410    /// (0x33,0x22,0x11), so it copies straight to B,G,R,A with an opaque alpha and no swap.
2411    #[test]
2412    fn rgb888_known_pixel_copies_straight() {
2413        let (bpp, swap) = shm_src_layout(wl_shm::Format::Rgb888 as u32);
2414        let src = [0x33u8, 0x22, 0x11];
2415        let mut dst = [0u8; 4];
2416        convert_shm_row(&src, &mut dst, bpp, swap);
2417        assert_eq!(dst, [0x33, 0x22, 0x11, 0xff]);
2418    }
2419
2420    /// The 32-bit `Xbgr8888` source (R,G,B,x in memory) swaps R/B and preserves the fourth
2421    /// byte, and `Xrgb8888` (B,G,R,x) copies straight.
2422    fn convert(format: wl_shm::Format, src: &[u8]) -> [u8; 4] {
2423        let (bpp, swap) = shm_src_layout(format as u32);
2424        let mut dst = [0u8; 4];
2425        convert_shm_row(src, &mut dst, bpp, swap);
2426        dst
2427    }
2428
2429    #[test]
2430    fn four_byte_formats_convert_to_bgra() {
2431        assert_eq!(convert(wl_shm::Format::Xbgr8888, &[0x11, 0x22, 0x33, 0x44]), [0x33, 0x22, 0x11, 0x44]);
2432        assert_eq!(convert(wl_shm::Format::Xrgb8888, &[0x33, 0x22, 0x11, 0x44]), [0x33, 0x22, 0x11, 0x44]);
2433    }
2434
2435    /// The layout ledger the calloop polls: a request is unanswered until the control
2436    /// thread decides an epoch at or past it, the newest verdict stands for every
2437    /// older request (the layout map is cumulative), and a stale verdict arriving
2438    /// after a newer one cannot overwrite it.
2439    #[test]
2440    fn layout_ledger_answers_by_epoch() {
2441        let mut ledger = LayoutLedger::default();
2442        let first = ledger.issue();
2443        let second = ledger.issue();
2444        assert_eq!((first, second), (1, 2));
2445        assert_eq!(ledger.outcome(first), None);
2446        assert_eq!(ledger.outcome(second), None);
2447
2448        ledger.decide(first, true);
2449        assert_eq!(ledger.outcome(first), Some(true));
2450        assert_eq!(ledger.outcome(second), None);
2451
2452        // The control thread coalesces queued requests and answers only the newest;
2453        // that verdict covers the older epoch too.
2454        let third = ledger.issue();
2455        ledger.decide(third, false);
2456        assert_eq!(ledger.outcome(second), Some(false));
2457        assert_eq!(ledger.outcome(third), Some(false));
2458
2459        // A late verdict for an older epoch does not roll the ledger back.
2460        ledger.decide(second, true);
2461        assert_eq!(ledger.outcome(third), Some(false));
2462        let fourth = ledger.issue();
2463        assert_eq!(ledger.outcome(fourth), None);
2464    }
2465}