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