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); input over libei where
10//! the backend grants an EIS socket, else a persistent `zwp_virtual_keyboard_v1`
11//! device carrying selkies' own keymap text with `zwlr_virtual_pointer_v1`, else
12//! kernel uinput devices where `/dev/uinput` is writable. Zero-copy is preserved by
13//! allocating the capture buffers from pixelflux's OWN GBM device (render node —
14//! no privileges): the compositor blits straight into the dmabufs the encoder
15//! imports, so no CPU ever touches a frame. A host that cannot import our
16//! dmabufs (other GPU, software renderer) and any run without a GPU degrade to
17//! wl_shm buffers feeding the CPU encode pool.
18//!
19//! Every host `wl_output` maps to one selkies display id, in registry order: a
20//! control thread on the primary connection owns wlr-output-management and
21//! applies the whole layout (per-output custom modes plus row positions) as one
22//! atomic configuration, while each output gets its own capture connection and
23//! thread. Input proxies are called from the calloop thread directly
24//! (wayland-client proxies are thread-safe); pointer coordinates arrive in the
25//! same union-layout space selkies uses, so the virtual-pointer extent is the
26//! layout's bounding box. All capture-side waits poll a wake pipe next to the
27//! Wayland socket, so a resize or teardown lands immediately even while the
28//! thread is parked in a `copy_with_damage` wait on a static screen.
29//!
30//! A host without those protocols (GNOME, KDE before ext-image-copy-capture) is captured
31//! through xdg-desktop-portal instead: one RemoteDesktop session hands out a PipeWire stream per
32//! monitor and takes the input the seat lacks a virtual device for (`portal`, `pwcapture`).
33//! Each rung is chosen per capability, ordered by measured delivery cost: a socket write to the
34//! compositor (libei, then the virtual-input protocols) runs about 4 us, a uinput write reaching
35//! evdev about 12 us, and a portal `Notify*` D-Bus call about 2 ms, so the kernel serves as the
36//! universal fallback rather than the first choice, and the portal comes last. A uinput pointer
37//! declares a normalized absolute axis, since the layout extent changes while a device's range
38//! cannot. Frames take the Wayland protocol where the host offers it and the portal where it does
39//! not — so a
40//! KWin that serves ext-image-copy-capture but no virtual pointer still gets its pointer from
41//! the portal. Portal keys go by keysym, resolved from selkies' own keymap, so the host applies
42//! its layout to the symbol rather than to a keycode from another layout. The compositor owns
43//! the portal buffers: a dmabuf frame is imported where it lies and a memfd frame is read in
44//! place, and the portal stream is asked for at most the capture's rate so no frame is
45//! produced only to be dropped.
46
47use std::fs::File;
48use std::os::fd::{AsFd, AsRawFd, OwnedFd, RawFd};
49use std::os::unix::net::UnixStream;
50use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
51use std::sync::mpsc::{Receiver, Sender, TryRecvError};
52use std::sync::{Arc, Condvar, Mutex};
53use std::time::{Duration, Instant};
54
55use gbm::{Device as GbmDevice, Format as GbmFormat};
56use smithay::backend::allocator::Buffer as _;
57use smithay::backend::allocator::dmabuf::Dmabuf;
58use smithay::input::keyboard::xkb;
59use smithay::utils::{Physical, Rectangle};
60use wayland_client::protocol::{wl_output, wl_pointer, wl_registry, wl_seat, wl_shm, wl_shm_pool};
61use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum, delegate_noop};
62use wayland_protocols::ext::image_capture_source::v1::client::{
63    ext_image_capture_source_v1::ExtImageCaptureSourceV1,
64    ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1,
65};
66use wayland_protocols::ext::image_copy_capture::v1::client::{
67    ext_image_copy_capture_frame_v1::{self, ExtImageCopyCaptureFrameV1},
68    ext_image_copy_capture_manager_v1::{self, ExtImageCopyCaptureManagerV1},
69    ext_image_copy_capture_session_v1::{self, ExtImageCopyCaptureSessionV1},
70};
71use wayland_protocols::wp::linux_dmabuf::zv1::client::{
72    zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1},
73    zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1,
74};
75use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
76    zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1,
77    zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1,
78};
79use wayland_protocols_wlr::output_management::v1::client::{
80    zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1,
81    zwlr_output_configuration_v1::{self, ZwlrOutputConfigurationV1},
82    zwlr_output_head_v1::{self, ZwlrOutputHeadV1},
83    zwlr_output_manager_v1::{self, ZwlrOutputManagerV1},
84    zwlr_output_mode_v1::ZwlrOutputModeV1,
85};
86use wayland_protocols_wlr::screencopy::v1::client::{
87    zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1},
88    zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
89};
90use wayland_protocols_wlr::virtual_pointer::v1::client::{
91    zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
92    zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
93};
94
95use crate::wayland::cursor::CursorJob;
96use crate::wayland::eiclient::EiInjector;
97use crate::wayland::portal::{self, PortalSession, PortalStream};
98use crate::wayland::pwcapture::{PwCore, PwStream, StreamConfig};
99use crate::wayland::wlclient::{
100    SyncState, bounded_roundtrip, drain_pipe, impl_sync_callback, memfd_with, socket_path,
101    wait_readable2, wake_pipe, wake_write,
102};
103
104const KEYMAP_FORMAT_XKB_V1: u32 = 1;
105/// Frames in flight between a capture thread and the encode tick. Two slots:
106/// one being blitted by the compositor while the previous one is encoded.
107const SLOTS: usize = 2;
108
109/// A frame the compositor finished blitting, ready for the encoder.
110pub struct HostFrame {
111    pub(crate) generation: u64,
112    pub(crate) slot: usize,
113    /// GPU path: the filled dmabuf (Arc-backed; the buffer itself stays owned by
114    /// the capture thread and is reused once the slot is released).
115    pub dmabuf: Option<Dmabuf>,
116    /// Software path: the shm mapping itself — the consumer converts straight
117    /// out of it (one copy total); the slot is not reused until release.
118    pub cpu: Option<HostCpuFrame>,
119    pub width: i32,
120    pub height: i32,
121    pub damage: Vec<Rectangle<i32, Physical>>,
122    /// The compositor's CLOCK_MONOTONIC stamp for the frame, or its arrival when the host gives none.
123    pub stamp_ns: i64,
124}
125
126/// CLOCK_MONOTONIC in nanoseconds, the clock PipeWire stamps frames with.
127pub(crate) fn now_ns() -> i64 {
128    let mut t = libc::timespec {
129        tv_sec: 0,
130        tv_nsec: 0,
131    };
132    unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut t) };
133    t.tv_sec * 1_000_000_000 + t.tv_nsec
134}
135
136/// `PIXELFLUX_HOST_TRACE` set: log every host frame's age on arrival and once encoded.
137pub(crate) fn trace() -> bool {
138    static ON: std::sync::LazyLock<bool> =
139        std::sync::LazyLock::new(|| std::env::var_os("PIXELFLUX_HOST_TRACE").is_some());
140    *ON
141}
142
143/// Borrow-by-Arc view of a software frame still sitting in its shm mapping.
144pub struct HostCpuFrame {
145    pub(crate) map: Arc<dyn AsRef<[u8]> + Send + Sync>,
146    /// First byte of the frame within the mapping.
147    pub(crate) offset: usize,
148    pub(crate) stride: usize,
149    pub(crate) format: u32,
150}
151
152/// Source bytes-per-pixel and whether R and B swap to reach BGRA, for one announced wl_shm
153/// format. The consumers downstream all read BGRA (byte order B,G,R,A), so a format is mapped by
154/// where its red and blue land in memory under the little-endian byte order wayland.xml specifies:
155///
156/// - `Xrgb8888` / `Argb8888` (`[31:0] x:R:G:B` LE) are B,G,R,x in memory: already BGRA, no swap.
157/// - `Xbgr8888` / `Abgr8888` (`[31:0] x:B:G:R` LE) are R,G,B,x in memory: swap R/B.
158/// - `Bgr888` (`[23:0] B:G:R` LE) is R,G,B in memory: swap R/B (the 24-bit NVIDIA-GLES read path).
159/// - `Rgb888` (`[23:0] R:G:B` LE) is B,G,R in memory: already BGR order, no swap.
160///
161/// The empirical NVIDIA-GLES host could not be exercised here, so `Bgr888` follows the wl_shm
162/// definition literally: `[23:0] B:G:R little endian` places R in the low byte, so byte 0 is red and
163/// reaching BGRA swaps R and B — the opposite of a straight copy.
164fn shm_src_layout(format: u32) -> (usize, bool) {
165    match format {
166        f if f == wl_shm::Format::Xbgr8888 as u32 || f == wl_shm::Format::Abgr8888 as u32 => {
167            (4, true)
168        }
169        f if f == wl_shm::Format::Bgr888 as u32 => (3, true),
170        f if f == wl_shm::Format::Rgb888 as u32 => (3, false),
171        _ => (4, false),
172    }
173}
174
175/// Convert one source row to a BGRA destination row per `(src_bpp, swap_rb)` from
176/// `shm_src_layout`: a 4-byte source either copies straight or swaps R/B keeping alpha, a 3-byte
177/// source fills opaque alpha and swaps R/B only when the layout calls for it.
178fn convert_shm_row(src: &[u8], dst: &mut [u8], src_bpp: usize, swap_rb: bool) {
179    match (src_bpp, swap_rb) {
180        (4, false) => {
181            let n = dst.len().min(src.len());
182            dst[..n].copy_from_slice(&src[..n]);
183        }
184        (4, true) => {
185            for (d, s) in dst
186                .as_chunks_mut::<4>()
187                .0
188                .iter_mut()
189                .zip(src.as_chunks::<4>().0)
190            {
191                d[0] = s[2];
192                d[1] = s[1];
193                d[2] = s[0];
194                d[3] = s[3];
195            }
196        }
197        // One 4-byte store per pixel rather than four byte stores: measured ~1.7x on a
198        // 3-byte source. The 4-byte arms above vectorize better as they stand.
199        (_, true) => {
200            for (d, s) in dst
201                .as_chunks_mut::<4>()
202                .0
203                .iter_mut()
204                .zip(src.as_chunks::<3>().0)
205            {
206                d.copy_from_slice(&[s[2], s[1], s[0], 0xff]);
207            }
208        }
209        (_, false) => {
210            for (d, s) in dst
211                .as_chunks_mut::<4>()
212                .0
213                .iter_mut()
214                .zip(src.as_chunks::<3>().0)
215            {
216                d.copy_from_slice(&[s[0], s[1], s[2], 0xff]);
217            }
218        }
219    }
220}
221
222impl HostCpuFrame {
223    /// Convert the frame into tight BGRA rows in `dst` (sized `w*4*h`); the announced shm
224    /// format decides the per-pixel conversion (compositors pick their renderer's preferred read
225    /// format, e.g. 24-bit BGR on NVIDIA GLES).
226    pub fn write_bgra(&self, w: i32, h: i32, dst: &mut [u8]) {
227        let row = (w * 4) as usize;
228        let (src_bpp, swap_rb) = shm_src_layout(self.format);
229        let bytes: &[u8] = (*self.map).as_ref();
230        let bytes = &bytes[self.offset.min(bytes.len())..];
231        for y in 0..h as usize {
232            let src_start = y * self.stride;
233            let src_end = (src_start + src_bpp * w as usize).min(bytes.len());
234            if src_start >= src_end || (y + 1) * row > dst.len() {
235                break;
236            }
237            let src_row = &bytes[src_start..src_end];
238            let dst_row = &mut dst[y * row..(y + 1) * row];
239            convert_shm_row(src_row, dst_row, src_bpp, swap_rb);
240        }
241    }
242}
243
244/// One display's slice of the union layout, as selkies configured it. Slots are
245/// keyed by selkies display id ('display2' is id 2 — ids are sparse); active ids
246/// map onto host outputs by ascending rank.
247#[derive(Clone, Copy, Default)]
248struct LayoutSlot {
249    want: (i32, i32),
250    pos: (i32, i32),
251    active: bool,
252    /// The consumer imports dmabufs directly, so the compositor blits into GPU
253    /// buffers. A CPU encode path clears this and takes shm frames instead.
254    zero_copy: bool,
255    /// The host paints its cursor into the frames (the consumer's native cursor
256    /// rendering); off, the consumer draws the cursor itself from the sprite callback.
257    paint_cursor: bool,
258    /// The capture's rate in millihertz, the most a portal stream is asked to deliver.
259    fps_milli: u32,
260}
261
262/// What a capture thread is currently aiming at: the encoder's dimensions, the buffer
263/// type its consumer can take, and whether the host paints the cursor in. A size or
264/// buffer-type change forces a slot renegotiation; a cursor change reopens the ext
265/// session, whose option is fixed at creation.
266#[derive(Clone, Copy, PartialEq, Eq)]
267struct Want {
268    size: (i32, i32),
269    zero_copy: bool,
270    paint_cursor: bool,
271    fps_milli: u32,
272}
273
274enum ToHost {
275    Start {
276        width: i32,
277        height: i32,
278        zero_copy: bool,
279        paint_cursor: bool,
280        fps_milli: u32,
281    },
282    /// Slot return; `generation` guards against slots recycled by a renegotiation
283    /// while the consumer still held the frame.
284    Release { generation: u64, slot: usize },
285    /// Park the capture (display stopped) without ending the thread.
286    Idle,
287}
288
289enum CtrlMsg {
290    Apply { epoch: u64, slots: Vec<LayoutSlot> },
291}
292
293/// The capture thread's end of one output's frame queue: the frames, an exact count of those
294/// queued and not yet taken (so a wake that the timer has already served is recognized as
295/// such), and the calloop wake that follows every frame.
296#[derive(Clone)]
297pub(crate) struct FrameSink {
298    pub(crate) tx: Sender<HostFrame>,
299    pub(crate) queued: Arc<AtomicI64>,
300    pub(crate) wake: smithay::reexports::calloop::channel::Sender<usize>,
301    pub(crate) index: usize,
302}
303
304impl FrameSink {
305    /// Queue `frame` and wake the consumer; false once the consumer is gone.
306    pub(crate) fn send(&self, frame: HostFrame) -> bool {
307        if self.tx.send(frame).is_err() {
308            return false;
309        }
310        self.queued.fetch_add(1, Ordering::AcqRel);
311        let _ = self.wake.send(self.index);
312        true
313    }
314}
315
316/// Outcome ledger for layout requests: every Apply carries an epoch, and the
317/// control thread records whether the newest one it processed was realized by
318/// the host. A request is answered once `decided` has caught up to its epoch;
319/// a newer request subsumes an older one (the layout map is cumulative), so
320/// the newest verdict stands for every epoch at or below it. Polled, never
321/// waited on: the session side reads it from its own loop.
322#[derive(Default)]
323struct LayoutLedger {
324    epoch: u64,
325    decided: u64,
326    realized: bool,
327}
328
329impl LayoutLedger {
330    /// Number the next request.
331    fn issue(&mut self) -> u64 {
332        self.epoch += 1;
333        self.epoch
334    }
335
336    /// Record the host's verdict on `epoch` (ignored if a newer one is already in).
337    fn decide(&mut self, epoch: u64, realized: bool) {
338        if epoch > self.decided {
339            self.decided = epoch;
340            self.realized = realized;
341        }
342    }
343
344    /// The verdict covering `epoch`: `None` while the host has not answered it yet.
345    fn outcome(&self, epoch: u64) -> Option<bool> {
346        (self.decided >= epoch).then_some(self.realized)
347    }
348}
349
350/// Bound for one layout application on the control thread: a host that has not answered by
351/// then is settled without it, and the session follows whatever mode the host announces.
352pub const LAYOUT_DEADLINE: Duration = Duration::from_secs(5);
353
354// ---------------------------------------------------------------------------
355// Control connection: seat + virtual input devices + wlr-output-management.
356// ---------------------------------------------------------------------------
357
358#[derive(Default)]
359struct CtrlState {
360    seat: Option<wl_seat::WlSeat>,
361    vk_mgr: Option<ZwpVirtualKeyboardManagerV1>,
362    vptr_mgr: Option<ZwlrVirtualPointerManagerV1>,
363    has_screencopy: bool,
364    has_ext_capture: bool,
365    has_ext_source: bool,
366    outputs: Vec<(wl_output::WlOutput, Option<String>)>,
367    /// Current mode per output, registry-indexed and shared with the session:
368    /// when the host keeps its own size (refused or unmanageable layout), the
369    /// capture follows this instead of gating on the size it asked for.
370    sizes: Arc<Mutex<Vec<(i32, i32)>>>,
371    /// Layout position and integer scale per output, registry-indexed.
372    positions: Vec<(i32, i32)>,
373    scales: Vec<i32>,
374    /// Registry indices in display order (natural name sort — registry
375    /// announcement order is not guaranteed to match output creation order).
376    order: Vec<usize>,
377    output_mgr: Option<ZwlrOutputManagerV1>,
378    heads: Vec<(ZwlrOutputHeadV1, Option<String>)>,
379    om_serial: Option<u32>,
380    cfg_result: Option<bool>,
381    cfg_canceled: bool,
382    sync_done: bool,
383}
384
385/// Natural sort key for an output name: text prefix plus trailing number, so
386/// HEADLESS-2 orders before HEADLESS-10. Unnamed outputs keep registry order
387/// after every named one.
388fn output_order_key(name: Option<&String>, registry_idx: usize) -> (bool, String, u64, usize) {
389    match name {
390        Some(n) => {
391            let digits_at = n
392                .rfind(|c: char| !c.is_ascii_digit())
393                .map(|i| i + 1)
394                .unwrap_or(0);
395            let num = n[digits_at..].parse::<u64>().unwrap_or(0);
396            (false, n[..digits_at].to_string(), num, registry_idx)
397        }
398        None => (true, String::new(), 0, registry_idx),
399    }
400}
401
402impl SyncState for CtrlState {
403    fn sync_done_mut(&mut self) -> &mut bool {
404        &mut self.sync_done
405    }
406}
407impl_sync_callback!(CtrlState);
408
409impl Dispatch<wl_registry::WlRegistry, ()> for CtrlState {
410    fn event(
411        state: &mut Self,
412        registry: &wl_registry::WlRegistry,
413        event: wl_registry::Event,
414        _: &(),
415        _: &Connection,
416        qh: &QueueHandle<Self>,
417    ) {
418        if let wl_registry::Event::Global {
419            name,
420            interface,
421            version,
422        } = event
423        {
424            match interface.as_str() {
425                "wl_seat" if state.seat.is_none() => {
426                    state.seat = Some(registry.bind(name, 1, qh, ()))
427                }
428                "zwp_virtual_keyboard_manager_v1" => {
429                    state.vk_mgr = Some(registry.bind(name, 1, qh, ()))
430                }
431                "zwlr_virtual_pointer_manager_v1" => {
432                    state.vptr_mgr = Some(registry.bind(name, version.min(2), qh, ()))
433                }
434                "zwlr_screencopy_manager_v1" if version >= 3 => state.has_screencopy = true,
435                "ext_image_copy_capture_manager_v1" => state.has_ext_capture = true,
436                "ext_output_image_capture_source_manager_v1" => state.has_ext_source = true,
437                "wl_output" => {
438                    let idx = state.outputs.len();
439                    let out = registry.bind(name, version.min(4), qh, idx);
440                    state.outputs.push((out, None));
441                }
442                "zwlr_output_manager_v1" => state.output_mgr = Some(registry.bind(name, 1, qh, ())),
443                _ => {}
444            }
445        }
446    }
447}
448
449macro_rules! impl_output_name {
450    ($t:ty) => {
451        impl Dispatch<wl_output::WlOutput, usize> for $t {
452            fn event(
453                state: &mut Self,
454                _: &wl_output::WlOutput,
455                event: wl_output::Event,
456                idx: &usize,
457                _: &Connection,
458                _: &QueueHandle<Self>,
459            ) {
460                if let wl_output::Event::Name { name } = event {
461                    if let Some(o) = state.outputs.get_mut(*idx) {
462                        o.1 = Some(name);
463                    }
464                }
465            }
466        }
467    };
468}
469impl Dispatch<wl_output::WlOutput, usize> for CtrlState {
470    fn event(
471        state: &mut Self,
472        _: &wl_output::WlOutput,
473        event: wl_output::Event,
474        idx: &usize,
475        _: &Connection,
476        _: &QueueHandle<Self>,
477    ) {
478        match event {
479            wl_output::Event::Name { name } => {
480                if let Some(o) = state.outputs.get_mut(*idx) {
481                    o.1 = Some(name);
482                }
483            }
484            wl_output::Event::Mode {
485                flags,
486                width,
487                height,
488                ..
489            } if flags
490                .into_result()
491                .is_ok_and(|f| f.contains(wl_output::Mode::Current)) =>
492            {
493                let mut sizes = state.sizes.lock().unwrap();
494                if sizes.len() <= *idx {
495                    sizes.resize(*idx + 1, (0, 0));
496                }
497                sizes[*idx] = (width, height);
498            }
499            wl_output::Event::Geometry { x, y, .. } => {
500                if state.positions.len() <= *idx {
501                    state.positions.resize(*idx + 1, (0, 0));
502                }
503                state.positions[*idx] = (x, y);
504            }
505            wl_output::Event::Scale { factor } => {
506                if state.scales.len() <= *idx {
507                    state.scales.resize(*idx + 1, 1);
508                }
509                state.scales[*idx] = factor.max(1);
510            }
511            _ => {}
512        }
513    }
514}
515
516delegate_noop!(CtrlState: ignore wl_seat::WlSeat);
517delegate_noop!(CtrlState: ZwpVirtualKeyboardManagerV1);
518delegate_noop!(CtrlState: ZwpVirtualKeyboardV1);
519delegate_noop!(CtrlState: ZwlrVirtualPointerManagerV1);
520delegate_noop!(CtrlState: ZwlrVirtualPointerV1);
521delegate_noop!(CtrlState: ignore ZwlrOutputModeV1);
522delegate_noop!(CtrlState: ZwlrOutputConfigurationHeadV1);
523
524impl Dispatch<ZwlrOutputManagerV1, ()> for CtrlState {
525    fn event(
526        state: &mut Self,
527        _: &ZwlrOutputManagerV1,
528        event: zwlr_output_manager_v1::Event,
529        _: &(),
530        _: &Connection,
531        _: &QueueHandle<Self>,
532    ) {
533        match event {
534            zwlr_output_manager_v1::Event::Head { head } => state.heads.push((head, None)),
535            zwlr_output_manager_v1::Event::Done { serial } => state.om_serial = Some(serial),
536            _ => {}
537        }
538    }
539
540    wayland_client::event_created_child!(CtrlState, ZwlrOutputManagerV1, [
541        zwlr_output_manager_v1::EVT_HEAD_OPCODE => (ZwlrOutputHeadV1, ()),
542    ]);
543}
544
545impl Dispatch<ZwlrOutputHeadV1, ()> for CtrlState {
546    fn event(
547        state: &mut Self,
548        head: &ZwlrOutputHeadV1,
549        event: zwlr_output_head_v1::Event,
550        _: &(),
551        _: &Connection,
552        _: &QueueHandle<Self>,
553    ) {
554        match event {
555            zwlr_output_head_v1::Event::Name { name } => {
556                if let Some(h) = state.heads.iter_mut().find(|(h, _)| h.id() == head.id()) {
557                    h.1 = Some(name);
558                }
559            }
560            zwlr_output_head_v1::Event::Finished => {
561                state.heads.retain(|(h, _)| h.id() != head.id());
562            }
563            _ => {}
564        }
565    }
566
567    wayland_client::event_created_child!(CtrlState, ZwlrOutputHeadV1, [
568        zwlr_output_head_v1::EVT_MODE_OPCODE => (ZwlrOutputModeV1, ()),
569    ]);
570}
571
572impl Dispatch<ZwlrOutputConfigurationV1, ()> for CtrlState {
573    fn event(
574        state: &mut Self,
575        _: &ZwlrOutputConfigurationV1,
576        event: zwlr_output_configuration_v1::Event,
577        _: &(),
578        _: &Connection,
579        _: &QueueHandle<Self>,
580    ) {
581        match event {
582            zwlr_output_configuration_v1::Event::Succeeded => state.cfg_result = Some(true),
583            zwlr_output_configuration_v1::Event::Failed => state.cfg_result = Some(false),
584            zwlr_output_configuration_v1::Event::Cancelled => state.cfg_canceled = true,
585            _ => {}
586        }
587    }
588}
589
590// ---------------------------------------------------------------------------
591// Capture connections: one per host output (screencopy + buffer allocation).
592// ---------------------------------------------------------------------------
593
594#[derive(Default)]
595struct CaptureState {
596    shm: Option<wl_shm::WlShm>,
597    dmabuf: Option<ZwpLinuxDmabufV1>,
598    screencopy: Option<ZwlrScreencopyManagerV1>,
599    ext_capture: Option<ExtImageCopyCaptureManagerV1>,
600    ext_source_mgr: Option<ExtOutputImageCaptureSourceManagerV1>,
601    outputs: Vec<(wl_output::WlOutput, Option<String>)>,
602    // Per-frame capture negotiation/results: the buffer types screencopy offered, as
603    // (fourcc, width, height) for dmabuf and (format, width, height, stride) for shm.
604    announce_dmabuf: Option<(u32, i32, i32)>,
605    announce_shm: Option<(u32, i32, i32, i32)>,
606    buffer_done: bool,
607    damage: Vec<Rectangle<i32, Physical>>,
608    ready: bool,
609    failed: bool,
610    sync_done: bool,
611    // Non-immed zwp_linux_buffer_params result: the host either delivered a
612    // wl_buffer for our dmabuf or refused the import.
613    params_created: Option<wayland_client::protocol::wl_buffer::WlBuffer>,
614    params_failed: bool,
615    // ext session constraints stream in flight; `done` seals it into the committed
616    // set and bumps the serial (a new set arrives whenever the output changes).
617    ext_pending_size: Option<(i32, i32)>,
618    ext_pending_shm: Vec<u32>,
619    ext_pending_dma: Vec<(u32, Vec<u64>)>,
620    ext_size: Option<(i32, i32)>,
621    ext_shm_formats: Vec<u32>,
622    ext_dma_formats: Vec<(u32, Vec<u64>)>,
623    ext_serial: u64,
624    ext_stopped: bool,
625    ext_fail_reason: Option<ext_image_copy_capture_frame_v1::FailureReason>,
626}
627
628impl CaptureState {
629    fn reset_frame(&mut self) {
630        self.announce_dmabuf = None;
631        self.announce_shm = None;
632        self.buffer_done = false;
633        self.damage.clear();
634        self.ready = false;
635        self.failed = false;
636        self.ext_fail_reason = None;
637    }
638}
639
640impl SyncState for CaptureState {
641    fn sync_done_mut(&mut self) -> &mut bool {
642        &mut self.sync_done
643    }
644}
645impl_sync_callback!(CaptureState);
646impl_output_name!(CaptureState);
647
648impl Dispatch<wl_registry::WlRegistry, ()> for CaptureState {
649    fn event(
650        state: &mut Self,
651        registry: &wl_registry::WlRegistry,
652        event: wl_registry::Event,
653        _: &(),
654        _: &Connection,
655        qh: &QueueHandle<Self>,
656    ) {
657        if let wl_registry::Event::Global {
658            name,
659            interface,
660            version,
661        } = event
662        {
663            match interface.as_str() {
664                "wl_shm" => state.shm = Some(registry.bind(name, 1, qh, ())),
665                "zwp_linux_dmabuf_v1" if version >= 3 => {
666                    state.dmabuf = Some(registry.bind(name, 3, qh, ()))
667                }
668                "zwlr_screencopy_manager_v1" if version >= 3 => {
669                    state.screencopy = Some(registry.bind(name, 3, qh, ()))
670                }
671                "ext_image_copy_capture_manager_v1" => {
672                    state.ext_capture = Some(registry.bind(name, 1, qh, ()))
673                }
674                "ext_output_image_capture_source_manager_v1" => {
675                    state.ext_source_mgr = Some(registry.bind(name, 1, qh, ()))
676                }
677                "wl_output" => {
678                    let idx = state.outputs.len();
679                    let out = registry.bind(name, version.min(4), qh, idx);
680                    state.outputs.push((out, None));
681                }
682                _ => {}
683            }
684        }
685    }
686}
687
688delegate_noop!(CaptureState: ignore wl_shm::WlShm);
689delegate_noop!(CaptureState: ignore wl_shm_pool::WlShmPool);
690delegate_noop!(CaptureState: ignore wayland_client::protocol::wl_buffer::WlBuffer);
691delegate_noop!(CaptureState: ZwlrScreencopyManagerV1);
692delegate_noop!(CaptureState: ExtImageCopyCaptureManagerV1);
693delegate_noop!(CaptureState: ExtOutputImageCaptureSourceManagerV1);
694delegate_noop!(CaptureState: ExtImageCaptureSourceV1);
695
696impl Dispatch<ExtImageCopyCaptureSessionV1, ()> for CaptureState {
697    fn event(
698        state: &mut Self,
699        _: &ExtImageCopyCaptureSessionV1,
700        event: ext_image_copy_capture_session_v1::Event,
701        _: &(),
702        _: &Connection,
703        _: &QueueHandle<Self>,
704    ) {
705        use ext_image_copy_capture_session_v1::Event;
706        match event {
707            Event::BufferSize { width, height } => {
708                state.ext_pending_size = Some((width as i32, height as i32));
709            }
710            Event::ShmFormat { format } => {
711                let raw = match format {
712                    WEnum::Value(f) => f as u32,
713                    WEnum::Unknown(u) => u,
714                };
715                state.ext_pending_shm.push(raw);
716            }
717            Event::DmabufDevice { .. } => {
718                // Buffers are allocated from pixelflux's own render node, like the
719                // wlr-screencopy path; a cross-device host fails the dmabuf import
720                // and the shm formats below still apply.
721            }
722            Event::DmabufFormat { format, modifiers } => {
723                let mods = modifiers
724                    .as_chunks::<8>()
725                    .0
726                    .iter()
727                    .map(|c| u64::from_ne_bytes(*c))
728                    .collect();
729                state.ext_pending_dma.push((format, mods));
730            }
731            Event::Done => {
732                state.ext_size = state.ext_pending_size.take();
733                state.ext_shm_formats = std::mem::take(&mut state.ext_pending_shm);
734                state.ext_dma_formats = std::mem::take(&mut state.ext_pending_dma);
735                state.ext_serial += 1;
736            }
737            Event::Stopped => state.ext_stopped = true,
738            _ => {}
739        }
740    }
741}
742
743impl Dispatch<ExtImageCopyCaptureFrameV1, ()> for CaptureState {
744    fn event(
745        state: &mut Self,
746        _: &ExtImageCopyCaptureFrameV1,
747        event: ext_image_copy_capture_frame_v1::Event,
748        _: &(),
749        _: &Connection,
750        _: &QueueHandle<Self>,
751    ) {
752        use ext_image_copy_capture_frame_v1::Event;
753        match event {
754            Event::Damage {
755                x,
756                y,
757                width,
758                height,
759            } => {
760                state
761                    .damage
762                    .push(Rectangle::new((x, y).into(), (width, height).into()));
763            }
764            Event::Ready => state.ready = true,
765            Event::Failed { reason } => {
766                state.failed = true;
767                if let WEnum::Value(r) = reason {
768                    state.ext_fail_reason = Some(r);
769                }
770            }
771            _ => {}
772        }
773    }
774}
775
776impl Dispatch<ZwpLinuxDmabufV1, ()> for CaptureState {
777    fn event(
778        _: &mut Self,
779        _: &ZwpLinuxDmabufV1,
780        _: <ZwpLinuxDmabufV1 as Proxy>::Event,
781        _: &(),
782        _: &Connection,
783        _: &QueueHandle<Self>,
784    ) {
785        // format/modifier advertisements: the allocation follows the screencopy
786        // frame's announcement instead.
787    }
788}
789
790impl Dispatch<ZwpLinuxBufferParamsV1, ()> for CaptureState {
791    fn event(
792        state: &mut Self,
793        _: &ZwpLinuxBufferParamsV1,
794        event: zwp_linux_buffer_params_v1::Event,
795        _: &(),
796        _: &Connection,
797        _: &QueueHandle<Self>,
798    ) {
799        match event {
800            zwp_linux_buffer_params_v1::Event::Created { buffer } => {
801                state.params_created = Some(buffer);
802            }
803            zwp_linux_buffer_params_v1::Event::Failed => state.params_failed = true,
804            _ => {}
805        }
806    }
807
808    fn event_created_child(
809        _opcode: u16,
810        qhandle: &QueueHandle<Self>,
811    ) -> std::sync::Arc<dyn wayland_client::backend::ObjectData> {
812        qhandle.make_data::<wayland_client::protocol::wl_buffer::WlBuffer, _>(())
813    }
814}
815
816impl Dispatch<ZwlrScreencopyFrameV1, ()> for CaptureState {
817    fn event(
818        state: &mut Self,
819        _: &ZwlrScreencopyFrameV1,
820        event: zwlr_screencopy_frame_v1::Event,
821        _: &(),
822        _: &Connection,
823        _: &QueueHandle<Self>,
824    ) {
825        match event {
826            zwlr_screencopy_frame_v1::Event::Buffer {
827                format: WEnum::Value(f),
828                width,
829                height,
830                stride,
831            } => {
832                state.announce_shm = Some((f as u32, width as i32, height as i32, stride as i32));
833            }
834            zwlr_screencopy_frame_v1::Event::LinuxDmabuf {
835                format,
836                width,
837                height,
838            } => {
839                state.announce_dmabuf = Some((format, width as i32, height as i32));
840            }
841            zwlr_screencopy_frame_v1::Event::BufferDone => state.buffer_done = true,
842            zwlr_screencopy_frame_v1::Event::Damage {
843                x,
844                y,
845                width,
846                height,
847            } => {
848                state.damage.push(Rectangle::new(
849                    (x as i32, y as i32).into(),
850                    (width as i32, height as i32).into(),
851                ));
852            }
853            zwlr_screencopy_frame_v1::Event::Ready { .. } => state.ready = true,
854            zwlr_screencopy_frame_v1::Event::Failed => state.failed = true,
855            _ => {}
856        }
857    }
858}
859
860enum SlotBuffer {
861    Gpu {
862        _bo: gbm::BufferObject<()>,
863        dmabuf: Dmabuf,
864        wl: wayland_client::protocol::wl_buffer::WlBuffer,
865    },
866    Cpu {
867        _pool: wl_shm_pool::WlShmPool,
868        map: Arc<memmap2::MmapMut>,
869        stride: i32,
870        format: u32,
871        wl: wayland_client::protocol::wl_buffer::WlBuffer,
872    },
873}
874
875/// Release the host proxies a slot created. Dropping a wayland-client proxy sends no request,
876/// so without this every resize or buffer-type switch strands the slot's `wl_buffer` — and, on the
877/// shm path, its `wl_shm_pool` — in the host compositor for the connection's lifetime. The buffer is
878/// destroyed before the pool it was created from.
879impl Drop for SlotBuffer {
880    fn drop(&mut self) {
881        match self {
882            SlotBuffer::Gpu { wl, .. } => wl.destroy(),
883            SlotBuffer::Cpu { _pool, wl, .. } => {
884                wl.destroy();
885                _pool.destroy();
886            }
887        }
888    }
889}
890
891/// One host output's calloop-side handle: control channel, wake pipe, frames.
892struct OutputHandle {
893    to_thread: Sender<ToHost>,
894    wake: OwnedFd,
895    frames: Receiver<HostFrame>,
896    /// Frames sent and not yet received, the capture thread adding and the takers below
897    /// subtracting; exact, so it may read negative for an instant.
898    queued: Arc<AtomicI64>,
899    /// Newest frame, kept (slot and all) until replaced so an IDR request on a
900    /// static screen can re-encode current content like compositor mode does.
901    retained: Mutex<Option<HostFrame>>,
902    name: Option<String>,
903}
904
905impl OutputHandle {
906    fn send(&self, msg: ToHost) {
907        let _ = self.to_thread.send(msg);
908        wake_write(self.wake.as_raw_fd());
909    }
910}
911
912#[derive(Clone, Copy)]
913struct SerializedMods {
914    depressed: u32,
915    latched: u32,
916    locked: u32,
917    group: u32,
918}
919
920#[derive(Default)]
921struct HostKeyboardState {
922    state: Option<xkb::State>,
923    pressed: std::collections::HashSet<u32>,
924}
925
926impl HostKeyboardState {
927    fn set_keymap(&mut self, text: &str) -> Option<SerializedMods> {
928        let keymap = crate::wayland::keymap::compile_keymap(text)?;
929        // Latched, locked, and the locked layout live in the xkb state rather than the
930        // pressed set, so they are read off the outgoing state and seeded back once the
931        // held keys are replayed; without this a keymap swap drops CapsLock and a locked
932        // group, and the modifiers() that follows clears them on the host.
933        let carried = self.state.as_ref().map(|s| {
934            (
935                s.serialize_mods(xkb::STATE_MODS_LATCHED),
936                s.serialize_mods(xkb::STATE_MODS_LOCKED),
937                s.serialize_layout(xkb::STATE_LAYOUT_LOCKED),
938            )
939        });
940        let mut state = xkb::State::new(&keymap);
941        for &keycode in &self.pressed {
942            state.update_key(xkb::Keycode::new(keycode), xkb::KeyDirection::Down);
943        }
944        if let Some((latched, locked, layout)) = carried {
945            let depressed = state.serialize_mods(xkb::STATE_MODS_DEPRESSED);
946            state.update_mask(depressed, latched, locked, 0, 0, layout);
947        }
948        let mods = Self::serialize(&state);
949        self.state = Some(state);
950        Some(mods)
951    }
952
953    fn update_key(&mut self, xkb_keycode: u32, pressed: bool) -> Option<SerializedMods> {
954        // A key can only move a state that exists; with no keymap the caller sends no key
955        // event, so the pressed set must not record the transition either.
956        self.state.as_ref()?;
957        let changed = if pressed {
958            self.pressed.insert(xkb_keycode)
959        } else {
960            self.pressed.remove(&xkb_keycode)
961        };
962        if !changed {
963            return None;
964        }
965        let state = self.state.as_mut()?;
966        state.update_key(
967            xkb::Keycode::new(xkb_keycode),
968            if pressed {
969                xkb::KeyDirection::Down
970            } else {
971                xkb::KeyDirection::Up
972            },
973        );
974        Some(Self::serialize(state))
975    }
976
977    /// The keysym `xkb_keycode` carries at the base level of the effective layout, if any.
978    fn base_keysym(&self, xkb_keycode: u32) -> Option<u32> {
979        let state = self.state.as_ref()?;
980        let layout = state.serialize_layout(xkb::STATE_LAYOUT_EFFECTIVE);
981        let sym = state
982            .get_keymap()
983            .key_get_syms_by_level(xkb::Keycode::new(xkb_keycode), layout, 0)
984            .first()?
985            .raw();
986        (sym != 0).then_some(sym)
987    }
988
989    fn serialize(state: &xkb::State) -> SerializedMods {
990        SerializedMods {
991            depressed: state.serialize_mods(xkb::STATE_MODS_DEPRESSED),
992            latched: state.serialize_mods(xkb::STATE_MODS_LATCHED),
993            locked: state.serialize_mods(xkb::STATE_MODS_LOCKED),
994            group: state.serialize_layout(xkb::STATE_LAYOUT_EFFECTIVE),
995        }
996    }
997}
998
999/// The calloop-side session: input proxies plus one capture handle per output.
1000pub struct HostSession {
1001    conn: Connection,
1002    vk: Option<ZwpVirtualKeyboardV1>,
1003    keyboard: Mutex<HostKeyboardState>,
1004    vptr: Option<ZwlrVirtualPointerV1>,
1005    /// Kernel input devices, taken where `/dev/uinput` can serve them and the
1006    /// compositor offers neither libei nor a virtual keyboard or pointer, ahead
1007    /// of the portal's own methods.
1008    uinput: Option<crate::uinput::Pair>,
1009    /// The portal session when frames or an input device come through xdg-desktop-portal.
1010    portal: Option<Arc<PortalCtl>>,
1011    /// Output geometry by rank, pairing portal streams with outputs.
1012    geoms: Vec<OutputGeom>,
1013    ctrl_tx: Sender<CtrlMsg>,
1014    ctrl_wake: OwnedFd,
1015    outputs: Vec<OutputHandle>,
1016    layout: Mutex<std::collections::BTreeMap<u32, LayoutSlot>>,
1017    /// Rank -> registry index, for looking a display's host output up in `sizes`.
1018    order: Vec<usize>,
1019    sizes: Arc<Mutex<Vec<(i32, i32)>>>,
1020    layouts: Arc<Mutex<LayoutLedger>>,
1021    alive: Arc<AtomicBool>,
1022    /// A finger's scroll the uinput wheels have not taken yet, in pixels (`pointer_scroll`).
1023    scroll_carry: Mutex<(f64, f64)>,
1024}
1025
1026impl HostSession {
1027    /// Connect to `display`, bring up input devices, enumerate the host's
1028    /// outputs, and spawn one capture thread per output (idle until
1029    /// [`start_capture`]) plus the layout-control thread. `gbm_path` (this
1030    /// process's render node) enables the zero-copy path; `frame_wake` is told the
1031    /// output index every time a capture thread queues a frame.
1032    pub fn connect(
1033        display: &str,
1034        gbm_path: Option<std::path::PathBuf>,
1035        frame_wake: smithay::reexports::calloop::channel::Sender<usize>,
1036        dma_formats: Vec<(u32, Vec<u64>)>,
1037        cursor_tx: std::sync::mpsc::Sender<CursorJob>,
1038    ) -> Result<Self, String> {
1039        let path = socket_path(display).ok_or("XDG_RUNTIME_DIR is unset")?;
1040        let stream = UnixStream::connect(&path).map_err(|e| format!("connect {path}: {e}"))?;
1041        let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
1042        let mut queue = conn.new_event_queue();
1043        let qh = queue.handle();
1044        let _registry = conn.display().get_registry(&qh, ());
1045        let mut state = CtrlState::default();
1046        bounded_roundtrip(&conn, &mut queue, &mut state)?;
1047
1048        let seat = state
1049            .seat
1050            .clone()
1051            .ok_or("host compositor advertises no wl_seat")?;
1052        if state.outputs.is_empty() {
1053            return Err("host compositor has no wl_output".into());
1054        }
1055        let native_capture =
1056            state.has_screencopy || (state.has_ext_capture && state.has_ext_source);
1057        let portal_devices = if state.vk_mgr.is_none() {
1058            portal::DEVICE_KEYBOARD
1059        } else {
1060            0
1061        } | if state.vptr_mgr.is_none() {
1062            portal::DEVICE_POINTER
1063        } else {
1064            0
1065        };
1066        let portal = if !native_capture || portal_devices != 0 {
1067            match PortalSession::probe() {
1068                Ok(()) => Some(PortalCtl::spawn(!native_capture, portal_devices)),
1069                Err(_) if native_capture => None,
1070                Err(why) => {
1071                    return Err(format!(
1072                        "host compositor offers neither ext-image-copy-capture nor zwlr_screencopy_manager_v1 (v3), and no xdg-desktop-portal answers on the session bus ({why})"
1073                    ));
1074                }
1075            }
1076        } else {
1077            None
1078        };
1079        if !native_capture {
1080            println!(
1081                "[HostCapture] host offers no capture protocol; frames come through the xdg-desktop-portal ScreenCast."
1082            );
1083        }
1084
1085        let base_keymap = crate::wayland::vkclient::us_base_text();
1086        let vk = match &state.vk_mgr {
1087            Some(mgr) => {
1088                let vk = mgr.create_virtual_keyboard(&seat, &qh, ());
1089                // A keymap must precede any key event; selkies replaces this with
1090                // its managed keymap through the ABI as soon as it starts.
1091                if let Some(text) = base_keymap {
1092                    let mut data = text.as_bytes().to_vec();
1093                    data.push(0);
1094                    let fd = memfd_with(&data)?;
1095                    vk.keymap(KEYMAP_FORMAT_XKB_V1, fd.as_fd(), data.len() as u32);
1096                }
1097                Some(vk)
1098            }
1099            None => {
1100                eprintln!(
1101                    "[HostCapture] no zwp_virtual_keyboard_manager_v1: keyboard {}.",
1102                    if portal.is_some() {
1103                        "goes through the portal by keysym"
1104                    } else {
1105                        "injection disabled"
1106                    }
1107                );
1108                None
1109            }
1110        };
1111        let vptr = match &state.vptr_mgr {
1112            Some(mgr) => Some(mgr.create_virtual_pointer(Some(&seat), &qh, ())),
1113            None => {
1114                eprintln!(
1115                    "[HostCapture] no zwlr_virtual_pointer_manager_v1: pointer {}.",
1116                    if portal.is_some() {
1117                        "goes through the portal"
1118                    } else {
1119                        "injection disabled"
1120                    }
1121                );
1122                None
1123            }
1124        };
1125        // Second round-trip: wl_output v4 name events and output-management heads.
1126        bounded_roundtrip(&conn, &mut queue, &mut state)?;
1127
1128        let alive = Arc::new(AtomicBool::new(true));
1129        let mut order: Vec<usize> = (0..state.outputs.len()).collect();
1130        order.sort_by_key(|&i| output_order_key(state.outputs[i].1.as_ref(), i));
1131        let names: Vec<Option<String>> =
1132            order.iter().map(|&i| state.outputs[i].1.clone()).collect();
1133        let geoms: Vec<OutputGeom> = {
1134            let sizes = state.sizes.lock().unwrap();
1135            order
1136                .iter()
1137                .map(|&i| {
1138                    let scale = state.scales.get(i).copied().unwrap_or(1).max(1);
1139                    let size = sizes.get(i).copied().unwrap_or((0, 0));
1140                    OutputGeom {
1141                        pos: state.positions.get(i).copied().unwrap_or((0, 0)),
1142                        logical: (size.0 / scale, size.1 / scale),
1143                    }
1144                })
1145                .collect()
1146        };
1147        state.order = order;
1148        println!(
1149            "[HostCapture] host outputs: {}.",
1150            names
1151                .iter()
1152                .enumerate()
1153                .map(|(i, n)| format!("{i}={}", n.as_deref().unwrap_or("?")))
1154                .collect::<Vec<_>>()
1155                .join(", ")
1156        );
1157
1158        let mut outputs = Vec::new();
1159        for (i, name) in names.iter().cloned().enumerate() {
1160            let (wake_rd, wake_wr) = wake_pipe()?;
1161            let (to_thread, from_main) = std::sync::mpsc::channel::<ToHost>();
1162            let (frame_tx, frames) = std::sync::mpsc::channel::<HostFrame>();
1163            let queued = Arc::new(AtomicI64::new(0));
1164            let sink = FrameSink {
1165                tx: frame_tx,
1166                queued: queued.clone(),
1167                wake: frame_wake.clone(),
1168                index: i,
1169            };
1170            let display = display.to_string();
1171            let expect = name.clone();
1172            let gbm_path = gbm_path.clone();
1173            let alive = alive.clone();
1174            let portal_ctl = portal.clone().filter(|_| !native_capture);
1175            let (dma_formats, cursor_tx, geoms) =
1176                (dma_formats.clone(), cursor_tx.clone(), geoms.clone());
1177            std::thread::Builder::new()
1178                .name(format!("pf-host-cap{i}"))
1179                .spawn(move || {
1180                    crate::boost_thread_priority(-10);
1181                    let outcome = match portal_ctl {
1182                        Some(ctl) => portal_capture_loop(
1183                            i,
1184                            ctl,
1185                            from_main,
1186                            sink,
1187                            cursor_tx,
1188                            dma_formats,
1189                            geoms,
1190                        ),
1191                        None => {
1192                            capture_loop(&display, i, expect, gbm_path, from_main, wake_rd, sink)
1193                        }
1194                    };
1195                    if let Err(e) = outcome {
1196                        eprintln!("[HostCapture] output {i} capture ended: {e}");
1197                    }
1198                    if i == 0 {
1199                        alive.store(false, Ordering::Relaxed);
1200                    }
1201                })
1202                .map_err(|e| format!("spawn: {e}"))?;
1203            outputs.push(OutputHandle {
1204                to_thread,
1205                wake: wake_wr,
1206                frames,
1207                queued,
1208                retained: Mutex::new(None),
1209                name,
1210            });
1211        }
1212
1213        let (ctrl_tx, ctrl_rx) = std::sync::mpsc::channel::<CtrlMsg>();
1214        let (ctrl_wake_rd, ctrl_wake) = wake_pipe()?;
1215        let order_for_session = state.order.clone();
1216        let sizes = state.sizes.clone();
1217        let layouts = Arc::new(Mutex::new(LayoutLedger::default()));
1218        {
1219            let conn = conn.clone();
1220            let ledger = layouts.clone();
1221            let alive = alive.clone();
1222            std::thread::Builder::new()
1223                .name("pf-host-ctrl".into())
1224                .spawn(move || {
1225                    control_loop(conn, queue, state, ctrl_rx, ctrl_wake_rd, ledger);
1226                    // The primary connection carries the virtual keyboard and pointer: its
1227                    // end is the session's end, whatever the capture threads still do.
1228                    alive.store(false, Ordering::Relaxed);
1229                })
1230                .map_err(|e| format!("spawn: {e}"))?;
1231        }
1232
1233        let layout = Mutex::new(std::collections::BTreeMap::new());
1234        let mut keyboard = HostKeyboardState::default();
1235        let has_keyboard =
1236            vk.is_some() || portal_devices & portal::DEVICE_KEYBOARD != 0 && portal.is_some();
1237        if has_keyboard
1238            && base_keymap
1239                .and_then(|text| keyboard.set_keymap(text))
1240                .is_none()
1241        {
1242            eprintln!(
1243                "[HostCapture] base keymap unavailable: keys are dropped until selkies uploads its keymap."
1244            );
1245        }
1246        let uinput = crate::uinput::Pair::open();
1247        if uinput.is_some() {
1248            println!("[HostCapture] input goes through kernel uinput devices.");
1249        }
1250        Ok(Self {
1251            conn,
1252            vk,
1253            keyboard: Mutex::new(keyboard),
1254            vptr,
1255            uinput,
1256            portal,
1257            geoms,
1258            ctrl_tx,
1259            ctrl_wake,
1260            outputs,
1261            layout,
1262            order: order_for_session,
1263            sizes,
1264            layouts,
1265            alive,
1266            scroll_carry: Mutex::new((0.0, 0.0)),
1267        })
1268    }
1269
1270    pub fn output_count(&self) -> usize {
1271        self.outputs.len()
1272    }
1273
1274    /// The host output backing `display_id`: active display ids map onto host
1275    /// outputs by ascending rank (selkies ids are sparse — 'display2' is id 2).
1276    fn output_index_for(&self, display_id: u32) -> Option<usize> {
1277        let layout = self.layout.lock().unwrap();
1278        let rank = layout
1279            .iter()
1280            .filter(|(_, s)| s.active)
1281            .position(|(id, _)| *id == display_id)?;
1282        (rank < self.outputs.len()).then_some(rank)
1283    }
1284
1285    /// True when an active capture on `display_id` has a host output behind it.
1286    pub fn has_output_for(&self, display_id: u32) -> bool {
1287        self.output_index_for(display_id).is_some()
1288    }
1289
1290    /// The display captured from host output `index`, if an active capture sits on it.
1291    pub fn display_for_output(&self, index: usize) -> Option<u32> {
1292        let layout = self.layout.lock().unwrap();
1293        layout
1294            .iter()
1295            .filter(|(_, s)| s.active)
1296            .nth(index)
1297            .map(|(id, _)| *id)
1298    }
1299
1300    /// Whether host output `index` has queued a frame no render has taken yet.
1301    pub fn has_queued_frame(&self, index: usize) -> bool {
1302        self.outputs
1303            .get(index)
1304            .is_some_and(|h| h.queued.load(Ordering::Acquire) > 0)
1305    }
1306
1307    /// Record where selkies laid out `display_id` (union coordinates); pushed
1308    /// to the host with the next capture (re)start.
1309    pub fn set_layout(&self, display_id: u32, x: i32, y: i32) {
1310        let mut layout = self.layout.lock().unwrap();
1311        layout.entry(display_id).or_default().pos = (x, y);
1312    }
1313
1314    /// The current mode of the host output backing `display_id` (physical
1315    /// pixels), as the host last announced it — what a capture follows when the
1316    /// host keeps its own mode instead of taking the requested one.
1317    pub fn current_output_size(&self, display_id: u32) -> Option<(i32, i32)> {
1318        let rank = self.output_index_for(display_id)?;
1319        let registry_idx = *self.order.get(rank)?;
1320        let size = *self.sizes.lock().unwrap().get(registry_idx)?;
1321        (size.0 > 0 && size.1 > 0).then_some(size)
1322    }
1323
1324    /// Number a layout request and hand it to the control thread.
1325    fn send_apply(&self, slots: Vec<LayoutSlot>) -> u64 {
1326        let epoch = self.layouts.lock().unwrap().issue();
1327        let _ = self.ctrl_tx.send(CtrlMsg::Apply { epoch, slots });
1328        wake_write(self.ctrl_wake.as_raw_fd());
1329        epoch
1330    }
1331
1332    /// The host's answer to the layout request `epoch` (as returned by [`start_capture`]):
1333    /// `Some(true)` once the host said it applied the request, `Some(false)` once it said it
1334    /// did not (refusal, no layout management at all — KWin —, or no answer within
1335    /// `LAYOUT_DEADLINE`), `None` while still unanswered. Never blocks. Which of the two it
1336    /// is settles nothing on its own, since a host may acknowledge a mode it did not take:
1337    /// the caller polls for either answer, then compares [`current_output_size`] with what
1338    /// it asked for.
1339    pub fn layout_outcome(&self, epoch: u64) -> Option<bool> {
1340        self.layouts.lock().unwrap().outcome(epoch)
1341    }
1342
1343    /// Aim `display_id` at `width`x`height`: assigns every active display its
1344    /// host output (by rank), asks the host for all modes and positions in one
1345    /// atomic configuration, and points the capture threads at their sizes and
1346    /// buffer types; frames gate until the host applies them. `zero_copy` states
1347    /// whether this display's encoder imports dmabufs — a CPU consumer needs shm
1348    /// frames it can read, and gets them even on a GPU-capable host. Returns the
1349    /// layout request's epoch for [`layout_outcome`]; nothing here waits on the host.
1350    pub fn start_capture(
1351        &self,
1352        display_id: u32,
1353        width: i32,
1354        height: i32,
1355        zero_copy: bool,
1356        paint_cursor: bool,
1357        fps: f64,
1358    ) -> u64 {
1359        if let Some(ctl) = &self.portal {
1360            ctl.set_paint(paint_cursor);
1361        }
1362        let assignments = {
1363            let mut layout = self.layout.lock().unwrap();
1364            let slot = layout.entry(display_id).or_default();
1365            slot.want = (width, height);
1366            slot.zero_copy = zero_copy;
1367            slot.paint_cursor = paint_cursor;
1368            slot.fps_milli = fps_milli(fps);
1369            slot.active = true;
1370            let active: Vec<(u32, LayoutSlot)> = layout
1371                .iter()
1372                .filter(|(_, s)| s.active)
1373                .map(|(id, s)| (*id, *s))
1374                .collect();
1375            if active
1376                .iter()
1377                .position(|(id, _)| *id == display_id)
1378                .unwrap_or(usize::MAX)
1379                >= self.outputs.len()
1380            {
1381                eprintln!(
1382                    "[HostCapture] display {display_id} has no host output (host has {}); not captured.",
1383                    self.outputs.len()
1384                );
1385            }
1386            active
1387        };
1388        // Ranks may have shifted (a lower id joined): repoint every assigned
1389        // capture thread; a same-size Start is a no-op for an unaffected one.
1390        let mut by_output: Vec<LayoutSlot> = Vec::new();
1391        for (rank, (_, slot)) in assignments.iter().enumerate() {
1392            if rank >= self.outputs.len() {
1393                break;
1394            }
1395            // A mode change invalidates everything buffered for this output:
1396            // those frames belong to the old geometry and would otherwise be
1397            // consumed at the new one.
1398            let size_mismatch = self.outputs[rank]
1399                .retained
1400                .lock()
1401                .unwrap()
1402                .as_ref()
1403                .is_some_and(|f| f.width != slot.want.0 || f.height != slot.want.1);
1404            if size_mismatch {
1405                self.drop_buffered_frames(rank);
1406            }
1407            self.outputs[rank].send(slot.start());
1408            by_output.push(*slot);
1409        }
1410        self.send_apply(by_output)
1411    }
1412
1413    /// Point `display_id`'s capture at dmabufs (`zero_copy`) or shm frames without touching its
1414    /// mode or position, which is what an encoder demoted or rebuilt mid-capture needs: the
1415    /// layout is already what the host applied, and re-running it would mode-set the host
1416    /// display again. Idempotent, so a consumer that keeps seeing the wrong buffer type until
1417    /// the capture thread reallocates can call this on every frame.
1418    pub fn set_buffer_type(&self, display_id: u32, zero_copy: bool) {
1419        let target = {
1420            let mut layout = self.layout.lock().unwrap();
1421            let slot = layout.entry(display_id).or_default();
1422            if slot.zero_copy == zero_copy {
1423                return;
1424            }
1425            slot.zero_copy = zero_copy;
1426            let (start, active) = (slot.start(), slot.active);
1427            let rank = layout
1428                .iter()
1429                .filter(|(_, s)| s.active)
1430                .position(|(id, _)| *id == display_id);
1431            match rank {
1432                Some(r) if active && r < self.outputs.len() => (r, start),
1433                _ => return,
1434            }
1435        };
1436        let (idx, start) = target;
1437        // Same geometry, new buffer type: a retained dmabuf cannot be consumed
1438        // by the shm path (nor vice versa), so everything buffered goes back.
1439        self.drop_buffered_frames(idx);
1440        self.outputs[idx].send(start);
1441    }
1442
1443    /// Point `display_id`'s capture at a new rate: what a portal stream asks the compositor
1444    /// to hold; the Wayland capture protocols are demand-driven and ignore it.
1445    pub fn set_fps(&self, display_id: u32, fps: f64) {
1446        let target = {
1447            let mut layout = self.layout.lock().unwrap();
1448            let slot = layout.entry(display_id).or_default();
1449            let milli = fps_milli(fps);
1450            if slot.fps_milli == milli {
1451                return;
1452            }
1453            slot.fps_milli = milli;
1454            let (start, active) = (slot.start(), slot.active);
1455            let rank = layout
1456                .iter()
1457                .filter(|(_, s)| s.active)
1458                .position(|(id, _)| *id == display_id);
1459            match rank {
1460                Some(r) if active && r < self.outputs.len() => (r, start),
1461                _ => return,
1462            }
1463        };
1464        self.outputs[target.0].send(target.1);
1465    }
1466
1467    /// Switch every active capture between host-painted and consumer-drawn cursors
1468    /// (the consumer's native cursor rendering toggle), keeping geometry and buffer type.
1469    /// Frames already buffered stay valid either way. Idempotent.
1470    pub fn set_cursor_painting(&self, paint_cursor: bool) {
1471        if let Some(ctl) = &self.portal {
1472            ctl.set_paint(paint_cursor);
1473        }
1474        let targets: Vec<(usize, LayoutSlot)> = {
1475            let mut layout = self.layout.lock().unwrap();
1476            let mut out = Vec::new();
1477            for (rank, (_, slot)) in layout.iter_mut().filter(|(_, s)| s.active).enumerate() {
1478                if rank >= self.outputs.len() {
1479                    break;
1480                }
1481                if slot.paint_cursor != paint_cursor {
1482                    slot.paint_cursor = paint_cursor;
1483                    out.push((rank, *slot));
1484                }
1485            }
1486            out
1487        };
1488        for (idx, slot) in targets {
1489            self.outputs[idx].send(slot.start());
1490        }
1491    }
1492
1493    /// Drop every buffered frame for an output: a mode or buffer-type change
1494    /// makes the cached pixels unusable — consumed at the new geometry they
1495    /// smear garbage rows or fault an encoder built for the new size. The
1496    /// slots go straight back to the pool; dropping them silently would wedge
1497    /// a slot until the next renegotiation.
1498    fn drop_buffered_frames(&self, idx: usize) {
1499        let handle = &self.outputs[idx];
1500        if let Some(old) = handle.retained.lock().unwrap().take() {
1501            handle.send(ToHost::Release {
1502                generation: old.generation,
1503                slot: old.slot,
1504            });
1505        }
1506        while let Ok(frame) = handle.frames.try_recv() {
1507            handle.queued.fetch_sub(1, Ordering::AcqRel);
1508            handle.send(ToHost::Release {
1509                generation: frame.generation,
1510                slot: frame.slot,
1511            });
1512        }
1513    }
1514
1515    /// Park `display_id`'s capture (its slot leaves the pointer extent). The
1516    /// retained frame and anything still queued go back to the capture pool —
1517    /// an idle output has no consumer for them, and dropping them silently
1518    /// would wedge slots until the next renegotiation.
1519    pub fn idle_output(&self, display_id: u32) {
1520        let Some(idx) = self.output_index_for(display_id) else {
1521            self.layout
1522                .lock()
1523                .unwrap()
1524                .entry(display_id)
1525                .or_default()
1526                .active = false;
1527            return;
1528        };
1529        self.layout
1530            .lock()
1531            .unwrap()
1532            .entry(display_id)
1533            .or_default()
1534            .active = false;
1535        self.outputs[idx].send(ToHost::Idle);
1536        self.drop_buffered_frames(idx);
1537    }
1538
1539    /// False once the host connection is gone (the primary connection or the first
1540    /// output's capture ended): the owner drops the session and reports its captures as
1541    /// stopped, so they are rebuilt against a new connection instead of feeding a dead one.
1542    pub fn alive(&self) -> bool {
1543        self.alive.load(Ordering::Relaxed) && !self.portal.as_ref().is_some_and(|c| c.dead())
1544    }
1545
1546    /// Hand one frame back to the pool (same path the drain/idle releases take).
1547    pub fn release_frame(&self, display_id: u32, frame: HostFrame) {
1548        if let Some(idx) = self.output_index_for(display_id) {
1549            self.outputs[idx].send(ToHost::Release {
1550                generation: frame.generation,
1551                slot: frame.slot,
1552            });
1553        }
1554    }
1555
1556    /// Newest ready frame for `display_id`, releasing any staler ones straight
1557    /// back to the pool.
1558    pub fn try_take_frame(&self, display_id: u32) -> Option<HostFrame> {
1559        let handle = self.outputs.get(self.output_index_for(display_id)?)?;
1560        let mut newest: Option<HostFrame> = None;
1561        while let Ok(frame) = handle.frames.try_recv() {
1562            handle.queued.fetch_sub(1, Ordering::AcqRel);
1563            if let Some(stale) = newest.replace(frame) {
1564                handle.send(ToHost::Release {
1565                    generation: stale.generation,
1566                    slot: stale.slot,
1567                });
1568            }
1569        }
1570        newest
1571    }
1572
1573    /// Keep `frame` as `display_id`'s current content (releasing the one it
1574    /// replaces).
1575    pub fn retain_frame(&self, display_id: u32, frame: HostFrame) {
1576        let Some(handle) = self
1577            .output_index_for(display_id)
1578            .and_then(|i| self.outputs.get(i))
1579        else {
1580            return;
1581        };
1582        let old = handle.retained.lock().unwrap().replace(frame);
1583        if let Some(old) = old {
1584            handle.send(ToHost::Release {
1585                generation: old.generation,
1586                slot: old.slot,
1587            });
1588        }
1589    }
1590
1591    /// Run `f` with `display_id`'s retained (current-content) frame, if any.
1592    pub fn with_retained<R>(&self, display_id: u32, f: impl FnOnce(Option<&HostFrame>) -> R) -> R {
1593        match self
1594            .output_index_for(display_id)
1595            .and_then(|i| self.outputs.get(i))
1596        {
1597            Some(handle) => f(handle.retained.lock().unwrap().as_ref()),
1598            None => f(None),
1599        }
1600    }
1601
1602    /// Take selkies' managed keymap: uploaded verbatim to the virtual keyboard, and the
1603    /// table portal keys resolve their keysyms from.
1604    pub fn set_keymap(&self, text: &str) {
1605        let mut keyboard = self.keyboard.lock().unwrap();
1606        let Some(mods) = keyboard.set_keymap(text) else {
1607            eprintln!("[HostCapture] keymap state update failed");
1608            return;
1609        };
1610        let Some(vk) = &self.vk else { return };
1611        let mut data = text.as_bytes().to_vec();
1612        data.push(0);
1613        let fd = match memfd_with(&data) {
1614            Ok(fd) => fd,
1615            Err(e) => {
1616                eprintln!("[HostCapture] keymap upload failed: {e}");
1617                return;
1618            }
1619        };
1620        vk.keymap(KEYMAP_FORMAT_XKB_V1, fd.as_fd(), data.len() as u32);
1621        vk.modifiers(mods.depressed, mods.latched, mods.locked, mods.group);
1622        let _ = self.conn.flush();
1623    }
1624
1625    /// Key event in xkb numbering (evdev + 8), matching the seat injectors. The virtual
1626    /// keyboard takes the keycode under selkies' keymap; the portal takes the keysym that
1627    /// keycode carries at its base level, which the host resolves in its own layout, and the
1628    /// evdev code only for a key without one.
1629    pub fn key(&self, xkb_keycode: u32, pressed: bool) {
1630        if xkb_keycode < 8 {
1631            return;
1632        }
1633        let mut keyboard = self.keyboard.lock().unwrap();
1634        let Some(mods) = keyboard.update_key(xkb_keycode, pressed) else {
1635            return;
1636        };
1637        let sym = keyboard.base_keysym(xkb_keycode);
1638        drop(keyboard);
1639        if let Some(live) = self.portal_ei() {
1640            live.ei.as_ref().unwrap().key(xkb_keycode, sym, pressed);
1641            return;
1642        }
1643        if let Some(vk) = &self.vk {
1644            vk.key(0, xkb_keycode - 8, if pressed { 1 } else { 0 });
1645            vk.modifiers(mods.depressed, mods.latched, mods.locked, mods.group);
1646            let _ = self.conn.flush();
1647            return;
1648        }
1649        if let Some(u) = &self.uinput {
1650            let _ = u.keyboard.emit(
1651                crate::uinput::EV_KEY,
1652                (xkb_keycode - 8) as u16,
1653                if pressed { 1 } else { 0 },
1654            );
1655            return;
1656        }
1657        self.with_portal(|p| match sym {
1658            Some(sym) => p.keysym(sym as i32, pressed),
1659            None => p.keycode(xkb_keycode as i32 - 8, pressed),
1660        });
1661    }
1662
1663    /// Run `f` with the portal session while one is live.
1664    fn with_portal(&self, f: impl FnOnce(&PortalSession)) {
1665        if let Some(live) = self.portal.as_ref().and_then(|c| c.live()) {
1666            f(&live.session);
1667        }
1668    }
1669
1670    /// The live portal session's libei injector, while one is up: input takes it over the
1671    /// portal's `Notify*` methods where the backend granted an EIS socket.
1672    fn portal_ei(&self) -> Option<Arc<PortalLive>> {
1673        self.portal
1674            .as_ref()
1675            .and_then(|c| c.live())
1676            .filter(|l| l.ei.as_ref().is_some_and(|e| e.alive()))
1677    }
1678
1679    /// Union-layout bounding box of every active, output-backed display (the
1680    /// virtual-pointer extent, matching the space selkies injects in).
1681    fn extent(&self) -> (i32, i32) {
1682        let layout = self.layout.lock().unwrap();
1683        let mut w = 0;
1684        let mut h = 0;
1685        for (rank, (_, slot)) in layout.iter().filter(|(_, s)| s.active).enumerate() {
1686            if rank >= self.outputs.len() {
1687                break;
1688            }
1689            w = w.max(slot.pos.0 + slot.want.0);
1690            h = h.max(slot.pos.1 + slot.want.1);
1691        }
1692        (w, h)
1693    }
1694
1695    /// The portal stream under a union-layout point and the point in that stream's logical
1696    /// space: the active display whose slot holds the point (else the nearest slot), paired
1697    /// with its stream by rank.
1698    fn portal_target(&self, x: f64, y: f64) -> Option<(u32, f64, f64)> {
1699        let live = self.portal.as_ref()?.live()?;
1700        let slots: Vec<LayoutSlot> = {
1701            let layout = self.layout.lock().unwrap();
1702            layout
1703                .values()
1704                .filter(|s| s.active)
1705                .copied()
1706                .take(self.outputs.len())
1707                .collect()
1708        };
1709        let rank = nearest_slot(&slots, x, y)?;
1710        let assignment = assign_streams(&live.session.streams, &self.geoms);
1711        let stream = &live.session.streams[(*assignment.get(rank)?)?];
1712        let slot = &slots[rank];
1713        let (sx, sy) = stream_point(slot, stream.size.unwrap_or(slot.want), x, y);
1714        Some((stream.node_id, sx, sy))
1715    }
1716
1717    pub fn pointer_motion_abs(&self, x: f64, y: f64) {
1718        if let Some(live) = self.portal_ei() {
1719            let (w, h) = self.extent();
1720            if w > 0 && h > 0 {
1721                let ei = live.ei.as_ref().unwrap();
1722                ei.pointer_motion_abs(x.clamp(0.0, (w - 1) as f64), y.clamp(0.0, (h - 1) as f64));
1723            }
1724            return;
1725        }
1726        let Some(vp) = &self.vptr else {
1727            if let Some(u) = &self.uinput {
1728                let (w, h) = self.extent();
1729                if w > 1 && h > 1 {
1730                    let scale = |v: f64, span: i32| {
1731                        (v.clamp(0.0, (span - 1) as f64) / (span - 1) as f64
1732                            * crate::uinput::ABS_RANGE as f64)
1733                            .round() as i32
1734                    };
1735                    let _ =
1736                        u.pointer
1737                            .emit(crate::uinput::EV_ABS, crate::uinput::ABS_X, scale(x, w));
1738                    let _ =
1739                        u.pointer
1740                            .emit(crate::uinput::EV_ABS, crate::uinput::ABS_Y, scale(y, h));
1741                }
1742            } else if let Some((node, sx, sy)) = self.portal_target(x, y) {
1743                self.with_portal(|p| p.pointer_motion_abs(node, sx, sy));
1744            }
1745            return;
1746        };
1747        let (w, h) = self.extent();
1748        if w <= 0 || h <= 0 {
1749            return;
1750        }
1751        let cx = x.clamp(0.0, (w - 1) as f64) as u32;
1752        let cy = y.clamp(0.0, (h - 1) as f64) as u32;
1753        vp.motion_absolute(0, cx, cy, w as u32, h as u32);
1754        vp.frame();
1755        let _ = self.conn.flush();
1756    }
1757
1758    /// Relative motion in union-layout units, sent as the host's own relative motion: the
1759    /// host moves its pointer by the delta and clamps it to its outputs, or, for a
1760    /// pointer-locked client, passes the delta on without moving it. An absolute warp
1761    /// against a position tracked here could not do the second: the locked host pointer
1762    /// stays put, so every warp would read as the whole distance from the lock point.
1763    pub fn pointer_motion_rel(&self, dx: f64, dy: f64) {
1764        if dx == 0.0 && dy == 0.0 {
1765            return;
1766        }
1767        if let Some(live) = self.portal_ei() {
1768            live.ei.as_ref().unwrap().pointer_motion(dx, dy);
1769            return;
1770        }
1771        let Some(vp) = &self.vptr else {
1772            if let Some(u) = &self.uinput {
1773                let _ = u.pointer.emit(
1774                    crate::uinput::EV_REL,
1775                    crate::uinput::REL_X,
1776                    dx.round() as i32,
1777                );
1778                let _ = u.pointer.emit(
1779                    crate::uinput::EV_REL,
1780                    crate::uinput::REL_Y,
1781                    dy.round() as i32,
1782                );
1783            } else {
1784                self.with_portal(|p| p.pointer_motion(dx, dy));
1785            }
1786            return;
1787        };
1788        vp.motion(0, dx, dy);
1789        vp.frame();
1790        let _ = self.conn.flush();
1791    }
1792
1793    pub fn pointer_button(&self, btn: u32, pressed: bool) {
1794        if let Some(live) = self.portal_ei() {
1795            live.ei
1796                .as_ref()
1797                .unwrap()
1798                .pointer_button(btn as i32, pressed);
1799            return;
1800        }
1801        let Some(vp) = &self.vptr else {
1802            if let Some(u) = &self.uinput {
1803                let _ = u.pointer.emit(
1804                    crate::uinput::EV_KEY,
1805                    btn as u16,
1806                    if pressed { 1 } else { 0 },
1807                );
1808            } else {
1809                self.with_portal(|p| p.pointer_button(btn as i32, pressed));
1810            }
1811            return;
1812        };
1813        vp.button(
1814            0,
1815            btn,
1816            if pressed {
1817                wl_pointer::ButtonState::Pressed
1818            } else {
1819                wl_pointer::ButtonState::Released
1820            },
1821        );
1822        vp.frame();
1823        let _ = self.conn.flush();
1824    }
1825
1826    /// Wheel scroll: the continuous value plus the discrete step count the seat path reports
1827    /// as v120, so host clients that scroll by notches (terminals, games, GTK list views)
1828    /// see the same steps as in nested mode; a value below one notch stays continuous. The
1829    /// source follows each axis request, which is the axis it applies to on wlroots.
1830    pub fn pointer_axis(&self, dx: f64, dy: f64) {
1831        if dx == 0.0 && dy == 0.0 {
1832            return;
1833        }
1834        let steps = |value: f64| (value * crate::SCROLL_V120_PER_UNIT / 120.0).round() as i32;
1835        let scroll =
1836            |axis: u32, value: f64, cont: &dyn Fn(f64, f64, bool), disc: &dyn Fn(u32, i32)| {
1837                match steps(value) {
1838                    0 => cont(
1839                        if axis == 1 { value } else { 0.0 },
1840                        if axis == 0 { value } else { 0.0 },
1841                        true,
1842                    ),
1843                    n => disc(axis, n),
1844                }
1845            };
1846        if let Some(live) = self.portal_ei() {
1847            let ei = live.ei.as_ref().unwrap();
1848            for (axis, value) in [(0u32, dy), (1u32, dx)] {
1849                if value != 0.0 {
1850                    scroll(axis, value, &|x, y, f| ei.pointer_axis(x, y, f), &|a, n| {
1851                        ei.pointer_axis_discrete(a, n)
1852                    });
1853                }
1854            }
1855            return;
1856        }
1857        let Some(vp) = &self.vptr else {
1858            if let Some(u) = &self.uinput {
1859                // A wheel counts up where the axis counts down, so the vertical step is negated.
1860                for (code, value, sign) in [
1861                    (crate::uinput::REL_WHEEL, dy, -1),
1862                    (crate::uinput::REL_HWHEEL, dx, 1),
1863                ] {
1864                    match steps(value) {
1865                        0 => {}
1866                        n => {
1867                            let _ = u.pointer.emit(crate::uinput::EV_REL, code, n * sign);
1868                        }
1869                    }
1870                }
1871            } else {
1872                self.with_portal(|p| {
1873                    for (axis, value) in [(0u32, dy), (1u32, dx)] {
1874                        if value != 0.0 {
1875                            scroll(axis, value, &|x, y, f| p.pointer_axis(x, y, f), &|a, n| {
1876                                p.pointer_axis_discrete(a, n)
1877                            });
1878                        }
1879                    }
1880                });
1881            }
1882            return;
1883        };
1884        for (axis, value) in [
1885            (wl_pointer::Axis::VerticalScroll, dy),
1886            (wl_pointer::Axis::HorizontalScroll, dx),
1887        ] {
1888            if value == 0.0 {
1889                continue;
1890            }
1891            let steps = steps(value);
1892            if steps != 0 {
1893                vp.axis_discrete(0, axis, value, steps);
1894            } else {
1895                vp.axis(0, axis, value);
1896            }
1897            vp.axis_source(wl_pointer::AxisSource::Wheel);
1898        }
1899        vp.frame();
1900        let _ = self.conn.flush();
1901    }
1902}
1903
1904/// Pixels of a finger's scroll per wheel notch where only notches exist (the uinput wheels):
1905/// the size Selkies' page takes for a touchpad's notch.
1906const SCROLL_PX_PER_NOTCH: f64 = 100.0;
1907
1908/// The whole notches in `carry + delta` pixels, the fraction left in `carry`.
1909fn scroll_notches(carry: &mut f64, delta: f64) -> i32 {
1910    *carry += delta;
1911    let notches = (*carry / SCROLL_PX_PER_NOTCH).trunc();
1912    *carry -= notches * SCROLL_PX_PER_NOTCH;
1913    notches as i32
1914}
1915
1916impl HostSession {
1917    /// A finger's scroll by (dx, dy) pixels on the host: smooth, as a finger source ended by
1918    /// `pointer_scroll_end`, through libei, the portal or the virtual pointer, and in whole
1919    /// wheel notches with the remainder carried on uinput, whose wheels have no other.
1920    pub fn pointer_scroll(&self, dx: f64, dy: f64) {
1921        if dx == 0.0 && dy == 0.0 {
1922            return;
1923        }
1924        if let Some(live) = self.portal_ei() {
1925            live.ei.as_ref().unwrap().pointer_axis(dx, dy, false);
1926            return;
1927        }
1928        let Some(vp) = &self.vptr else {
1929            if let Some(u) = &self.uinput {
1930                let mut carry = self.scroll_carry.lock().unwrap();
1931                // A wheel counts up where the axis counts down, so the vertical step is negated.
1932                let (wheel, hwheel) = (
1933                    scroll_notches(&mut carry.1, dy),
1934                    scroll_notches(&mut carry.0, dx),
1935                );
1936                if wheel != 0 {
1937                    let _ = u
1938                        .pointer
1939                        .emit(crate::uinput::EV_REL, crate::uinput::REL_WHEEL, -wheel);
1940                }
1941                if hwheel != 0 {
1942                    let _ =
1943                        u.pointer
1944                            .emit(crate::uinput::EV_REL, crate::uinput::REL_HWHEEL, hwheel);
1945                }
1946            } else {
1947                self.with_portal(|p| p.pointer_axis(dx, dy, false));
1948            }
1949            return;
1950        };
1951        vp.axis_source(wl_pointer::AxisSource::Finger);
1952        for (axis, value) in [
1953            (wl_pointer::Axis::VerticalScroll, dy),
1954            (wl_pointer::Axis::HorizontalScroll, dx),
1955        ] {
1956            if value != 0.0 {
1957                vp.axis(0, axis, value);
1958            }
1959        }
1960        vp.frame();
1961        let _ = self.conn.flush();
1962    }
1963
1964    /// The finger lifted: the stop each smooth rung takes; uinput drops what it carried.
1965    pub fn pointer_scroll_end(&self) {
1966        if let Some(live) = self.portal_ei() {
1967            live.ei.as_ref().unwrap().pointer_axis(0.0, 0.0, true);
1968            return;
1969        }
1970        let Some(vp) = &self.vptr else {
1971            if self.uinput.is_some() {
1972                *self.scroll_carry.lock().unwrap() = (0.0, 0.0);
1973            } else {
1974                self.with_portal(|p| p.pointer_axis(0.0, 0.0, true));
1975            }
1976            return;
1977        };
1978        vp.axis_source(wl_pointer::AxisSource::Finger);
1979        vp.axis_stop(0, wl_pointer::Axis::VerticalScroll);
1980        vp.axis_stop(0, wl_pointer::Axis::HorizontalScroll);
1981        vp.frame();
1982        let _ = self.conn.flush();
1983    }
1984}
1985
1986impl Drop for HostSession {
1987    fn drop(&mut self) {
1988        if let Some(ctl) = &self.portal {
1989            ctl.close();
1990        }
1991    }
1992}
1993
1994/// A capture rate as the millihertz a portal stream is asked to hold.
1995fn fps_milli(fps: f64) -> u32 {
1996    (fps.max(1.0) * 1000.0).round() as u32
1997}
1998
1999impl LayoutSlot {
2000    /// The Start message aiming a capture thread at this slot.
2001    fn start(&self) -> ToHost {
2002        ToHost::Start {
2003            width: self.want.0,
2004            height: self.want.1,
2005            zero_copy: self.zero_copy,
2006            paint_cursor: self.paint_cursor,
2007            fps_milli: self.fps_milli,
2008        }
2009    }
2010}
2011
2012/// The active slot holding `(x, y)`, else the one nearest to it, by rank.
2013fn nearest_slot(slots: &[LayoutSlot], x: f64, y: f64) -> Option<usize> {
2014    slots
2015        .iter()
2016        .enumerate()
2017        .map(|(rank, s)| {
2018            let cx = x.clamp(s.pos.0 as f64, (s.pos.0 + s.want.0).max(s.pos.0) as f64);
2019            let cy = y.clamp(s.pos.1 as f64, (s.pos.1 + s.want.1).max(s.pos.1) as f64);
2020            (rank, (cx - x).powi(2) + (cy - y).powi(2))
2021        })
2022        .min_by(|a, b| a.1.total_cmp(&b.1))
2023        .map(|(rank, _)| rank)
2024}
2025
2026/// A union-layout point in the logical space of the stream behind `slot`: the slot captures
2027/// its output at physical size `want`, the portal addresses it at logical size `logical`.
2028fn stream_point(slot: &LayoutSlot, logical: (i32, i32), x: f64, y: f64) -> (f64, f64) {
2029    let scale = |v: f64, from: i32, to: i32| {
2030        let mapped = if from > 0 {
2031            v * to as f64 / from as f64
2032        } else {
2033            v
2034        };
2035        mapped.clamp(0.0, (to.max(1) - 1) as f64)
2036    };
2037    (
2038        scale(x - slot.pos.0 as f64, slot.want.0, logical.0),
2039        scale(y - slot.pos.1 as f64, slot.want.1, logical.1),
2040    )
2041}
2042
2043/// Wait, bounded, until the GPU work the compositor queued on a host dmabuf has finished:
2044/// the CUDA import reads the buffer without honoring its implicit fences, so a frame
2045/// encoded the instant it was announced could otherwise carry a half-blitted image. Drivers
2046/// that attach no fence return at once.
2047pub fn wait_gpu_done(dmabuf: &Dmabuf) {
2048    for fd in dmabuf.handles() {
2049        let mut pfd = libc::pollfd {
2050            fd: fd.as_raw_fd(),
2051            events: libc::POLLIN,
2052            revents: 0,
2053        };
2054        unsafe { libc::poll(&mut pfd, 1, 50) };
2055    }
2056}
2057
2058// ---------------------------------------------------------------------------
2059// Portal rung: the session-wide portal, and the stream consumer per output.
2060// ---------------------------------------------------------------------------
2061
2062/// One host output's placement, for pairing portal streams with outputs.
2063#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2064pub(crate) struct OutputGeom {
2065    /// Layout position in the compositor's logical space.
2066    pos: (i32, i32),
2067    /// Mode divided by the integer scale.
2068    logical: (i32, i32),
2069}
2070
2071/// Which portal stream backs each output rank: the stream announcing the output's position,
2072/// else the one announcing its logical size when that is unambiguous, else the stream at the
2073/// same index (a backend that sends no geometry lists monitors in its output order).
2074fn assign_streams(streams: &[PortalStream], geoms: &[OutputGeom]) -> Vec<Option<usize>> {
2075    let mut taken = vec![false; streams.len()];
2076    let mut out = vec![None; geoms.len()];
2077    for (rank, g) in geoms.iter().enumerate() {
2078        if let Some(i) = streams.iter().position(|s| s.position == Some(g.pos))
2079            && !taken[i]
2080        {
2081            out[rank] = Some(i);
2082            taken[i] = true;
2083        }
2084    }
2085    for (rank, g) in geoms.iter().enumerate() {
2086        if out[rank].is_some() {
2087            continue;
2088        }
2089        let mut same_size = streams
2090            .iter()
2091            .enumerate()
2092            .filter(|(i, s)| !taken[*i] && s.position.is_none() && s.size == Some(g.logical));
2093        if let (Some((i, _)), None) = (same_size.next(), same_size.next()) {
2094            out[rank] = Some(i);
2095            taken[i] = true;
2096        }
2097    }
2098    for (rank, slot) in out.iter_mut().enumerate() {
2099        if slot.is_none() && rank < streams.len() && !taken[rank] {
2100            *slot = Some(rank);
2101            taken[rank] = true;
2102        }
2103    }
2104    out
2105}
2106
2107/// A live portal session: the D-Bus session, and the PipeWire connection its streams live on
2108/// when frames come through it.
2109pub(crate) struct PortalLive {
2110    pub(crate) session: PortalSession,
2111    pub(crate) core: Option<Arc<PwCore>>,
2112    /// Numbers the session so an output thread notices a reopened one.
2113    pub(crate) epoch: u64,
2114    /// The cursor painting this session was opened for.
2115    paint: bool,
2116    /// The libei injector when the portal handed out an EIS socket; input then rides it
2117    /// instead of the portal's `Notify*` methods, which the session refuses once EIS is on.
2118    ei: Option<EiInjector>,
2119}
2120
2121#[derive(Default)]
2122struct PortalInner {
2123    /// The cursor painting the next session opens with; none until the first Start decides.
2124    paint: Option<bool>,
2125    live: Option<Arc<PortalLive>>,
2126    epoch: u64,
2127    restore_token: Option<String>,
2128    dead: Option<String>,
2129    closed: bool,
2130}
2131
2132/// Session-wide portal state: the session thread opens (and, when the cursor mode changes,
2133/// reopens) the portal session; the outputs and the input methods read the live one.
2134pub(crate) struct PortalCtl {
2135    inner: Mutex<PortalInner>,
2136    changed: Condvar,
2137    capture: bool,
2138    devices: u32,
2139}
2140
2141impl PortalCtl {
2142    fn spawn(capture: bool, devices: u32) -> Arc<Self> {
2143        let ctl = Arc::new(PortalCtl {
2144            inner: Mutex::new(PortalInner::default()),
2145            changed: Condvar::new(),
2146            capture,
2147            devices,
2148        });
2149        let worker = ctl.clone();
2150        let _ = std::thread::Builder::new()
2151            .name("pf-host-portal".into())
2152            .spawn(move || portal_thread(worker));
2153        ctl
2154    }
2155
2156    /// The cursor painting the session should carry (embedded cursor, or cursor metadata
2157    /// for the consumer to draw); a change reopens the session.
2158    fn set_paint(&self, paint: bool) {
2159        let mut inner = self.inner.lock().unwrap();
2160        if inner.paint != Some(paint) {
2161            inner.paint = Some(paint);
2162            self.changed.notify_all();
2163        }
2164    }
2165
2166    pub(crate) fn live(&self) -> Option<Arc<PortalLive>> {
2167        self.inner.lock().unwrap().live.clone()
2168    }
2169
2170    fn dead(&self) -> bool {
2171        self.inner.lock().unwrap().dead.is_some()
2172    }
2173
2174    fn mark_dead(&self, why: String) {
2175        let mut inner = self.inner.lock().unwrap();
2176        if inner.dead.is_none() {
2177            eprintln!("[HostCapture] portal session lost: {why}");
2178            inner.dead = Some(why);
2179        }
2180        inner.live = None;
2181        self.changed.notify_all();
2182    }
2183
2184    fn close(&self) {
2185        let mut inner = self.inner.lock().unwrap();
2186        inner.closed = true;
2187        inner.live = None;
2188        self.changed.notify_all();
2189    }
2190}
2191
2192/// The portal thread: opens the session once the first Start has decided the cursor mode,
2193/// reopens it (restoring with the token the backend gave) whenever that mode changes, and
2194/// ends with the host session.
2195fn portal_thread(ctl: Arc<PortalCtl>) {
2196    loop {
2197        let (paint, token) = {
2198            let mut inner = ctl.inner.lock().unwrap();
2199            loop {
2200                if inner.closed || inner.dead.is_some() {
2201                    return;
2202                }
2203                if let Some(p) = inner.paint
2204                    && inner.live.as_ref().is_none_or(|l| l.paint != p)
2205                {
2206                    break (
2207                        p,
2208                        inner
2209                            .restore_token
2210                            .clone()
2211                            .or_else(portal::saved_restore_token),
2212                    );
2213                }
2214                inner = ctl.changed.wait(inner).unwrap();
2215            }
2216        };
2217        // The outgoing session closes outside the lock; its Close is what stops the backend's
2218        // streams before the new session asks for them again.
2219        let old = ctl.inner.lock().unwrap().live.take();
2220        drop(old);
2221        let cursor_mode = if ctl.capture && !paint {
2222            portal::CURSOR_METADATA
2223        } else {
2224            portal::CURSOR_EMBEDDED
2225        };
2226        let open = |devices: u32| {
2227            PortalSession::open(ctl.capture, devices, cursor_mode, token.as_deref()).and_then(
2228                |session| {
2229                    let core = if ctl.capture {
2230                        Some(PwCore::connect(session.open_pipewire_remote()?)?)
2231                    } else {
2232                        None
2233                    };
2234                    Ok((session, core))
2235                },
2236            )
2237        };
2238        // A host that grants the screen but refuses the input devices answers the whole request as
2239        // a refusal, so asking again for capture alone is what turns that into a session with
2240        // video and no injection, rather than no session at all.
2241        let opened = match open(ctl.devices) {
2242            Err(e) if ctl.capture && ctl.devices != 0 => {
2243                eprintln!(
2244                    "[HostCapture] portal refused keyboard and pointer ({e}); capturing without them."
2245                );
2246                open(0)
2247            }
2248            other => other,
2249        };
2250        let mut inner = ctl.inner.lock().unwrap();
2251        match opened {
2252            Ok((session, core)) => {
2253                inner.epoch += 1;
2254                if session.restore_token.is_some() {
2255                    inner.restore_token = session.restore_token.clone();
2256                }
2257                // Prefer libei where the backend offers it: the injector is kept only once its
2258                // handshake binds a device, and a successful ConnectToEIS makes the session
2259                // refuse the Notify* methods, so the choice stands for the session's life.
2260                let ei = (session.devices != 0 && session.eis_capable())
2261                    .then(|| open_eis(&session))
2262                    .flatten();
2263                println!(
2264                    "[HostCapture] portal session open: {} stream(s), devices {}{}, cursor {}, input via {}.",
2265                    session.streams.len(),
2266                    if session.devices & portal::DEVICE_KEYBOARD != 0 {
2267                        "keyboard "
2268                    } else {
2269                        ""
2270                    },
2271                    if session.devices & portal::DEVICE_POINTER != 0 {
2272                        "pointer"
2273                    } else {
2274                        ""
2275                    },
2276                    match session.cursor_mode {
2277                        portal::CURSOR_METADATA => "metadata",
2278                        portal::CURSOR_EMBEDDED => "embedded",
2279                        _ => "hidden",
2280                    },
2281                    if ei.is_some() {
2282                        "libei"
2283                    } else {
2284                        "portal notify"
2285                    }
2286                );
2287                inner.live = Some(Arc::new(PortalLive {
2288                    session,
2289                    core,
2290                    epoch: inner.epoch,
2291                    paint,
2292                    ei,
2293                }));
2294            }
2295            Err(e) => {
2296                eprintln!("[HostCapture] portal session failed: {e}");
2297                inner.dead = Some(e);
2298            }
2299        }
2300        ctl.changed.notify_all();
2301    }
2302}
2303
2304/// Ask the portal for an EIS socket and bring an injector up on it, or `None` when the backend
2305/// refuses `ConnectToEIS` (leaving the `Notify*` path live) or the EIS handshake binds nothing.
2306fn open_eis(session: &PortalSession) -> Option<EiInjector> {
2307    let fd = match session.connect_to_eis() {
2308        Ok(fd) => fd,
2309        Err(e) => {
2310            eprintln!(
2311                "[HostCapture] portal offers no EIS socket ({e}); input goes through the portal."
2312            );
2313            return None;
2314        }
2315    };
2316    match EiInjector::spawn(fd) {
2317        Ok(ei) => Some(ei),
2318        Err(e) => {
2319            eprintln!(
2320                "[HostCapture] EIS injector unavailable ({e}); input goes through the portal."
2321            );
2322            None
2323        }
2324    }
2325}
2326
2327/// The capture thread of a portal-fed output: it holds the PipeWire stream of the output's
2328/// portal stream for as long as a Start aims at it, re-offering the stream's configuration
2329/// on every change, returning released buffers, and following the portal session across
2330/// reopenings. Frames themselves are published by the stream's own thread.
2331fn portal_capture_loop(
2332    index: usize,
2333    ctl: Arc<PortalCtl>,
2334    from_main: Receiver<ToHost>,
2335    sink: FrameSink,
2336    cursor_tx: std::sync::mpsc::Sender<CursorJob>,
2337    dma_formats: Vec<(u32, Vec<u64>)>,
2338    geoms: Vec<OutputGeom>,
2339) -> Result<(), String> {
2340    let mut want: Option<Want> = None;
2341    let mut stream: Option<(PwStream, u64)> = None;
2342    let mut warned_unshared = false;
2343    loop {
2344        let first = match from_main.recv_timeout(Duration::from_millis(100)) {
2345            Ok(m) => Some(m),
2346            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => None,
2347            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return Ok(()),
2348        };
2349        let mut pending: Vec<ToHost> = first.into_iter().collect();
2350        loop {
2351            match from_main.try_recv() {
2352                Ok(m) => pending.push(m),
2353                Err(TryRecvError::Empty) => break,
2354                Err(TryRecvError::Disconnected) => return Ok(()),
2355            }
2356        }
2357        for msg in pending {
2358            match msg {
2359                ToHost::Release { generation, slot } => {
2360                    if let Some((s, _)) = &stream {
2361                        s.release(generation, slot);
2362                    }
2363                }
2364                ToHost::Idle => {
2365                    want = None;
2366                    if let Some((s, _)) = &stream {
2367                        s.release_all();
2368                        s.set_active(false);
2369                    }
2370                }
2371                ToHost::Start {
2372                    width,
2373                    height,
2374                    zero_copy,
2375                    paint_cursor,
2376                    fps_milli,
2377                } => {
2378                    want = Some(Want {
2379                        size: (width, height),
2380                        zero_copy,
2381                        paint_cursor,
2382                        fps_milli,
2383                    });
2384                }
2385            }
2386        }
2387        let live = ctl.live();
2388        if let Some((_, epoch)) = &stream
2389            && live.as_ref().is_none_or(|l| l.epoch != *epoch)
2390        {
2391            stream = None;
2392        }
2393        let Some(w) = want else { continue };
2394        let Some(live) = live else { continue };
2395        let Some(core) = live.core.as_ref() else {
2396            continue;
2397        };
2398        let cfg = StreamConfig {
2399            zero_copy: w.zero_copy && !dma_formats.is_empty(),
2400            dma_formats: dma_formats.clone(),
2401            fps_milli: w.fps_milli,
2402            expect: w.size,
2403        };
2404        match &stream {
2405            Some((s, _)) => {
2406                s.reconfigure(cfg);
2407                s.set_active(true);
2408                if s.errored() {
2409                    ctl.mark_dead(format!("output {index}: PipeWire stream failed"));
2410                    return Err("PipeWire stream failed".into());
2411                }
2412            }
2413            None => {
2414                let Some(si) = assign_streams(&live.session.streams, &geoms)
2415                    .get(index)
2416                    .copied()
2417                    .flatten()
2418                else {
2419                    if !warned_unshared {
2420                        warned_unshared = true;
2421                        eprintln!(
2422                            "[HostCapture] output {index}: the portal shares no stream for it; nothing captured."
2423                        );
2424                    }
2425                    continue;
2426                };
2427                match PwStream::connect(
2428                    core,
2429                    live.session.streams[si].node_id,
2430                    sink.clone(),
2431                    cursor_tx.clone(),
2432                    cfg,
2433                ) {
2434                    Ok(s) => stream = Some((s, live.epoch)),
2435                    Err(e) => {
2436                        ctl.mark_dead(format!("output {index}: {e}"));
2437                        return Err(e);
2438                    }
2439                }
2440            }
2441        }
2442    }
2443}
2444
2445// Dropping the session drops every channel sender and wake-pipe write end; the
2446// capture and control threads observe the disconnect (poll sees POLLHUP) and
2447// exit on their own.
2448
2449// ---------------------------------------------------------------------------
2450// Control thread: pumps the primary connection and applies layout requests.
2451// ---------------------------------------------------------------------------
2452
2453fn control_loop(
2454    conn: Connection,
2455    mut queue: EventQueue<CtrlState>,
2456    mut state: CtrlState,
2457    ctrl_rx: Receiver<CtrlMsg>,
2458    wake_rd: OwnedFd,
2459    ledger: Arc<Mutex<LayoutLedger>>,
2460) {
2461    let qh = queue.handle();
2462    loop {
2463        let mut pending: Option<(u64, Vec<LayoutSlot>)> = None;
2464        loop {
2465            match ctrl_rx.try_recv() {
2466                Ok(CtrlMsg::Apply { epoch, slots }) => pending = Some((epoch, slots)),
2467                Err(TryRecvError::Empty) => break,
2468                Err(TryRecvError::Disconnected) => return,
2469            }
2470        }
2471        if let Some((epoch, slots)) = pending {
2472            let realized = apply_layout(&conn, &mut queue, &mut state, &qh, &slots);
2473            ledger.lock().unwrap().decide(epoch, realized);
2474        }
2475        if queue.dispatch_pending(&mut state).is_err() {
2476            return;
2477        }
2478        let _ = queue.flush();
2479        if let Some(guard) = conn.prepare_read() {
2480            match wait_readable2(
2481                guard.connection_fd().as_raw_fd(),
2482                wake_rd.as_raw_fd(),
2483                Some(Duration::from_secs(1)),
2484            ) {
2485                Ok((wl, wake)) => {
2486                    if wake {
2487                        drain_pipe(wake_rd.as_raw_fd());
2488                    }
2489                    if wl {
2490                        let _ = guard.read();
2491                    } else {
2492                        drop(guard);
2493                    }
2494                }
2495                Err(_) => return,
2496            }
2497        }
2498    }
2499}
2500
2501/// Ask the host (wlr-output-management) to give every active output its wanted mode and
2502/// layout position in one atomic configuration. Retries across a `canceled` (stale serial).
2503/// Returns whether the host said it applied the layout: `false` for a refusal, no answer by
2504/// `LAYOUT_DEADLINE`, or no manager at all (KWin offers only its own kde_output_management
2505/// protocol). Either answer settles the request, and the session owner then re-sizes its
2506/// capture to the mode the host announces, so capture follows the host instead of gating
2507/// forever on a size it will never produce.
2508fn apply_layout(
2509    conn: &Connection,
2510    queue: &mut EventQueue<CtrlState>,
2511    state: &mut CtrlState,
2512    qh: &QueueHandle<CtrlState>,
2513    slots: &[LayoutSlot],
2514) -> bool {
2515    let Some(mgr) = state.output_mgr.clone() else {
2516        eprintln!(
2517            "[HostCapture] host lacks zwlr_output_manager_v1; capture follows the host's own size."
2518        );
2519        return false;
2520    };
2521    let deadline = Instant::now() + LAYOUT_DEADLINE;
2522    for _ in 0..3 {
2523        while state.om_serial.is_none() && Instant::now() < deadline {
2524            if !pump_ctrl(conn, queue, state) {
2525                return false;
2526            }
2527        }
2528        let Some(serial) = state.om_serial else {
2529            eprintln!("[HostCapture] output-management serial never arrived; resize skipped.");
2530            return false;
2531        };
2532        state.cfg_result = None;
2533        state.cfg_canceled = false;
2534        let cfg = mgr.create_configuration(serial, qh, ());
2535        let mut any = false;
2536        for (i, slot) in slots.iter().enumerate() {
2537            if !slot.active {
2538                continue;
2539            }
2540            let want_name = state
2541                .order
2542                .get(i)
2543                .and_then(|&oi| state.outputs.get(oi))
2544                .and_then(|(_, n)| n.clone());
2545            let head = match want_name
2546                .as_ref()
2547                .and_then(|n| state.heads.iter().find(|(_, hn)| hn.as_ref() == Some(n)))
2548                .or_else(|| state.heads.get(i))
2549            {
2550                Some((h, _)) => h.clone(),
2551                None => {
2552                    eprintln!(
2553                        "[HostCapture] no output-management head for output {i}; not resized."
2554                    );
2555                    continue;
2556                }
2557            };
2558            let cfg_head = cfg.enable_head(&head, qh, ());
2559            cfg_head.set_custom_mode(slot.want.0, slot.want.1, 0);
2560            cfg_head.set_position(slot.pos.0, slot.pos.1);
2561            any = true;
2562        }
2563        if !any {
2564            cfg.destroy();
2565            return false;
2566        }
2567        cfg.apply();
2568        let _ = queue.flush();
2569        while state.cfg_result.is_none() && !state.cfg_canceled && Instant::now() < deadline {
2570            if !pump_ctrl(conn, queue, state) {
2571                cfg.destroy();
2572                return false;
2573            }
2574        }
2575        cfg.destroy();
2576        if state.cfg_canceled {
2577            // Stale serial: the compositor re-announces its state with a fresh one.
2578            state.om_serial = None;
2579            continue;
2580        }
2581        if state.cfg_result != Some(true) {
2582            eprintln!(
2583                "[HostCapture] host refused the layout; capture follows the host's own size."
2584            );
2585            return false;
2586        }
2587        return true;
2588    }
2589    false
2590}
2591
2592/// One bounded dispatch step for the control connection (false = connection died).
2593fn pump_ctrl(conn: &Connection, queue: &mut EventQueue<CtrlState>, state: &mut CtrlState) -> bool {
2594    if queue.dispatch_pending(state).is_err() {
2595        return false;
2596    }
2597    let _ = queue.flush();
2598    let Some(guard) = conn.prepare_read() else {
2599        return true;
2600    };
2601    match crate::wayland::wlclient::wait_readable(
2602        guard.connection_fd().as_raw_fd(),
2603        Duration::from_millis(200),
2604    ) {
2605        Ok(readable) => {
2606            if readable {
2607                let _ = guard.read();
2608            } else {
2609                drop(guard);
2610            }
2611            true
2612        }
2613        Err(_) => false,
2614    }
2615}
2616
2617// ---------------------------------------------------------------------------
2618// Capture threads: one connection + screencopy loop per host output.
2619// ---------------------------------------------------------------------------
2620
2621fn fourcc_to_gbm(fourcc: u32) -> GbmFormat {
2622    // XR24 / AR24; anything else falls back to ARGB (the encoder reads BGRA bytes
2623    // and ignores alpha).
2624    match fourcc {
2625        0x34325258 => GbmFormat::Xrgb8888,
2626        _ => GbmFormat::Argb8888,
2627    }
2628}
2629
2630/// A dmabuf-backed capture slot: a GBM buffer object from pixelflux's render node,
2631/// exported and wrapped in a host `wl_buffer`. `modifiers` narrows the allocation to
2632/// what the host advertised; empty leaves the driver's choice, which the host learns
2633/// through the params anyway. `Ok(None)` means the host refused the import (wrong
2634/// device, unsupported modifier): the caller degrades to shm slots instead of dying
2635/// to the protocol error `create_immed` would raise.
2636#[allow(clippy::too_many_arguments)]
2637fn alloc_gpu_slot(
2638    conn: &Connection,
2639    queue: &mut EventQueue<CaptureState>,
2640    state: &mut CaptureState,
2641    wake: RawFd,
2642    dev: &GbmDevice<File>,
2643    dmabuf_global: &ZwpLinuxDmabufV1,
2644    fourcc: u32,
2645    w: i32,
2646    h: i32,
2647    modifiers: &[u64],
2648) -> Result<Option<SlotBuffer>, String> {
2649    let qh = queue.handle();
2650    let format = fourcc_to_gbm(fourcc);
2651    let bo = if modifiers.is_empty() {
2652        crate::alloc_render_target(dev, w as u32, h as u32, format)
2653    } else {
2654        // Without the flags argument, which implies the rendering use asked for either
2655        // way: the entry point taking flags is Mesa 21.1 and later, and an extension
2656        // referencing it does not load against an older libgbm at all.
2657        dev.create_buffer_object_with_modifiers::<()>(
2658            w as u32,
2659            h as u32,
2660            format,
2661            modifiers.iter().map(|&m| gbm::Modifier::from(m)),
2662        )
2663        .or_else(|_| crate::alloc_render_target(dev, w as u32, h as u32, format))
2664    }
2665    .map_err(|e| format!("GBM allocation {w}x{h}: {e:?}"))?;
2666    let dmabuf = crate::create_dmabuf_from_bo(&bo);
2667    let params = dmabuf_global.create_params(&qh, ());
2668    let modifier: u64 = dmabuf.format().modifier.into();
2669    for (i, handle) in dmabuf.handles().enumerate() {
2670        params.add(
2671            handle,
2672            i as u32,
2673            dmabuf.offsets().nth(i).unwrap_or(0),
2674            dmabuf.strides().nth(i).unwrap_or(0),
2675            (modifier >> 32) as u32,
2676            (modifier & 0xffff_ffff) as u32,
2677        );
2678    }
2679    state.params_created = None;
2680    state.params_failed = false;
2681    params.create(w, h, fourcc, zwp_linux_buffer_params_v1::Flags::empty());
2682    loop {
2683        match pump_until(
2684            conn,
2685            queue,
2686            state,
2687            wake,
2688            Some(Duration::from_secs(2)),
2689            |s| s.params_created.is_some() || s.params_failed,
2690        )? {
2691            Pump::Done => break,
2692            Pump::Control => continue,
2693            Pump::Timeout => {
2694                params.destroy();
2695                return Err("host answered neither created nor failed for the dmabuf".into());
2696            }
2697        }
2698    }
2699    params.destroy();
2700    match state.params_created.take() {
2701        Some(wl) => Ok(Some(SlotBuffer::Gpu {
2702            _bo: bo,
2703            dmabuf,
2704            wl,
2705        })),
2706        None => Ok(None),
2707    }
2708}
2709
2710/// An shm-backed capture slot in a memfd pool the CPU consumer reads in place.
2711fn alloc_cpu_slot(
2712    shm: &wl_shm::WlShm,
2713    qh: &QueueHandle<CaptureState>,
2714    format: u32,
2715    w: i32,
2716    h: i32,
2717    stride: i32,
2718) -> Result<SlotBuffer, String> {
2719    let size = (stride * h) as usize;
2720    let fd = memfd_with(&vec![0u8; size])?;
2721    let pool = shm.create_pool(fd.as_fd(), size as i32, qh, ());
2722    let wl = pool.create_buffer(
2723        0,
2724        w,
2725        h,
2726        stride,
2727        WEnum::<wl_shm::Format>::from(format)
2728            .into_result()
2729            .unwrap_or(wl_shm::Format::Xrgb8888),
2730        qh,
2731        (),
2732    );
2733    let file = File::from(fd);
2734    let map = unsafe { memmap2::MmapMut::map_mut(&file) }.map_err(|e| format!("shm map: {e}"))?;
2735    Ok(SlotBuffer::Cpu {
2736        _pool: pool,
2737        map: Arc::new(map),
2738        stride,
2739        format,
2740        wl,
2741    })
2742}
2743
2744/// What a control-channel drain decided while a capture wait was in progress.
2745enum Ctl {
2746    None,
2747    Renegotiate,
2748    Idle,
2749    Dead,
2750}
2751
2752enum Pump {
2753    Done,
2754    Control,
2755    Timeout,
2756}
2757
2758/// Dispatch events until `done(state)` holds, waiting on the Wayland socket and
2759/// the wake pipe together so control messages interrupt any capture wait. The
2760/// predicate is re-checked after every dispatch, so a wait whose condition was
2761/// satisfied by already-queued events returns without ever blocking.
2762fn pump_until(
2763    conn: &Connection,
2764    queue: &mut EventQueue<CaptureState>,
2765    state: &mut CaptureState,
2766    wake_rd: RawFd,
2767    timeout: Option<Duration>,
2768    done: impl Fn(&CaptureState) -> bool,
2769) -> Result<Pump, String> {
2770    let deadline = timeout.map(|t| Instant::now() + t);
2771    loop {
2772        queue
2773            .dispatch_pending(state)
2774            .map_err(|e| format!("dispatch: {e}"))?;
2775        if done(state) {
2776            return Ok(Pump::Done);
2777        }
2778        queue.flush().map_err(|e| format!("flush: {e}"))?;
2779        let remaining = match deadline {
2780            Some(d) => match d.checked_duration_since(Instant::now()) {
2781                Some(r) => Some(r),
2782                None => return Ok(Pump::Timeout),
2783            },
2784            None => None,
2785        };
2786        let Some(guard) = conn.prepare_read() else {
2787            continue;
2788        };
2789        let (wl, wake) = wait_readable2(guard.connection_fd().as_raw_fd(), wake_rd, remaining)?;
2790        if wake {
2791            drop(guard);
2792            drain_pipe(wake_rd);
2793            return Ok(Pump::Control);
2794        }
2795        if wl {
2796            let _ = guard.read();
2797        } else {
2798            drop(guard);
2799            if deadline.is_some_and(|d| Instant::now() >= d) {
2800                return Ok(Pump::Timeout);
2801            }
2802        }
2803    }
2804}
2805
2806fn capture_loop(
2807    display: &str,
2808    index: usize,
2809    expect_name: Option<String>,
2810    gbm_path: Option<std::path::PathBuf>,
2811    from_main: Receiver<ToHost>,
2812    wake_rd: OwnedFd,
2813    frames: FrameSink,
2814) -> Result<(), String> {
2815    let path = socket_path(display).ok_or("XDG_RUNTIME_DIR is unset")?;
2816    let stream = UnixStream::connect(&path).map_err(|e| format!("connect {path}: {e}"))?;
2817    let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
2818    let mut queue = conn.new_event_queue();
2819    let qh = queue.handle();
2820    let _registry = conn.display().get_registry(&qh, ());
2821    let mut state = CaptureState::default();
2822    bounded_roundtrip(&conn, &mut queue, &mut state)?;
2823    // Names (wl_output v4) arrive after the binds from the first round-trip.
2824    bounded_roundtrip(&conn, &mut queue, &mut state)?;
2825
2826    // Select this thread's output by the name the control connection assigned
2827    // it; ordering fallback only when the host names nothing.
2828    let by_name = expect_name.as_ref().and_then(|expect| {
2829        state
2830            .outputs
2831            .iter()
2832            .find(|(_, n)| n.as_ref() == Some(expect))
2833            .cloned()
2834    });
2835    let (output, _name) = match by_name {
2836        Some(o) => o,
2837        None => {
2838            let mut order: Vec<usize> = (0..state.outputs.len()).collect();
2839            order.sort_by_key(|&i| output_order_key(state.outputs[i].1.as_ref(), i));
2840            let oi = *order
2841                .get(index)
2842                .ok_or_else(|| format!("host has no wl_output {index}"))?;
2843            state.outputs[oi].clone()
2844        }
2845    };
2846    let gbm = gbm_path
2847        .as_ref()
2848        .and_then(|p| File::options().read(true).write(true).open(p).ok())
2849        .and_then(|f| GbmDevice::new(f).ok());
2850    let wake = wake_rd.as_raw_fd();
2851
2852    // Hosts speaking ext-image-copy-capture (wlroots 0.19+, KWin 6.2+, cosmic) get a
2853    // persistent session with compositor-side damage gating; everything else takes
2854    // wlr-screencopy v3 below, unchanged. PIXELFLUX_HOST_CAPTURE=zwlr forces the
2855    // fallback for triage.
2856    // The current aim outlives the ext attempt: a Start consumed while that session was
2857    // being opened still drives the wlr-screencopy fallback.
2858    let mut want: Option<Want> = None;
2859    let force = std::env::var("PIXELFLUX_HOST_CAPTURE").unwrap_or_default();
2860    if force != "zwlr" && state.ext_capture.is_some() && state.ext_source_mgr.is_some() {
2861        match capture_loop_ext(
2862            &conn,
2863            &mut queue,
2864            &mut state,
2865            &output,
2866            gbm.as_ref(),
2867            wake,
2868            index,
2869            &from_main,
2870            &frames,
2871            &mut want,
2872        ) {
2873            ExtOutcome::Finished => return Ok(()),
2874            ExtOutcome::Unavailable(e) => {
2875                eprintln!(
2876                    "[HostCapture] output {index}: ext capture unavailable ({e}); using wlr-screencopy."
2877                );
2878            }
2879        }
2880    }
2881
2882    let screencopy = state
2883        .screencopy
2884        .clone()
2885        .ok_or("no zwlr_screencopy_manager_v1")?;
2886
2887    let mut slots: Vec<Option<SlotBuffer>> = (0..SLOTS).map(|_| None).collect();
2888    let mut free: Vec<usize> = (0..SLOTS).collect();
2889    let mut generation: u64 = 0;
2890    let mut announced: Option<(i32, i32)> = None;
2891    let mut warned_mismatch = false;
2892    let mut consecutive_failures = 0u32;
2893    // Buffer type the currently allocated slots hold; None while none exist.
2894    let mut slot_zero_copy: Option<bool> = None;
2895    let mut gpu_refused = false;
2896
2897    'main: loop {
2898        // Drain control; block while idle or out of slots.
2899        loop {
2900            let blocking = want.is_none() || free.is_empty();
2901            let msg = if blocking {
2902                match from_main.recv() {
2903                    Ok(m) => m,
2904                    Err(_) => return Ok(()),
2905                }
2906            } else {
2907                match from_main.try_recv() {
2908                    Ok(m) => m,
2909                    Err(TryRecvError::Empty) => break,
2910                    Err(TryRecvError::Disconnected) => return Ok(()),
2911                }
2912            };
2913            match msg {
2914                ToHost::Release {
2915                    generation: g,
2916                    slot,
2917                } => {
2918                    if g == generation {
2919                        free.push(slot);
2920                    }
2921                }
2922                ToHost::Idle => want = None,
2923                ToHost::Start {
2924                    width,
2925                    height,
2926                    zero_copy,
2927                    paint_cursor,
2928                    fps_milli,
2929                } => {
2930                    let next = Want {
2931                        size: (width, height),
2932                        zero_copy,
2933                        paint_cursor,
2934                        fps_milli,
2935                    };
2936                    if want != Some(next) {
2937                        warned_mismatch = false;
2938                    }
2939                    want = Some(next);
2940                }
2941            }
2942        }
2943        let (want_w, want_h, want_zero_copy, want_paint) = match want {
2944            Some(w) => (w.size.0, w.size.1, w.zero_copy, w.paint_cursor),
2945            None => continue,
2946        };
2947
2948        // One frame: negotiate, attach our buffer, wait for damage. Control
2949        // messages (resize, idle, teardown) interrupt any of the waits.
2950        state.reset_frame();
2951        let frame = screencopy.capture_output(i32::from(want_paint), &output, &qh, ());
2952        loop {
2953            match pump_until(&conn, &mut queue, &mut state, wake, None, |s| {
2954                s.buffer_done || s.failed
2955            })? {
2956                Pump::Done => break,
2957                Pump::Control => match drain_ctl(&from_main, generation, &mut free, &mut want) {
2958                    Ctl::None => {}
2959                    Ctl::Renegotiate | Ctl::Idle => {
2960                        frame.destroy();
2961                        continue 'main;
2962                    }
2963                    Ctl::Dead => {
2964                        frame.destroy();
2965                        return Ok(());
2966                    }
2967                },
2968                Pump::Timeout => {}
2969            }
2970        }
2971        if state.failed {
2972            frame.destroy();
2973            consecutive_failures += 1;
2974            if consecutive_failures == 3 {
2975                eprintln!(
2976                    "[HostCapture] output {index}: repeated screencopy failures; is the output alive?"
2977                );
2978            }
2979            let _ = pump_until(
2980                &conn,
2981                &mut queue,
2982                &mut state,
2983                wake,
2984                Some(Duration::from_millis(100)),
2985                |_| false,
2986            );
2987            continue;
2988        }
2989
2990        let (fw, fh) = state
2991            .announce_dmabuf
2992            .map(|(_, w, h)| (w, h))
2993            .or(state.announce_shm.map(|(_, w, h, _)| (w, h)))
2994            .ok_or("screencopy announced no buffer type")?;
2995        let mut renegotiate = false;
2996        if announced != Some((fw, fh)) {
2997            announced = Some((fw, fh));
2998            eprintln!(
2999                "[HostCapture] output {index} negotiated: {fw}x{fh} gbm={} dmabuf_global={} dmabuf_announce={:?} shm={:?}",
3000                gbm.is_some(),
3001                state.dmabuf.is_some(),
3002                state.announce_dmabuf,
3003                state.announce_shm,
3004            );
3005            renegotiate = true;
3006        }
3007        if slot_zero_copy.is_some_and(|z| z != want_zero_copy) {
3008            // The consumer switched between importing dmabufs and reading pixels (an
3009            // encoder demoted, or a restart with different encode settings), so the
3010            // allocated buffers are the wrong kind.
3011            renegotiate = true;
3012        }
3013        if renegotiate {
3014            // Drop the stale buffers. The generation bump makes any release still in
3015            // flight for them a no-op.
3016            for s in slots.iter_mut() {
3017                *s = None;
3018            }
3019            free = (0..SLOTS).collect();
3020            slot_zero_copy = None;
3021            gpu_refused = false;
3022            generation += 1;
3023        }
3024        if (fw, fh) != (want_w, want_h) {
3025            // The encoder is configured for (want_w, want_h): hold frames until
3026            // the host applies the resize rather than encode mismatched buffers.
3027            frame.destroy();
3028            if !warned_mismatch {
3029                warned_mismatch = true;
3030                eprintln!(
3031                    "[HostCapture] output {index}: waiting for host {want_w}x{want_h} (currently {fw}x{fh})."
3032                );
3033            }
3034            let _ = pump_until(
3035                &conn,
3036                &mut queue,
3037                &mut state,
3038                wake,
3039                Some(Duration::from_millis(150)),
3040                |_| false,
3041            );
3042            continue;
3043        }
3044        warned_mismatch = false;
3045
3046        let slot_idx = *free.last().unwrap();
3047        if slots[slot_idx].is_none() {
3048            // Only a consumer that imports dmabufs gets GPU slots; anything reading the
3049            // pixels on the CPU takes the shm path even where the host offers dmabuf.
3050            slot_zero_copy = Some(want_zero_copy);
3051            let mut built: Option<SlotBuffer> = None;
3052            if want_zero_copy && !gpu_refused {
3053                let dmabuf_global = state.dmabuf.clone();
3054                if let (Some(dev), Some(dmabuf_global), Some((fourcc, w, h))) =
3055                    (gbm.as_ref(), dmabuf_global.as_ref(), state.announce_dmabuf)
3056                {
3057                    match alloc_gpu_slot(
3058                        &conn,
3059                        &mut queue,
3060                        &mut state,
3061                        wake,
3062                        dev,
3063                        dmabuf_global,
3064                        fourcc,
3065                        w,
3066                        h,
3067                        &[],
3068                    )? {
3069                        Some(slot) => built = Some(slot),
3070                        None => {
3071                            gpu_refused = true;
3072                            eprintln!(
3073                                "[HostCapture] output {index}: host refused the dmabuf import; capturing via shm."
3074                            );
3075                        }
3076                    }
3077                }
3078            }
3079            slots[slot_idx] = Some(match built {
3080                Some(slot) => slot,
3081                None => {
3082                    let (format, w, h, stride) =
3083                        state.announce_shm.ok_or("no shm fallback announced")?;
3084                    let shm = state.shm.clone().ok_or("host compositor lacks wl_shm")?;
3085                    alloc_cpu_slot(&shm, &qh, format, w, h, stride)?
3086                }
3087            });
3088        }
3089        free.pop();
3090
3091        {
3092            let slot = slots[slot_idx].as_ref().unwrap();
3093            let wl = match slot {
3094                SlotBuffer::Gpu { wl, .. } => wl,
3095                SlotBuffer::Cpu { wl, .. } => wl,
3096            };
3097            frame.copy_with_damage(wl);
3098        }
3099        queue.flush().map_err(|e| format!("flush: {e}"))?;
3100        let mut aborted = false;
3101        loop {
3102            match pump_until(&conn, &mut queue, &mut state, wake, None, |s| {
3103                s.ready || s.failed
3104            })? {
3105                Pump::Done => break,
3106                Pump::Control => match drain_ctl(&from_main, generation, &mut free, &mut want) {
3107                    Ctl::None => {}
3108                    Ctl::Renegotiate | Ctl::Idle => {
3109                        aborted = true;
3110                        break;
3111                    }
3112                    Ctl::Dead => {
3113                        frame.destroy();
3114                        return Ok(());
3115                    }
3116                },
3117                Pump::Timeout => {}
3118            }
3119        }
3120        frame.destroy();
3121        if aborted {
3122            free.push(slot_idx);
3123            continue 'main;
3124        }
3125        if state.failed {
3126            free.push(slot_idx);
3127            consecutive_failures += 1;
3128            if consecutive_failures == 3 {
3129                eprintln!(
3130                    "[HostCapture] output {index}: repeated screencopy failures; is the output alive?"
3131                );
3132            }
3133            let _ = pump_until(
3134                &conn,
3135                &mut queue,
3136                &mut state,
3137                wake,
3138                Some(Duration::from_millis(50)),
3139                |_| false,
3140            );
3141            continue;
3142        }
3143        consecutive_failures = 0;
3144
3145        let damage = std::mem::take(&mut state.damage);
3146        let out = match slots[slot_idx].as_ref().unwrap() {
3147            SlotBuffer::Gpu { dmabuf, .. } => HostFrame {
3148                generation,
3149                slot: slot_idx,
3150                dmabuf: Some(dmabuf.clone()),
3151                cpu: None,
3152                width: fw,
3153                height: fh,
3154                damage,
3155                stamp_ns: now_ns(),
3156            },
3157            SlotBuffer::Cpu {
3158                map,
3159                stride,
3160                format,
3161                ..
3162            } => HostFrame {
3163                generation,
3164                slot: slot_idx,
3165                dmabuf: None,
3166                cpu: Some(HostCpuFrame {
3167                    map: map.clone() as Arc<dyn AsRef<[u8]> + Send + Sync>,
3168                    offset: 0,
3169                    stride: *stride as usize,
3170                    format: *format,
3171                }),
3172                width: fw,
3173                height: fh,
3174                damage,
3175                stamp_ns: now_ns(),
3176            },
3177        };
3178        if !frames.send(out) {
3179            return Ok(());
3180        }
3181    }
3182}
3183
3184/// How the ext capture loop ended: `Finished` is a normal teardown (the thread is
3185/// done); `Unavailable` means it could not get going, so wlr-screencopy should run.
3186enum ExtOutcome {
3187    Finished,
3188    Unavailable(String),
3189}
3190
3191/// ext-image-copy-capture capture loop: one persistent session per output. The
3192/// compositor holds each capture until the content actually changes, so a static
3193/// screen costs nothing — the property `copy_with_damage` provides on the wlr path.
3194/// Frames land in the same slot machinery and `HostFrame` shape as that path.
3195#[allow(clippy::too_many_arguments)]
3196fn capture_loop_ext(
3197    conn: &Connection,
3198    queue: &mut EventQueue<CaptureState>,
3199    state: &mut CaptureState,
3200    output: &wl_output::WlOutput,
3201    gbm: Option<&GbmDevice<File>>,
3202    wake: RawFd,
3203    index: usize,
3204    from_main: &Receiver<ToHost>,
3205    frames: &FrameSink,
3206    want: &mut Option<Want>,
3207) -> ExtOutcome {
3208    let qh = queue.handle();
3209    let mut slots: Vec<Option<SlotBuffer>> = (0..SLOTS).map(|_| None).collect();
3210    let mut free: Vec<usize> = (0..SLOTS).collect();
3211    let mut generation: u64 = 0;
3212    let mut slot_zero_copy: Option<bool> = None;
3213    let mut gpu_refused = false;
3214    let mut seen_serial: u64 = 0;
3215    let mut warned_mismatch = false;
3216    let mut consecutive_failures = 0u32;
3217
3218    let teardown = |session: &ExtImageCopyCaptureSessionV1, source: &ExtImageCaptureSourceV1| {
3219        session.destroy();
3220        source.destroy();
3221    };
3222
3223    // The session opens with the cursor option of the first Start, which may already be
3224    // queued; before any Start the consumer's default (drawing the cursor itself) applies.
3225    let mut session_paints = false;
3226    let (mut source, mut session) = match open_ext_session(
3227        conn,
3228        queue,
3229        state,
3230        output,
3231        wake,
3232        index,
3233        from_main,
3234        session_paints,
3235        generation,
3236        &mut free,
3237        want,
3238    ) {
3239        Ok(opened) => opened,
3240        Err(outcome) => return outcome,
3241    };
3242
3243    'main: loop {
3244        // Drain control; block while idle or out of slots.
3245        loop {
3246            let blocking = want.is_none() || free.is_empty();
3247            let msg = if blocking {
3248                match from_main.recv() {
3249                    Ok(m) => m,
3250                    Err(_) => {
3251                        teardown(&session, &source);
3252                        return ExtOutcome::Finished;
3253                    }
3254                }
3255            } else {
3256                match from_main.try_recv() {
3257                    Ok(m) => m,
3258                    Err(TryRecvError::Empty) => break,
3259                    Err(TryRecvError::Disconnected) => {
3260                        teardown(&session, &source);
3261                        return ExtOutcome::Finished;
3262                    }
3263                }
3264            };
3265            match msg {
3266                ToHost::Release {
3267                    generation: g,
3268                    slot,
3269                } => {
3270                    if g == generation {
3271                        free.push(slot);
3272                    }
3273                }
3274                ToHost::Idle => *want = None,
3275                ToHost::Start {
3276                    width,
3277                    height,
3278                    zero_copy,
3279                    paint_cursor,
3280                    fps_milli,
3281                } => {
3282                    let next = Want {
3283                        size: (width, height),
3284                        zero_copy,
3285                        paint_cursor,
3286                        fps_milli,
3287                    };
3288                    if *want != Some(next) {
3289                        warned_mismatch = false;
3290                    }
3291                    *want = Some(next);
3292                }
3293            }
3294        }
3295        let (want_w, want_h, want_zero_copy, want_paint) = match *want {
3296            Some(w) => (w.size.0, w.size.1, w.zero_copy, w.paint_cursor),
3297            None => continue,
3298        };
3299        if state.ext_stopped {
3300            // The output is going away; nothing else on this connection will revive it.
3301            teardown(&session, &source);
3302            return ExtOutcome::Finished;
3303        }
3304        if want_paint != session_paints {
3305            // The cursor option is fixed per session: reopen with the wanted one. Its
3306            // constraints arrive under a new serial, which reallocates the slots below.
3307            teardown(&session, &source);
3308            (source, session) = match open_ext_session(
3309                conn, queue, state, output, wake, index, from_main, want_paint, generation,
3310                &mut free, want,
3311            ) {
3312                Ok(opened) => opened,
3313                Err(outcome) => return outcome,
3314            };
3315            session_paints = want_paint;
3316            continue;
3317        }
3318
3319        // Fresh constraints obsolete every allocated buffer.
3320        if seen_serial != state.ext_serial || slot_zero_copy.is_some_and(|z| z != want_zero_copy) {
3321            seen_serial = state.ext_serial;
3322            for s in slots.iter_mut() {
3323                *s = None;
3324            }
3325            free = (0..SLOTS).collect();
3326            slot_zero_copy = None;
3327            gpu_refused = false;
3328            generation += 1;
3329        }
3330
3331        let Some((cw, ch)) = state.ext_size else {
3332            teardown(&session, &source);
3333            return ExtOutcome::Unavailable("constraints carried no size".into());
3334        };
3335        if (cw, ch) != (want_w, want_h) {
3336            // The encoder is configured for (want_w, want_h): hold frames until the
3337            // host applies the resize rather than capture mismatched buffers.
3338            if !warned_mismatch {
3339                warned_mismatch = true;
3340                eprintln!(
3341                    "[HostCapture] output {index}: waiting for host {want_w}x{want_h} (currently {cw}x{ch})."
3342                );
3343            }
3344            let _ = pump_until(
3345                conn,
3346                queue,
3347                state,
3348                wake,
3349                Some(Duration::from_millis(150)),
3350                |s| s.ext_serial != seen_serial,
3351            );
3352            continue;
3353        }
3354        warned_mismatch = false;
3355
3356        let slot_idx = *free.last().unwrap();
3357        if slots[slot_idx].is_none() {
3358            // Only a consumer that imports dmabufs gets GPU slots; anything reading
3359            // the pixels on the CPU takes the shm path even where the host offers
3360            // dmabuf. XR24/AR24 keep the bytes BGRA, which is what every consumer
3361            // downstream assumes.
3362            let dma_choice = [0x3432_5258u32, 0x3432_5241]
3363                .iter()
3364                .find_map(|f| state.ext_dma_formats.iter().find(|(code, _)| code == f))
3365                .cloned();
3366            slot_zero_copy = Some(want_zero_copy);
3367            let mut slot: Option<SlotBuffer> = None;
3368            if want_zero_copy && !gpu_refused {
3369                let dmabuf_global = state.dmabuf.clone();
3370                if let (Some(dev), Some(dmabuf_global), Some((fourcc, modifiers))) =
3371                    (gbm, dmabuf_global.as_ref(), dma_choice)
3372                {
3373                    match alloc_gpu_slot(
3374                        conn,
3375                        queue,
3376                        state,
3377                        wake,
3378                        dev,
3379                        dmabuf_global,
3380                        fourcc,
3381                        cw,
3382                        ch,
3383                        &modifiers,
3384                    ) {
3385                        Ok(Some(built)) => slot = Some(built),
3386                        Ok(None) => {
3387                            gpu_refused = true;
3388                            eprintln!(
3389                                "[HostCapture] output {index}: host refused the dmabuf import; capturing via shm."
3390                            );
3391                        }
3392                        Err(e) => {
3393                            teardown(&session, &source);
3394                            return ExtOutcome::Unavailable(e);
3395                        }
3396                    }
3397                }
3398            }
3399            let built = match slot {
3400                Some(slot) => Ok(slot),
3401                None => {
3402                    let format = [1u32, 0]
3403                        .iter()
3404                        .find(|f| state.ext_shm_formats.contains(f))
3405                        .copied()
3406                        .or_else(|| state.ext_shm_formats.first().copied());
3407                    match (state.shm.clone(), format) {
3408                        (Some(shm), Some(format)) => {
3409                            alloc_cpu_slot(&shm, &qh, format, cw, ch, cw * 4)
3410                        }
3411                        _ => Err("host offers no usable shm format".into()),
3412                    }
3413                }
3414            };
3415            match built {
3416                Ok(slot) => slots[slot_idx] = Some(slot),
3417                Err(e) => {
3418                    teardown(&session, &source);
3419                    return ExtOutcome::Unavailable(e);
3420                }
3421            }
3422        }
3423        free.pop();
3424
3425        state.reset_frame();
3426        let frame = {
3427            let slot = slots[slot_idx].as_ref().unwrap();
3428            let wl = match slot {
3429                SlotBuffer::Gpu { wl, .. } => wl,
3430                SlotBuffer::Cpu { wl, .. } => wl,
3431            };
3432            let frame = session.create_frame(&qh, ());
3433            frame.attach_buffer(wl);
3434            // Slots rotate, so the attached buffer holds a frame from SLOTS captures
3435            // ago: everything in it is stale from the host's point of view.
3436            frame.damage_buffer(0, 0, cw, ch);
3437            frame.capture();
3438            frame
3439        };
3440        if let Err(e) = queue.flush() {
3441            frame.destroy();
3442            teardown(&session, &source);
3443            return ExtOutcome::Unavailable(format!("flush: {e}"));
3444        }
3445
3446        let mut aborted = false;
3447        loop {
3448            match pump_until(conn, queue, state, wake, None, |s| {
3449                s.ready || s.failed || s.ext_stopped || s.ext_serial != seen_serial
3450            }) {
3451                Ok(Pump::Done) => break,
3452                Ok(Pump::Control) => match drain_ctl(from_main, generation, &mut free, want) {
3453                    Ctl::None => {}
3454                    Ctl::Renegotiate | Ctl::Idle => {
3455                        aborted = true;
3456                        break;
3457                    }
3458                    Ctl::Dead => {
3459                        frame.destroy();
3460                        teardown(&session, &source);
3461                        return ExtOutcome::Finished;
3462                    }
3463                },
3464                Ok(Pump::Timeout) => {}
3465                Err(e) => {
3466                    frame.destroy();
3467                    teardown(&session, &source);
3468                    return ExtOutcome::Unavailable(e);
3469                }
3470            }
3471        }
3472        frame.destroy();
3473        if aborted || state.ext_serial != seen_serial {
3474            free.push(slot_idx);
3475            continue 'main;
3476        }
3477        if state.ext_stopped {
3478            teardown(&session, &source);
3479            return ExtOutcome::Finished;
3480        }
3481        if state.failed {
3482            free.push(slot_idx);
3483            if state.ext_fail_reason
3484                == Some(ext_image_copy_capture_frame_v1::FailureReason::BufferConstraints)
3485            {
3486                // Constraints changed under us; the serial check above rebuilds the
3487                // buffers on the next pass.
3488                continue;
3489            }
3490            consecutive_failures += 1;
3491            if consecutive_failures == 3 {
3492                eprintln!(
3493                    "[HostCapture] output {index}: repeated capture failures; is the output alive?"
3494                );
3495            }
3496            let _ = pump_until(
3497                conn,
3498                queue,
3499                state,
3500                wake,
3501                Some(Duration::from_millis(50)),
3502                |_| false,
3503            );
3504            continue;
3505        }
3506        consecutive_failures = 0;
3507
3508        let damage = std::mem::take(&mut state.damage);
3509        let out = match slots[slot_idx].as_ref().unwrap() {
3510            SlotBuffer::Gpu { dmabuf, .. } => HostFrame {
3511                generation,
3512                slot: slot_idx,
3513                dmabuf: Some(dmabuf.clone()),
3514                cpu: None,
3515                width: cw,
3516                height: ch,
3517                damage,
3518                stamp_ns: now_ns(),
3519            },
3520            SlotBuffer::Cpu {
3521                map,
3522                stride,
3523                format,
3524                ..
3525            } => HostFrame {
3526                generation,
3527                slot: slot_idx,
3528                dmabuf: None,
3529                cpu: Some(HostCpuFrame {
3530                    map: map.clone() as Arc<dyn AsRef<[u8]> + Send + Sync>,
3531                    offset: 0,
3532                    stride: *stride as usize,
3533                    format: *format,
3534                }),
3535                width: cw,
3536                height: ch,
3537                damage,
3538                stamp_ns: now_ns(),
3539            },
3540        };
3541        if !frames.send(out) {
3542            teardown(&session, &source);
3543            return ExtOutcome::Finished;
3544        }
3545    }
3546}
3547
3548/// Open an ext capture session on `output`, with or without the host painting its cursor
3549/// into the frames, and wait for its first buffer constraints: the point that decides
3550/// whether this protocol works here at all. A session's cursor option is fixed at
3551/// creation, so a toggle closes the session and opens another through here.
3552#[allow(clippy::too_many_arguments)]
3553fn open_ext_session(
3554    conn: &Connection,
3555    queue: &mut EventQueue<CaptureState>,
3556    state: &mut CaptureState,
3557    output: &wl_output::WlOutput,
3558    wake: RawFd,
3559    index: usize,
3560    from_main: &Receiver<ToHost>,
3561    paint_cursor: bool,
3562    generation: u64,
3563    free: &mut Vec<usize>,
3564    want: &mut Option<Want>,
3565) -> Result<(ExtImageCaptureSourceV1, ExtImageCopyCaptureSessionV1), ExtOutcome> {
3566    let qh = queue.handle();
3567    let src_mgr = state.ext_source_mgr.clone().expect("checked by caller");
3568    let mgr = state.ext_capture.clone().expect("checked by caller");
3569    let source = src_mgr.create_source(output, &qh, ());
3570    let options = if paint_cursor {
3571        ext_image_copy_capture_manager_v1::Options::PaintCursors
3572    } else {
3573        ext_image_copy_capture_manager_v1::Options::empty()
3574    };
3575    let session = mgr.create_session(&source, options, &qh, ());
3576    let teardown = |session: &ExtImageCopyCaptureSessionV1, source: &ExtImageCaptureSourceV1| {
3577        session.destroy();
3578        source.destroy();
3579    };
3580    // A previous session's stop or half-streamed constraints must not pass for this one's.
3581    state.ext_stopped = false;
3582    state.ext_pending_size = None;
3583    state.ext_pending_shm.clear();
3584    state.ext_pending_dma.clear();
3585    let before = state.ext_serial;
3586    loop {
3587        match pump_until(
3588            conn,
3589            queue,
3590            state,
3591            wake,
3592            Some(Duration::from_secs(5)),
3593            |s| s.ext_serial > before || s.ext_stopped,
3594        ) {
3595            Ok(Pump::Done) => break,
3596            Ok(Pump::Control) => match drain_ctl(from_main, generation, free, want) {
3597                Ctl::Dead => {
3598                    teardown(&session, &source);
3599                    return Err(ExtOutcome::Finished);
3600                }
3601                _ => continue,
3602            },
3603            Ok(Pump::Timeout) => {
3604                teardown(&session, &source);
3605                return Err(ExtOutcome::Unavailable(
3606                    "no buffer constraints within 5s".into(),
3607                ));
3608            }
3609            Err(e) => {
3610                teardown(&session, &source);
3611                return Err(ExtOutcome::Unavailable(e));
3612            }
3613        }
3614    }
3615    if state.ext_stopped {
3616        teardown(&session, &source);
3617        return Err(ExtOutcome::Unavailable(
3618            "session stopped before constraints".into(),
3619        ));
3620    }
3621    eprintln!(
3622        "[HostCapture] output {index} ext session: {:?} dma_formats={} shm_formats={} cursor={}",
3623        state.ext_size,
3624        state.ext_dma_formats.len(),
3625        state.ext_shm_formats.len(),
3626        if paint_cursor { "painted" } else { "consumer" },
3627    );
3628    Ok((source, session))
3629}
3630
3631/// Drain the control channel from inside a capture wait: releases are applied
3632/// (generation-checked), and the newest Start/Idle decides the wait's fate.
3633fn drain_ctl(
3634    rx: &Receiver<ToHost>,
3635    generation: u64,
3636    free: &mut Vec<usize>,
3637    want: &mut Option<Want>,
3638) -> Ctl {
3639    let mut out = Ctl::None;
3640    loop {
3641        match rx.try_recv() {
3642            Ok(ToHost::Release {
3643                generation: g,
3644                slot,
3645            }) => {
3646                if g == generation {
3647                    free.push(slot);
3648                }
3649            }
3650            Ok(ToHost::Start {
3651                width,
3652                height,
3653                zero_copy,
3654                paint_cursor,
3655                fps_milli,
3656            }) => {
3657                let next = Want {
3658                    size: (width, height),
3659                    zero_copy,
3660                    paint_cursor,
3661                    fps_milli,
3662                };
3663                if *want != Some(next) {
3664                    *want = Some(next);
3665                    out = Ctl::Renegotiate;
3666                }
3667            }
3668            Ok(ToHost::Idle) => {
3669                *want = None;
3670                out = Ctl::Idle;
3671            }
3672            Err(TryRecvError::Empty) => return out,
3673            Err(TryRecvError::Disconnected) => return Ctl::Dead,
3674        }
3675    }
3676}
3677
3678#[cfg(test)]
3679mod tests {
3680    /// A finger's travel turns into whole notches with the fraction carried, both ways.
3681    #[test]
3682    fn a_finger_scroll_turns_into_whole_notches_with_the_rest_carried() {
3683        let mut carry = 0.0;
3684        assert_eq!(super::scroll_notches(&mut carry, 60.0), 0);
3685        assert_eq!(super::scroll_notches(&mut carry, 60.0), 1);
3686        assert!((carry - 20.0).abs() < 1e-9);
3687        assert_eq!(super::scroll_notches(&mut carry, -260.0), -2);
3688        assert!((carry + 40.0).abs() < 1e-9);
3689    }
3690
3691    use super::*;
3692
3693    fn us_host_keyboard() -> HostKeyboardState {
3694        let text = crate::wayland::keymap::compile_rmlvo("", "", "us", "", "").unwrap();
3695        let mut keyboard = HostKeyboardState::default();
3696        assert!(keyboard.set_keymap(&text).is_some());
3697        keyboard
3698    }
3699
3700    /// Every shm layout a compositor may announce, converted to BGRA.
3701    #[test]
3702    fn shm_rows_reach_bgra_from_each_announced_layout() {
3703        let cases = [
3704            (
3705                wl_shm::Format::Xrgb8888 as u32,
3706                vec![1u8, 2, 3, 4],
3707                vec![1u8, 2, 3, 4],
3708            ),
3709            (
3710                wl_shm::Format::Xbgr8888 as u32,
3711                vec![1u8, 2, 3, 9],
3712                vec![3u8, 2, 1, 9],
3713            ),
3714            (
3715                wl_shm::Format::Bgr888 as u32,
3716                vec![1u8, 2, 3],
3717                vec![3u8, 2, 1, 0xff],
3718            ),
3719            (
3720                wl_shm::Format::Rgb888 as u32,
3721                vec![1u8, 2, 3],
3722                vec![1u8, 2, 3, 0xff],
3723            ),
3724        ];
3725        for (format, src, want) in cases {
3726            let (bpp, swap) = shm_src_layout(format);
3727            let mut dst = vec![0u8; 4];
3728            convert_shm_row(&src, &mut dst, bpp, swap);
3729            assert_eq!(dst, want, "format {format}");
3730        }
3731    }
3732
3733    /// A row wider than one pixel converts every pixel, not just the first.
3734    #[test]
3735    fn shm_rows_convert_every_pixel_of_the_row() {
3736        let src: Vec<u8> = (0..12u8).collect();
3737        let mut dst = vec![0u8; 16];
3738        convert_shm_row(&src, &mut dst, 3, true);
3739        assert_eq!(
3740            dst,
3741            vec![2, 1, 0, 0xff, 5, 4, 3, 0xff, 8, 7, 6, 0xff, 11, 10, 9, 0xff]
3742        );
3743        let src: Vec<u8> = (0..8u8).collect();
3744        let mut dst = vec![0u8; 8];
3745        convert_shm_row(&src, &mut dst, 4, true);
3746        assert_eq!(dst, vec![2, 1, 0, 3, 6, 5, 4, 7]);
3747    }
3748
3749    #[test]
3750    fn host_keyboard_serializes_control_modifier() {
3751        let mut keyboard = us_host_keyboard();
3752        let control_down = keyboard.update_key(37, true).unwrap();
3753        assert_ne!(control_down.depressed, 0);
3754        let control_up = keyboard.update_key(37, false).unwrap();
3755        assert_eq!(control_up.depressed, 0);
3756    }
3757
3758    #[test]
3759    fn host_keyboard_ignores_repeated_key_down() {
3760        let mut keyboard = us_host_keyboard();
3761        keyboard.update_key(37, true).unwrap();
3762        assert!(keyboard.update_key(37, true).is_none());
3763        let control_up = keyboard.update_key(37, false).unwrap();
3764        assert_eq!(control_up.depressed, 0);
3765    }
3766
3767    #[test]
3768    fn host_keyboard_keeps_lock_across_keymap_changes() {
3769        let text = crate::wayland::keymap::compile_rmlvo("", "", "us", "", "").unwrap();
3770        let mut keyboard = us_host_keyboard();
3771        keyboard.update_key(66, true).unwrap();
3772        let locked = keyboard.update_key(66, false).unwrap();
3773        assert_ne!(locked.locked, 0);
3774        // A keymap swap must not drop the lock; the modifiers() that follows would
3775        // otherwise clear CapsLock on the host.
3776        let after_keymap = keyboard.set_keymap(&text).unwrap();
3777        assert_ne!(after_keymap.locked, 0);
3778    }
3779
3780    #[test]
3781    fn host_keyboard_preserves_pressed_modifiers_across_keymap_changes() {
3782        let text = crate::wayland::keymap::compile_rmlvo("", "", "us", "", "").unwrap();
3783        let mut keyboard = us_host_keyboard();
3784        keyboard.update_key(37, true).unwrap();
3785        let after_keymap = keyboard.set_keymap(&text).unwrap();
3786        assert_ne!(after_keymap.depressed, 0);
3787        let control_up = keyboard.update_key(37, false).unwrap();
3788        assert_eq!(control_up.depressed, 0);
3789    }
3790
3791    #[test]
3792    fn host_keyboard_keeps_control_active_until_both_keys_are_released() {
3793        let mut keyboard = us_host_keyboard();
3794        keyboard.update_key(37, true).unwrap();
3795        keyboard.update_key(105, true).unwrap();
3796        let after_left_release = keyboard.update_key(37, false).unwrap();
3797        assert_ne!(after_left_release.depressed, 0);
3798        let after_right_release = keyboard.update_key(105, false).unwrap();
3799        assert_eq!(after_right_release.depressed, 0);
3800    }
3801
3802    #[test]
3803    fn host_keyboard_ignores_repeated_lock_key_down() {
3804        let mut keyboard = us_host_keyboard();
3805        keyboard.update_key(66, true).unwrap();
3806        assert!(keyboard.update_key(66, true).is_none());
3807        let caps_lock = keyboard.update_key(66, false).unwrap();
3808        assert_eq!(caps_lock.depressed, 0);
3809        assert_ne!(caps_lock.locked, 0);
3810    }
3811
3812    /// The shm-format table maps each announced format to the right BGRA conversion. The
3813    /// wl_shm definitions are little-endian, so `Bgr888` (`[23:0] B:G:R`) is red-first in memory and
3814    /// must swap R/B, while `Rgb888` (`[23:0] R:G:B`) is already blue-first and copies straight.
3815    #[test]
3816    fn shm_layout_matches_wl_shm_definitions() {
3817        assert_eq!(shm_src_layout(wl_shm::Format::Bgr888 as u32), (3, true));
3818        assert_eq!(shm_src_layout(wl_shm::Format::Rgb888 as u32), (3, false));
3819        assert_eq!(shm_src_layout(wl_shm::Format::Xbgr8888 as u32), (4, true));
3820        assert_eq!(shm_src_layout(wl_shm::Format::Abgr8888 as u32), (4, true));
3821        assert_eq!(shm_src_layout(wl_shm::Format::Xrgb8888 as u32), (4, false));
3822        assert_eq!(shm_src_layout(wl_shm::Format::Argb8888 as u32), (4, false));
3823    }
3824
3825    /// A known pixel round-trips to BGRA. The logical color is R=0x11, G=0x22, B=0x33; in a
3826    /// `Bgr888` buffer its little-endian bytes are R,G,B (0x11,0x22,0x33) and the destination must be
3827    /// B,G,R,A (0x33,0x22,0x11,0xff). Without the R/B swap this path produced 0x11,0x22,0x33 —
3828    /// red and blue transposed.
3829    #[test]
3830    fn bgr888_known_pixel_swaps_red_and_blue() {
3831        let (bpp, swap) = shm_src_layout(wl_shm::Format::Bgr888 as u32);
3832        let src = [0x11u8, 0x22, 0x33];
3833        let mut dst = [0u8; 4];
3834        convert_shm_row(&src, &mut dst, bpp, swap);
3835        assert_eq!(dst, [0x33, 0x22, 0x11, 0xff]);
3836    }
3837
3838    /// The same logical color in an `Rgb888` buffer is already B,G,R in memory
3839    /// (0x33,0x22,0x11), so it copies straight to B,G,R,A with an opaque alpha and no swap.
3840    #[test]
3841    fn rgb888_known_pixel_copies_straight() {
3842        let (bpp, swap) = shm_src_layout(wl_shm::Format::Rgb888 as u32);
3843        let src = [0x33u8, 0x22, 0x11];
3844        let mut dst = [0u8; 4];
3845        convert_shm_row(&src, &mut dst, bpp, swap);
3846        assert_eq!(dst, [0x33, 0x22, 0x11, 0xff]);
3847    }
3848
3849    /// The 32-bit `Xbgr8888` source (R,G,B,x in memory) swaps R/B and preserves the fourth
3850    /// byte, and `Xrgb8888` (B,G,R,x) copies straight.
3851    fn convert(format: wl_shm::Format, src: &[u8]) -> [u8; 4] {
3852        let (bpp, swap) = shm_src_layout(format as u32);
3853        let mut dst = [0u8; 4];
3854        convert_shm_row(src, &mut dst, bpp, swap);
3855        dst
3856    }
3857
3858    #[test]
3859    fn four_byte_formats_convert_to_bgra() {
3860        assert_eq!(
3861            convert(wl_shm::Format::Xbgr8888, &[0x11, 0x22, 0x33, 0x44]),
3862            [0x33, 0x22, 0x11, 0x44]
3863        );
3864        assert_eq!(
3865            convert(wl_shm::Format::Xrgb8888, &[0x33, 0x22, 0x11, 0x44]),
3866            [0x33, 0x22, 0x11, 0x44]
3867        );
3868    }
3869
3870    /// The layout ledger the calloop polls: a request is unanswered until the control
3871    /// thread decides an epoch at or past it, the newest verdict stands for every
3872    /// older request (the layout map is cumulative), and a stale verdict arriving
3873    /// after a newer one cannot overwrite it.
3874    #[test]
3875    fn layout_ledger_answers_by_epoch() {
3876        let mut ledger = LayoutLedger::default();
3877        let first = ledger.issue();
3878        let second = ledger.issue();
3879        assert_eq!((first, second), (1, 2));
3880        assert_eq!(ledger.outcome(first), None);
3881        assert_eq!(ledger.outcome(second), None);
3882
3883        ledger.decide(first, true);
3884        assert_eq!(ledger.outcome(first), Some(true));
3885        assert_eq!(ledger.outcome(second), None);
3886
3887        // The control thread coalesces queued requests and answers only the newest;
3888        // that verdict covers the older epoch too.
3889        let third = ledger.issue();
3890        ledger.decide(third, false);
3891        assert_eq!(ledger.outcome(second), Some(false));
3892        assert_eq!(ledger.outcome(third), Some(false));
3893
3894        // A late verdict for an older epoch does not roll the ledger back.
3895        ledger.decide(second, true);
3896        assert_eq!(ledger.outcome(third), Some(false));
3897        let fourth = ledger.issue();
3898        assert_eq!(ledger.outcome(fourth), None);
3899    }
3900}