1use 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;
105const SLOTS: usize = 2;
108
109pub struct HostFrame {
111 pub(crate) generation: u64,
112 pub(crate) slot: usize,
113 pub dmabuf: Option<Dmabuf>,
116 pub cpu: Option<HostCpuFrame>,
119 pub width: i32,
120 pub height: i32,
121 pub damage: Vec<Rectangle<i32, Physical>>,
122 pub stamp_ns: i64,
124}
125
126pub(crate) fn now_ns() -> i64 {
128 let mut t = libc::timespec {
129 tv_sec: 0,
130 tv_nsec: 0,
131 };
132 unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut t) };
133 t.tv_sec * 1_000_000_000 + t.tv_nsec
134}
135
136pub(crate) fn trace() -> bool {
138 static ON: std::sync::LazyLock<bool> =
139 std::sync::LazyLock::new(|| std::env::var_os("PIXELFLUX_HOST_TRACE").is_some());
140 *ON
141}
142
143pub struct HostCpuFrame {
145 pub(crate) map: Arc<dyn AsRef<[u8]> + Send + Sync>,
146 pub(crate) offset: usize,
148 pub(crate) stride: usize,
149 pub(crate) format: u32,
150}
151
152fn shm_src_layout(format: u32) -> (usize, bool) {
165 match format {
166 f if f == wl_shm::Format::Xbgr8888 as u32 || f == wl_shm::Format::Abgr8888 as u32 => {
167 (4, true)
168 }
169 f if f == wl_shm::Format::Bgr888 as u32 => (3, true),
170 f if f == wl_shm::Format::Rgb888 as u32 => (3, false),
171 _ => (4, false),
172 }
173}
174
175fn convert_shm_row(src: &[u8], dst: &mut [u8], src_bpp: usize, swap_rb: bool) {
179 match (src_bpp, swap_rb) {
180 (4, false) => {
181 let n = dst.len().min(src.len());
182 dst[..n].copy_from_slice(&src[..n]);
183 }
184 (4, true) => {
185 for (d, s) in dst
186 .as_chunks_mut::<4>()
187 .0
188 .iter_mut()
189 .zip(src.as_chunks::<4>().0)
190 {
191 d[0] = s[2];
192 d[1] = s[1];
193 d[2] = s[0];
194 d[3] = s[3];
195 }
196 }
197 (_, true) => {
200 for (d, s) in dst
201 .as_chunks_mut::<4>()
202 .0
203 .iter_mut()
204 .zip(src.as_chunks::<3>().0)
205 {
206 d.copy_from_slice(&[s[2], s[1], s[0], 0xff]);
207 }
208 }
209 (_, false) => {
210 for (d, s) in dst
211 .as_chunks_mut::<4>()
212 .0
213 .iter_mut()
214 .zip(src.as_chunks::<3>().0)
215 {
216 d.copy_from_slice(&[s[0], s[1], s[2], 0xff]);
217 }
218 }
219 }
220}
221
222impl HostCpuFrame {
223 pub fn write_bgra(&self, w: i32, h: i32, dst: &mut [u8]) {
227 let row = (w * 4) as usize;
228 let (src_bpp, swap_rb) = shm_src_layout(self.format);
229 let bytes: &[u8] = (*self.map).as_ref();
230 let bytes = &bytes[self.offset.min(bytes.len())..];
231 for y in 0..h as usize {
232 let src_start = y * self.stride;
233 let src_end = (src_start + src_bpp * w as usize).min(bytes.len());
234 if src_start >= src_end || (y + 1) * row > dst.len() {
235 break;
236 }
237 let src_row = &bytes[src_start..src_end];
238 let dst_row = &mut dst[y * row..(y + 1) * row];
239 convert_shm_row(src_row, dst_row, src_bpp, swap_rb);
240 }
241 }
242}
243
244#[derive(Clone, Copy, Default)]
248struct LayoutSlot {
249 want: (i32, i32),
250 pos: (i32, i32),
251 active: bool,
252 zero_copy: bool,
255 paint_cursor: bool,
258 fps_milli: u32,
260}
261
262#[derive(Clone, Copy, PartialEq, Eq)]
267struct Want {
268 size: (i32, i32),
269 zero_copy: bool,
270 paint_cursor: bool,
271 fps_milli: u32,
272}
273
274enum ToHost {
275 Start {
276 width: i32,
277 height: i32,
278 zero_copy: bool,
279 paint_cursor: bool,
280 fps_milli: u32,
281 },
282 Release { generation: u64, slot: usize },
285 Idle,
287}
288
289enum CtrlMsg {
290 Apply { epoch: u64, slots: Vec<LayoutSlot> },
291}
292
293#[derive(Clone)]
297pub(crate) struct FrameSink {
298 pub(crate) tx: Sender<HostFrame>,
299 pub(crate) queued: Arc<AtomicI64>,
300 pub(crate) wake: smithay::reexports::calloop::channel::Sender<usize>,
301 pub(crate) index: usize,
302}
303
304impl FrameSink {
305 pub(crate) fn send(&self, frame: HostFrame) -> bool {
307 if self.tx.send(frame).is_err() {
308 return false;
309 }
310 self.queued.fetch_add(1, Ordering::AcqRel);
311 let _ = self.wake.send(self.index);
312 true
313 }
314}
315
316#[derive(Default)]
323struct LayoutLedger {
324 epoch: u64,
325 decided: u64,
326 realized: bool,
327}
328
329impl LayoutLedger {
330 fn issue(&mut self) -> u64 {
332 self.epoch += 1;
333 self.epoch
334 }
335
336 fn decide(&mut self, epoch: u64, realized: bool) {
338 if epoch > self.decided {
339 self.decided = epoch;
340 self.realized = realized;
341 }
342 }
343
344 fn outcome(&self, epoch: u64) -> Option<bool> {
346 (self.decided >= epoch).then_some(self.realized)
347 }
348}
349
350pub const LAYOUT_DEADLINE: Duration = Duration::from_secs(5);
353
354#[derive(Default)]
359struct CtrlState {
360 seat: Option<wl_seat::WlSeat>,
361 vk_mgr: Option<ZwpVirtualKeyboardManagerV1>,
362 vptr_mgr: Option<ZwlrVirtualPointerManagerV1>,
363 has_screencopy: bool,
364 has_ext_capture: bool,
365 has_ext_source: bool,
366 outputs: Vec<(wl_output::WlOutput, Option<String>)>,
367 sizes: Arc<Mutex<Vec<(i32, i32)>>>,
371 positions: Vec<(i32, i32)>,
373 scales: Vec<i32>,
374 order: Vec<usize>,
377 output_mgr: Option<ZwlrOutputManagerV1>,
378 heads: Vec<(ZwlrOutputHeadV1, Option<String>)>,
379 om_serial: Option<u32>,
380 cfg_result: Option<bool>,
381 cfg_canceled: bool,
382 sync_done: bool,
383}
384
385fn output_order_key(name: Option<&String>, registry_idx: usize) -> (bool, String, u64, usize) {
389 match name {
390 Some(n) => {
391 let digits_at = n
392 .rfind(|c: char| !c.is_ascii_digit())
393 .map(|i| i + 1)
394 .unwrap_or(0);
395 let num = n[digits_at..].parse::<u64>().unwrap_or(0);
396 (false, n[..digits_at].to_string(), num, registry_idx)
397 }
398 None => (true, String::new(), 0, registry_idx),
399 }
400}
401
402impl SyncState for CtrlState {
403 fn sync_done_mut(&mut self) -> &mut bool {
404 &mut self.sync_done
405 }
406}
407impl_sync_callback!(CtrlState);
408
409impl Dispatch<wl_registry::WlRegistry, ()> for CtrlState {
410 fn event(
411 state: &mut Self,
412 registry: &wl_registry::WlRegistry,
413 event: wl_registry::Event,
414 _: &(),
415 _: &Connection,
416 qh: &QueueHandle<Self>,
417 ) {
418 if let wl_registry::Event::Global {
419 name,
420 interface,
421 version,
422 } = event
423 {
424 match interface.as_str() {
425 "wl_seat" if state.seat.is_none() => {
426 state.seat = Some(registry.bind(name, 1, qh, ()))
427 }
428 "zwp_virtual_keyboard_manager_v1" => {
429 state.vk_mgr = Some(registry.bind(name, 1, qh, ()))
430 }
431 "zwlr_virtual_pointer_manager_v1" => {
432 state.vptr_mgr = Some(registry.bind(name, version.min(2), qh, ()))
433 }
434 "zwlr_screencopy_manager_v1" if version >= 3 => state.has_screencopy = true,
435 "ext_image_copy_capture_manager_v1" => state.has_ext_capture = true,
436 "ext_output_image_capture_source_manager_v1" => state.has_ext_source = true,
437 "wl_output" => {
438 let idx = state.outputs.len();
439 let out = registry.bind(name, version.min(4), qh, idx);
440 state.outputs.push((out, None));
441 }
442 "zwlr_output_manager_v1" => state.output_mgr = Some(registry.bind(name, 1, qh, ())),
443 _ => {}
444 }
445 }
446 }
447}
448
449macro_rules! impl_output_name {
450 ($t:ty) => {
451 impl Dispatch<wl_output::WlOutput, usize> for $t {
452 fn event(
453 state: &mut Self,
454 _: &wl_output::WlOutput,
455 event: wl_output::Event,
456 idx: &usize,
457 _: &Connection,
458 _: &QueueHandle<Self>,
459 ) {
460 if let wl_output::Event::Name { name } = event {
461 if let Some(o) = state.outputs.get_mut(*idx) {
462 o.1 = Some(name);
463 }
464 }
465 }
466 }
467 };
468}
469impl Dispatch<wl_output::WlOutput, usize> for CtrlState {
470 fn event(
471 state: &mut Self,
472 _: &wl_output::WlOutput,
473 event: wl_output::Event,
474 idx: &usize,
475 _: &Connection,
476 _: &QueueHandle<Self>,
477 ) {
478 match event {
479 wl_output::Event::Name { name } => {
480 if let Some(o) = state.outputs.get_mut(*idx) {
481 o.1 = Some(name);
482 }
483 }
484 wl_output::Event::Mode {
485 flags,
486 width,
487 height,
488 ..
489 } if flags
490 .into_result()
491 .is_ok_and(|f| f.contains(wl_output::Mode::Current)) =>
492 {
493 let mut sizes = state.sizes.lock().unwrap();
494 if sizes.len() <= *idx {
495 sizes.resize(*idx + 1, (0, 0));
496 }
497 sizes[*idx] = (width, height);
498 }
499 wl_output::Event::Geometry { x, y, .. } => {
500 if state.positions.len() <= *idx {
501 state.positions.resize(*idx + 1, (0, 0));
502 }
503 state.positions[*idx] = (x, y);
504 }
505 wl_output::Event::Scale { factor } => {
506 if state.scales.len() <= *idx {
507 state.scales.resize(*idx + 1, 1);
508 }
509 state.scales[*idx] = factor.max(1);
510 }
511 _ => {}
512 }
513 }
514}
515
516delegate_noop!(CtrlState: ignore wl_seat::WlSeat);
517delegate_noop!(CtrlState: ZwpVirtualKeyboardManagerV1);
518delegate_noop!(CtrlState: ZwpVirtualKeyboardV1);
519delegate_noop!(CtrlState: ZwlrVirtualPointerManagerV1);
520delegate_noop!(CtrlState: ZwlrVirtualPointerV1);
521delegate_noop!(CtrlState: ignore ZwlrOutputModeV1);
522delegate_noop!(CtrlState: ZwlrOutputConfigurationHeadV1);
523
524impl Dispatch<ZwlrOutputManagerV1, ()> for CtrlState {
525 fn event(
526 state: &mut Self,
527 _: &ZwlrOutputManagerV1,
528 event: zwlr_output_manager_v1::Event,
529 _: &(),
530 _: &Connection,
531 _: &QueueHandle<Self>,
532 ) {
533 match event {
534 zwlr_output_manager_v1::Event::Head { head } => state.heads.push((head, None)),
535 zwlr_output_manager_v1::Event::Done { serial } => state.om_serial = Some(serial),
536 _ => {}
537 }
538 }
539
540 wayland_client::event_created_child!(CtrlState, ZwlrOutputManagerV1, [
541 zwlr_output_manager_v1::EVT_HEAD_OPCODE => (ZwlrOutputHeadV1, ()),
542 ]);
543}
544
545impl Dispatch<ZwlrOutputHeadV1, ()> for CtrlState {
546 fn event(
547 state: &mut Self,
548 head: &ZwlrOutputHeadV1,
549 event: zwlr_output_head_v1::Event,
550 _: &(),
551 _: &Connection,
552 _: &QueueHandle<Self>,
553 ) {
554 match event {
555 zwlr_output_head_v1::Event::Name { name } => {
556 if let Some(h) = state.heads.iter_mut().find(|(h, _)| h.id() == head.id()) {
557 h.1 = Some(name);
558 }
559 }
560 zwlr_output_head_v1::Event::Finished => {
561 state.heads.retain(|(h, _)| h.id() != head.id());
562 }
563 _ => {}
564 }
565 }
566
567 wayland_client::event_created_child!(CtrlState, ZwlrOutputHeadV1, [
568 zwlr_output_head_v1::EVT_MODE_OPCODE => (ZwlrOutputModeV1, ()),
569 ]);
570}
571
572impl Dispatch<ZwlrOutputConfigurationV1, ()> for CtrlState {
573 fn event(
574 state: &mut Self,
575 _: &ZwlrOutputConfigurationV1,
576 event: zwlr_output_configuration_v1::Event,
577 _: &(),
578 _: &Connection,
579 _: &QueueHandle<Self>,
580 ) {
581 match event {
582 zwlr_output_configuration_v1::Event::Succeeded => state.cfg_result = Some(true),
583 zwlr_output_configuration_v1::Event::Failed => state.cfg_result = Some(false),
584 zwlr_output_configuration_v1::Event::Cancelled => state.cfg_canceled = true,
585 _ => {}
586 }
587 }
588}
589
590#[derive(Default)]
595struct CaptureState {
596 shm: Option<wl_shm::WlShm>,
597 dmabuf: Option<ZwpLinuxDmabufV1>,
598 screencopy: Option<ZwlrScreencopyManagerV1>,
599 ext_capture: Option<ExtImageCopyCaptureManagerV1>,
600 ext_source_mgr: Option<ExtOutputImageCaptureSourceManagerV1>,
601 outputs: Vec<(wl_output::WlOutput, Option<String>)>,
602 announce_dmabuf: Option<(u32, i32, i32)>,
605 announce_shm: Option<(u32, i32, i32, i32)>,
606 buffer_done: bool,
607 damage: Vec<Rectangle<i32, Physical>>,
608 ready: bool,
609 failed: bool,
610 sync_done: bool,
611 params_created: Option<wayland_client::protocol::wl_buffer::WlBuffer>,
614 params_failed: bool,
615 ext_pending_size: Option<(i32, i32)>,
618 ext_pending_shm: Vec<u32>,
619 ext_pending_dma: Vec<(u32, Vec<u64>)>,
620 ext_size: Option<(i32, i32)>,
621 ext_shm_formats: Vec<u32>,
622 ext_dma_formats: Vec<(u32, Vec<u64>)>,
623 ext_serial: u64,
624 ext_stopped: bool,
625 ext_fail_reason: Option<ext_image_copy_capture_frame_v1::FailureReason>,
626}
627
628impl CaptureState {
629 fn reset_frame(&mut self) {
630 self.announce_dmabuf = None;
631 self.announce_shm = None;
632 self.buffer_done = false;
633 self.damage.clear();
634 self.ready = false;
635 self.failed = false;
636 self.ext_fail_reason = None;
637 }
638}
639
640impl SyncState for CaptureState {
641 fn sync_done_mut(&mut self) -> &mut bool {
642 &mut self.sync_done
643 }
644}
645impl_sync_callback!(CaptureState);
646impl_output_name!(CaptureState);
647
648impl Dispatch<wl_registry::WlRegistry, ()> for CaptureState {
649 fn event(
650 state: &mut Self,
651 registry: &wl_registry::WlRegistry,
652 event: wl_registry::Event,
653 _: &(),
654 _: &Connection,
655 qh: &QueueHandle<Self>,
656 ) {
657 if let wl_registry::Event::Global {
658 name,
659 interface,
660 version,
661 } = event
662 {
663 match interface.as_str() {
664 "wl_shm" => state.shm = Some(registry.bind(name, 1, qh, ())),
665 "zwp_linux_dmabuf_v1" if version >= 3 => {
666 state.dmabuf = Some(registry.bind(name, 3, qh, ()))
667 }
668 "zwlr_screencopy_manager_v1" if version >= 3 => {
669 state.screencopy = Some(registry.bind(name, 3, qh, ()))
670 }
671 "ext_image_copy_capture_manager_v1" => {
672 state.ext_capture = Some(registry.bind(name, 1, qh, ()))
673 }
674 "ext_output_image_capture_source_manager_v1" => {
675 state.ext_source_mgr = Some(registry.bind(name, 1, qh, ()))
676 }
677 "wl_output" => {
678 let idx = state.outputs.len();
679 let out = registry.bind(name, version.min(4), qh, idx);
680 state.outputs.push((out, None));
681 }
682 _ => {}
683 }
684 }
685 }
686}
687
688delegate_noop!(CaptureState: ignore wl_shm::WlShm);
689delegate_noop!(CaptureState: ignore wl_shm_pool::WlShmPool);
690delegate_noop!(CaptureState: ignore wayland_client::protocol::wl_buffer::WlBuffer);
691delegate_noop!(CaptureState: ZwlrScreencopyManagerV1);
692delegate_noop!(CaptureState: ExtImageCopyCaptureManagerV1);
693delegate_noop!(CaptureState: ExtOutputImageCaptureSourceManagerV1);
694delegate_noop!(CaptureState: ExtImageCaptureSourceV1);
695
696impl Dispatch<ExtImageCopyCaptureSessionV1, ()> for CaptureState {
697 fn event(
698 state: &mut Self,
699 _: &ExtImageCopyCaptureSessionV1,
700 event: ext_image_copy_capture_session_v1::Event,
701 _: &(),
702 _: &Connection,
703 _: &QueueHandle<Self>,
704 ) {
705 use ext_image_copy_capture_session_v1::Event;
706 match event {
707 Event::BufferSize { width, height } => {
708 state.ext_pending_size = Some((width as i32, height as i32));
709 }
710 Event::ShmFormat { format } => {
711 let raw = match format {
712 WEnum::Value(f) => f as u32,
713 WEnum::Unknown(u) => u,
714 };
715 state.ext_pending_shm.push(raw);
716 }
717 Event::DmabufDevice { .. } => {
718 }
722 Event::DmabufFormat { format, modifiers } => {
723 let mods = modifiers
724 .as_chunks::<8>()
725 .0
726 .iter()
727 .map(|c| u64::from_ne_bytes(*c))
728 .collect();
729 state.ext_pending_dma.push((format, mods));
730 }
731 Event::Done => {
732 state.ext_size = state.ext_pending_size.take();
733 state.ext_shm_formats = std::mem::take(&mut state.ext_pending_shm);
734 state.ext_dma_formats = std::mem::take(&mut state.ext_pending_dma);
735 state.ext_serial += 1;
736 }
737 Event::Stopped => state.ext_stopped = true,
738 _ => {}
739 }
740 }
741}
742
743impl Dispatch<ExtImageCopyCaptureFrameV1, ()> for CaptureState {
744 fn event(
745 state: &mut Self,
746 _: &ExtImageCopyCaptureFrameV1,
747 event: ext_image_copy_capture_frame_v1::Event,
748 _: &(),
749 _: &Connection,
750 _: &QueueHandle<Self>,
751 ) {
752 use ext_image_copy_capture_frame_v1::Event;
753 match event {
754 Event::Damage {
755 x,
756 y,
757 width,
758 height,
759 } => {
760 state
761 .damage
762 .push(Rectangle::new((x, y).into(), (width, height).into()));
763 }
764 Event::Ready => state.ready = true,
765 Event::Failed { reason } => {
766 state.failed = true;
767 if let WEnum::Value(r) = reason {
768 state.ext_fail_reason = Some(r);
769 }
770 }
771 _ => {}
772 }
773 }
774}
775
776impl Dispatch<ZwpLinuxDmabufV1, ()> for CaptureState {
777 fn event(
778 _: &mut Self,
779 _: &ZwpLinuxDmabufV1,
780 _: <ZwpLinuxDmabufV1 as Proxy>::Event,
781 _: &(),
782 _: &Connection,
783 _: &QueueHandle<Self>,
784 ) {
785 }
788}
789
790impl Dispatch<ZwpLinuxBufferParamsV1, ()> for CaptureState {
791 fn event(
792 state: &mut Self,
793 _: &ZwpLinuxBufferParamsV1,
794 event: zwp_linux_buffer_params_v1::Event,
795 _: &(),
796 _: &Connection,
797 _: &QueueHandle<Self>,
798 ) {
799 match event {
800 zwp_linux_buffer_params_v1::Event::Created { buffer } => {
801 state.params_created = Some(buffer);
802 }
803 zwp_linux_buffer_params_v1::Event::Failed => state.params_failed = true,
804 _ => {}
805 }
806 }
807
808 fn event_created_child(
809 _opcode: u16,
810 qhandle: &QueueHandle<Self>,
811 ) -> std::sync::Arc<dyn wayland_client::backend::ObjectData> {
812 qhandle.make_data::<wayland_client::protocol::wl_buffer::WlBuffer, _>(())
813 }
814}
815
816impl Dispatch<ZwlrScreencopyFrameV1, ()> for CaptureState {
817 fn event(
818 state: &mut Self,
819 _: &ZwlrScreencopyFrameV1,
820 event: zwlr_screencopy_frame_v1::Event,
821 _: &(),
822 _: &Connection,
823 _: &QueueHandle<Self>,
824 ) {
825 match event {
826 zwlr_screencopy_frame_v1::Event::Buffer {
827 format: WEnum::Value(f),
828 width,
829 height,
830 stride,
831 } => {
832 state.announce_shm = Some((f as u32, width as i32, height as i32, stride as i32));
833 }
834 zwlr_screencopy_frame_v1::Event::LinuxDmabuf {
835 format,
836 width,
837 height,
838 } => {
839 state.announce_dmabuf = Some((format, width as i32, height as i32));
840 }
841 zwlr_screencopy_frame_v1::Event::BufferDone => state.buffer_done = true,
842 zwlr_screencopy_frame_v1::Event::Damage {
843 x,
844 y,
845 width,
846 height,
847 } => {
848 state.damage.push(Rectangle::new(
849 (x as i32, y as i32).into(),
850 (width as i32, height as i32).into(),
851 ));
852 }
853 zwlr_screencopy_frame_v1::Event::Ready { .. } => state.ready = true,
854 zwlr_screencopy_frame_v1::Event::Failed => state.failed = true,
855 _ => {}
856 }
857 }
858}
859
860enum SlotBuffer {
861 Gpu {
862 _bo: gbm::BufferObject<()>,
863 dmabuf: Dmabuf,
864 wl: wayland_client::protocol::wl_buffer::WlBuffer,
865 },
866 Cpu {
867 _pool: wl_shm_pool::WlShmPool,
868 map: Arc<memmap2::MmapMut>,
869 stride: i32,
870 format: u32,
871 wl: wayland_client::protocol::wl_buffer::WlBuffer,
872 },
873}
874
875impl Drop for SlotBuffer {
880 fn drop(&mut self) {
881 match self {
882 SlotBuffer::Gpu { wl, .. } => wl.destroy(),
883 SlotBuffer::Cpu { _pool, wl, .. } => {
884 wl.destroy();
885 _pool.destroy();
886 }
887 }
888 }
889}
890
891struct OutputHandle {
893 to_thread: Sender<ToHost>,
894 wake: OwnedFd,
895 frames: Receiver<HostFrame>,
896 queued: Arc<AtomicI64>,
899 retained: Mutex<Option<HostFrame>>,
902 name: Option<String>,
903}
904
905impl OutputHandle {
906 fn send(&self, msg: ToHost) {
907 let _ = self.to_thread.send(msg);
908 wake_write(self.wake.as_raw_fd());
909 }
910}
911
912#[derive(Clone, Copy)]
913struct SerializedMods {
914 depressed: u32,
915 latched: u32,
916 locked: u32,
917 group: u32,
918}
919
920#[derive(Default)]
921struct HostKeyboardState {
922 state: Option<xkb::State>,
923 pressed: std::collections::HashSet<u32>,
924}
925
926impl HostKeyboardState {
927 fn set_keymap(&mut self, text: &str) -> Option<SerializedMods> {
928 let keymap = crate::wayland::keymap::compile_keymap(text)?;
929 let carried = self.state.as_ref().map(|s| {
934 (
935 s.serialize_mods(xkb::STATE_MODS_LATCHED),
936 s.serialize_mods(xkb::STATE_MODS_LOCKED),
937 s.serialize_layout(xkb::STATE_LAYOUT_LOCKED),
938 )
939 });
940 let mut state = xkb::State::new(&keymap);
941 for &keycode in &self.pressed {
942 state.update_key(xkb::Keycode::new(keycode), xkb::KeyDirection::Down);
943 }
944 if let Some((latched, locked, layout)) = carried {
945 let depressed = state.serialize_mods(xkb::STATE_MODS_DEPRESSED);
946 state.update_mask(depressed, latched, locked, 0, 0, layout);
947 }
948 let mods = Self::serialize(&state);
949 self.state = Some(state);
950 Some(mods)
951 }
952
953 fn update_key(&mut self, xkb_keycode: u32, pressed: bool) -> Option<SerializedMods> {
954 self.state.as_ref()?;
957 let changed = if pressed {
958 self.pressed.insert(xkb_keycode)
959 } else {
960 self.pressed.remove(&xkb_keycode)
961 };
962 if !changed {
963 return None;
964 }
965 let state = self.state.as_mut()?;
966 state.update_key(
967 xkb::Keycode::new(xkb_keycode),
968 if pressed {
969 xkb::KeyDirection::Down
970 } else {
971 xkb::KeyDirection::Up
972 },
973 );
974 Some(Self::serialize(state))
975 }
976
977 fn base_keysym(&self, xkb_keycode: u32) -> Option<u32> {
979 let state = self.state.as_ref()?;
980 let layout = state.serialize_layout(xkb::STATE_LAYOUT_EFFECTIVE);
981 let sym = state
982 .get_keymap()
983 .key_get_syms_by_level(xkb::Keycode::new(xkb_keycode), layout, 0)
984 .first()?
985 .raw();
986 (sym != 0).then_some(sym)
987 }
988
989 fn serialize(state: &xkb::State) -> SerializedMods {
990 SerializedMods {
991 depressed: state.serialize_mods(xkb::STATE_MODS_DEPRESSED),
992 latched: state.serialize_mods(xkb::STATE_MODS_LATCHED),
993 locked: state.serialize_mods(xkb::STATE_MODS_LOCKED),
994 group: state.serialize_layout(xkb::STATE_LAYOUT_EFFECTIVE),
995 }
996 }
997}
998
999pub struct HostSession {
1001 conn: Connection,
1002 vk: Option<ZwpVirtualKeyboardV1>,
1003 keyboard: Mutex<HostKeyboardState>,
1004 vptr: Option<ZwlrVirtualPointerV1>,
1005 uinput: Option<crate::uinput::Pair>,
1009 portal: Option<Arc<PortalCtl>>,
1011 geoms: Vec<OutputGeom>,
1013 ctrl_tx: Sender<CtrlMsg>,
1014 ctrl_wake: OwnedFd,
1015 outputs: Vec<OutputHandle>,
1016 layout: Mutex<std::collections::BTreeMap<u32, LayoutSlot>>,
1017 order: Vec<usize>,
1019 sizes: Arc<Mutex<Vec<(i32, i32)>>>,
1020 layouts: Arc<Mutex<LayoutLedger>>,
1021 alive: Arc<AtomicBool>,
1022 scroll_carry: Mutex<(f64, f64)>,
1024}
1025
1026impl HostSession {
1027 pub fn connect(
1033 display: &str,
1034 gbm_path: Option<std::path::PathBuf>,
1035 frame_wake: smithay::reexports::calloop::channel::Sender<usize>,
1036 dma_formats: Vec<(u32, Vec<u64>)>,
1037 cursor_tx: std::sync::mpsc::Sender<CursorJob>,
1038 ) -> Result<Self, String> {
1039 let path = socket_path(display).ok_or("XDG_RUNTIME_DIR is unset")?;
1040 let stream = UnixStream::connect(&path).map_err(|e| format!("connect {path}: {e}"))?;
1041 let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
1042 let mut queue = conn.new_event_queue();
1043 let qh = queue.handle();
1044 let _registry = conn.display().get_registry(&qh, ());
1045 let mut state = CtrlState::default();
1046 bounded_roundtrip(&conn, &mut queue, &mut state)?;
1047
1048 let seat = state
1049 .seat
1050 .clone()
1051 .ok_or("host compositor advertises no wl_seat")?;
1052 if state.outputs.is_empty() {
1053 return Err("host compositor has no wl_output".into());
1054 }
1055 let native_capture =
1056 state.has_screencopy || (state.has_ext_capture && state.has_ext_source);
1057 let portal_devices = if state.vk_mgr.is_none() {
1058 portal::DEVICE_KEYBOARD
1059 } else {
1060 0
1061 } | if state.vptr_mgr.is_none() {
1062 portal::DEVICE_POINTER
1063 } else {
1064 0
1065 };
1066 let portal = if !native_capture || portal_devices != 0 {
1067 match PortalSession::probe() {
1068 Ok(()) => Some(PortalCtl::spawn(!native_capture, portal_devices)),
1069 Err(_) if native_capture => None,
1070 Err(why) => {
1071 return Err(format!(
1072 "host compositor offers neither ext-image-copy-capture nor zwlr_screencopy_manager_v1 (v3), and no xdg-desktop-portal answers on the session bus ({why})"
1073 ));
1074 }
1075 }
1076 } else {
1077 None
1078 };
1079 if !native_capture {
1080 println!(
1081 "[HostCapture] host offers no capture protocol; frames come through the xdg-desktop-portal ScreenCast."
1082 );
1083 }
1084
1085 let base_keymap = crate::wayland::vkclient::us_base_text();
1086 let vk = match &state.vk_mgr {
1087 Some(mgr) => {
1088 let vk = mgr.create_virtual_keyboard(&seat, &qh, ());
1089 if let Some(text) = base_keymap {
1092 let mut data = text.as_bytes().to_vec();
1093 data.push(0);
1094 let fd = memfd_with(&data)?;
1095 vk.keymap(KEYMAP_FORMAT_XKB_V1, fd.as_fd(), data.len() as u32);
1096 }
1097 Some(vk)
1098 }
1099 None => {
1100 eprintln!(
1101 "[HostCapture] no zwp_virtual_keyboard_manager_v1: keyboard {}.",
1102 if portal.is_some() {
1103 "goes through the portal by keysym"
1104 } else {
1105 "injection disabled"
1106 }
1107 );
1108 None
1109 }
1110 };
1111 let vptr = match &state.vptr_mgr {
1112 Some(mgr) => Some(mgr.create_virtual_pointer(Some(&seat), &qh, ())),
1113 None => {
1114 eprintln!(
1115 "[HostCapture] no zwlr_virtual_pointer_manager_v1: pointer {}.",
1116 if portal.is_some() {
1117 "goes through the portal"
1118 } else {
1119 "injection disabled"
1120 }
1121 );
1122 None
1123 }
1124 };
1125 bounded_roundtrip(&conn, &mut queue, &mut state)?;
1127
1128 let alive = Arc::new(AtomicBool::new(true));
1129 let mut order: Vec<usize> = (0..state.outputs.len()).collect();
1130 order.sort_by_key(|&i| output_order_key(state.outputs[i].1.as_ref(), i));
1131 let names: Vec<Option<String>> =
1132 order.iter().map(|&i| state.outputs[i].1.clone()).collect();
1133 let geoms: Vec<OutputGeom> = {
1134 let sizes = state.sizes.lock().unwrap();
1135 order
1136 .iter()
1137 .map(|&i| {
1138 let scale = state.scales.get(i).copied().unwrap_or(1).max(1);
1139 let size = sizes.get(i).copied().unwrap_or((0, 0));
1140 OutputGeom {
1141 pos: state.positions.get(i).copied().unwrap_or((0, 0)),
1142 logical: (size.0 / scale, size.1 / scale),
1143 }
1144 })
1145 .collect()
1146 };
1147 state.order = order;
1148 println!(
1149 "[HostCapture] host outputs: {}.",
1150 names
1151 .iter()
1152 .enumerate()
1153 .map(|(i, n)| format!("{i}={}", n.as_deref().unwrap_or("?")))
1154 .collect::<Vec<_>>()
1155 .join(", ")
1156 );
1157
1158 let mut outputs = Vec::new();
1159 for (i, name) in names.iter().cloned().enumerate() {
1160 let (wake_rd, wake_wr) = wake_pipe()?;
1161 let (to_thread, from_main) = std::sync::mpsc::channel::<ToHost>();
1162 let (frame_tx, frames) = std::sync::mpsc::channel::<HostFrame>();
1163 let queued = Arc::new(AtomicI64::new(0));
1164 let sink = FrameSink {
1165 tx: frame_tx,
1166 queued: queued.clone(),
1167 wake: frame_wake.clone(),
1168 index: i,
1169 };
1170 let display = display.to_string();
1171 let expect = name.clone();
1172 let gbm_path = gbm_path.clone();
1173 let alive = alive.clone();
1174 let portal_ctl = portal.clone().filter(|_| !native_capture);
1175 let (dma_formats, cursor_tx, geoms) =
1176 (dma_formats.clone(), cursor_tx.clone(), geoms.clone());
1177 std::thread::Builder::new()
1178 .name(format!("pf-host-cap{i}"))
1179 .spawn(move || {
1180 crate::boost_thread_priority(-10);
1181 let outcome = match portal_ctl {
1182 Some(ctl) => portal_capture_loop(
1183 i,
1184 ctl,
1185 from_main,
1186 sink,
1187 cursor_tx,
1188 dma_formats,
1189 geoms,
1190 ),
1191 None => {
1192 capture_loop(&display, i, expect, gbm_path, from_main, wake_rd, sink)
1193 }
1194 };
1195 if let Err(e) = outcome {
1196 eprintln!("[HostCapture] output {i} capture ended: {e}");
1197 }
1198 if i == 0 {
1199 alive.store(false, Ordering::Relaxed);
1200 }
1201 })
1202 .map_err(|e| format!("spawn: {e}"))?;
1203 outputs.push(OutputHandle {
1204 to_thread,
1205 wake: wake_wr,
1206 frames,
1207 queued,
1208 retained: Mutex::new(None),
1209 name,
1210 });
1211 }
1212
1213 let (ctrl_tx, ctrl_rx) = std::sync::mpsc::channel::<CtrlMsg>();
1214 let (ctrl_wake_rd, ctrl_wake) = wake_pipe()?;
1215 let order_for_session = state.order.clone();
1216 let sizes = state.sizes.clone();
1217 let layouts = Arc::new(Mutex::new(LayoutLedger::default()));
1218 {
1219 let conn = conn.clone();
1220 let ledger = layouts.clone();
1221 let alive = alive.clone();
1222 std::thread::Builder::new()
1223 .name("pf-host-ctrl".into())
1224 .spawn(move || {
1225 control_loop(conn, queue, state, ctrl_rx, ctrl_wake_rd, ledger);
1226 alive.store(false, Ordering::Relaxed);
1229 })
1230 .map_err(|e| format!("spawn: {e}"))?;
1231 }
1232
1233 let layout = Mutex::new(std::collections::BTreeMap::new());
1234 let mut keyboard = HostKeyboardState::default();
1235 let has_keyboard =
1236 vk.is_some() || portal_devices & portal::DEVICE_KEYBOARD != 0 && portal.is_some();
1237 if has_keyboard
1238 && base_keymap
1239 .and_then(|text| keyboard.set_keymap(text))
1240 .is_none()
1241 {
1242 eprintln!(
1243 "[HostCapture] base keymap unavailable: keys are dropped until selkies uploads its keymap."
1244 );
1245 }
1246 let uinput = crate::uinput::Pair::open();
1247 if uinput.is_some() {
1248 println!("[HostCapture] input goes through kernel uinput devices.");
1249 }
1250 Ok(Self {
1251 conn,
1252 vk,
1253 keyboard: Mutex::new(keyboard),
1254 vptr,
1255 uinput,
1256 portal,
1257 geoms,
1258 ctrl_tx,
1259 ctrl_wake,
1260 outputs,
1261 layout,
1262 order: order_for_session,
1263 sizes,
1264 layouts,
1265 alive,
1266 scroll_carry: Mutex::new((0.0, 0.0)),
1267 })
1268 }
1269
1270 pub fn output_count(&self) -> usize {
1271 self.outputs.len()
1272 }
1273
1274 fn output_index_for(&self, display_id: u32) -> Option<usize> {
1277 let layout = self.layout.lock().unwrap();
1278 let rank = layout
1279 .iter()
1280 .filter(|(_, s)| s.active)
1281 .position(|(id, _)| *id == display_id)?;
1282 (rank < self.outputs.len()).then_some(rank)
1283 }
1284
1285 pub fn has_output_for(&self, display_id: u32) -> bool {
1287 self.output_index_for(display_id).is_some()
1288 }
1289
1290 pub fn display_for_output(&self, index: usize) -> Option<u32> {
1292 let layout = self.layout.lock().unwrap();
1293 layout
1294 .iter()
1295 .filter(|(_, s)| s.active)
1296 .nth(index)
1297 .map(|(id, _)| *id)
1298 }
1299
1300 pub fn has_queued_frame(&self, index: usize) -> bool {
1302 self.outputs
1303 .get(index)
1304 .is_some_and(|h| h.queued.load(Ordering::Acquire) > 0)
1305 }
1306
1307 pub fn set_layout(&self, display_id: u32, x: i32, y: i32) {
1310 let mut layout = self.layout.lock().unwrap();
1311 layout.entry(display_id).or_default().pos = (x, y);
1312 }
1313
1314 pub fn current_output_size(&self, display_id: u32) -> Option<(i32, i32)> {
1318 let rank = self.output_index_for(display_id)?;
1319 let registry_idx = *self.order.get(rank)?;
1320 let size = *self.sizes.lock().unwrap().get(registry_idx)?;
1321 (size.0 > 0 && size.1 > 0).then_some(size)
1322 }
1323
1324 fn send_apply(&self, slots: Vec<LayoutSlot>) -> u64 {
1326 let epoch = self.layouts.lock().unwrap().issue();
1327 let _ = self.ctrl_tx.send(CtrlMsg::Apply { epoch, slots });
1328 wake_write(self.ctrl_wake.as_raw_fd());
1329 epoch
1330 }
1331
1332 pub fn layout_outcome(&self, epoch: u64) -> Option<bool> {
1340 self.layouts.lock().unwrap().outcome(epoch)
1341 }
1342
1343 pub fn start_capture(
1351 &self,
1352 display_id: u32,
1353 width: i32,
1354 height: i32,
1355 zero_copy: bool,
1356 paint_cursor: bool,
1357 fps: f64,
1358 ) -> u64 {
1359 if let Some(ctl) = &self.portal {
1360 ctl.set_paint(paint_cursor);
1361 }
1362 let assignments = {
1363 let mut layout = self.layout.lock().unwrap();
1364 let slot = layout.entry(display_id).or_default();
1365 slot.want = (width, height);
1366 slot.zero_copy = zero_copy;
1367 slot.paint_cursor = paint_cursor;
1368 slot.fps_milli = fps_milli(fps);
1369 slot.active = true;
1370 let active: Vec<(u32, LayoutSlot)> = layout
1371 .iter()
1372 .filter(|(_, s)| s.active)
1373 .map(|(id, s)| (*id, *s))
1374 .collect();
1375 if active
1376 .iter()
1377 .position(|(id, _)| *id == display_id)
1378 .unwrap_or(usize::MAX)
1379 >= self.outputs.len()
1380 {
1381 eprintln!(
1382 "[HostCapture] display {display_id} has no host output (host has {}); not captured.",
1383 self.outputs.len()
1384 );
1385 }
1386 active
1387 };
1388 let mut by_output: Vec<LayoutSlot> = Vec::new();
1391 for (rank, (_, slot)) in assignments.iter().enumerate() {
1392 if rank >= self.outputs.len() {
1393 break;
1394 }
1395 let size_mismatch = self.outputs[rank]
1399 .retained
1400 .lock()
1401 .unwrap()
1402 .as_ref()
1403 .is_some_and(|f| f.width != slot.want.0 || f.height != slot.want.1);
1404 if size_mismatch {
1405 self.drop_buffered_frames(rank);
1406 }
1407 self.outputs[rank].send(slot.start());
1408 by_output.push(*slot);
1409 }
1410 self.send_apply(by_output)
1411 }
1412
1413 pub fn set_buffer_type(&self, display_id: u32, zero_copy: bool) {
1419 let target = {
1420 let mut layout = self.layout.lock().unwrap();
1421 let slot = layout.entry(display_id).or_default();
1422 if slot.zero_copy == zero_copy {
1423 return;
1424 }
1425 slot.zero_copy = zero_copy;
1426 let (start, active) = (slot.start(), slot.active);
1427 let rank = layout
1428 .iter()
1429 .filter(|(_, s)| s.active)
1430 .position(|(id, _)| *id == display_id);
1431 match rank {
1432 Some(r) if active && r < self.outputs.len() => (r, start),
1433 _ => return,
1434 }
1435 };
1436 let (idx, start) = target;
1437 self.drop_buffered_frames(idx);
1440 self.outputs[idx].send(start);
1441 }
1442
1443 pub fn set_fps(&self, display_id: u32, fps: f64) {
1446 let target = {
1447 let mut layout = self.layout.lock().unwrap();
1448 let slot = layout.entry(display_id).or_default();
1449 let milli = fps_milli(fps);
1450 if slot.fps_milli == milli {
1451 return;
1452 }
1453 slot.fps_milli = milli;
1454 let (start, active) = (slot.start(), slot.active);
1455 let rank = layout
1456 .iter()
1457 .filter(|(_, s)| s.active)
1458 .position(|(id, _)| *id == display_id);
1459 match rank {
1460 Some(r) if active && r < self.outputs.len() => (r, start),
1461 _ => return,
1462 }
1463 };
1464 self.outputs[target.0].send(target.1);
1465 }
1466
1467 pub fn set_cursor_painting(&self, paint_cursor: bool) {
1471 if let Some(ctl) = &self.portal {
1472 ctl.set_paint(paint_cursor);
1473 }
1474 let targets: Vec<(usize, LayoutSlot)> = {
1475 let mut layout = self.layout.lock().unwrap();
1476 let mut out = Vec::new();
1477 for (rank, (_, slot)) in layout.iter_mut().filter(|(_, s)| s.active).enumerate() {
1478 if rank >= self.outputs.len() {
1479 break;
1480 }
1481 if slot.paint_cursor != paint_cursor {
1482 slot.paint_cursor = paint_cursor;
1483 out.push((rank, *slot));
1484 }
1485 }
1486 out
1487 };
1488 for (idx, slot) in targets {
1489 self.outputs[idx].send(slot.start());
1490 }
1491 }
1492
1493 fn drop_buffered_frames(&self, idx: usize) {
1499 let handle = &self.outputs[idx];
1500 if let Some(old) = handle.retained.lock().unwrap().take() {
1501 handle.send(ToHost::Release {
1502 generation: old.generation,
1503 slot: old.slot,
1504 });
1505 }
1506 while let Ok(frame) = handle.frames.try_recv() {
1507 handle.queued.fetch_sub(1, Ordering::AcqRel);
1508 handle.send(ToHost::Release {
1509 generation: frame.generation,
1510 slot: frame.slot,
1511 });
1512 }
1513 }
1514
1515 pub fn idle_output(&self, display_id: u32) {
1520 let Some(idx) = self.output_index_for(display_id) else {
1521 self.layout
1522 .lock()
1523 .unwrap()
1524 .entry(display_id)
1525 .or_default()
1526 .active = false;
1527 return;
1528 };
1529 self.layout
1530 .lock()
1531 .unwrap()
1532 .entry(display_id)
1533 .or_default()
1534 .active = false;
1535 self.outputs[idx].send(ToHost::Idle);
1536 self.drop_buffered_frames(idx);
1537 }
1538
1539 pub fn alive(&self) -> bool {
1543 self.alive.load(Ordering::Relaxed) && !self.portal.as_ref().is_some_and(|c| c.dead())
1544 }
1545
1546 pub fn release_frame(&self, display_id: u32, frame: HostFrame) {
1548 if let Some(idx) = self.output_index_for(display_id) {
1549 self.outputs[idx].send(ToHost::Release {
1550 generation: frame.generation,
1551 slot: frame.slot,
1552 });
1553 }
1554 }
1555
1556 pub fn try_take_frame(&self, display_id: u32) -> Option<HostFrame> {
1559 let handle = self.outputs.get(self.output_index_for(display_id)?)?;
1560 let mut newest: Option<HostFrame> = None;
1561 while let Ok(frame) = handle.frames.try_recv() {
1562 handle.queued.fetch_sub(1, Ordering::AcqRel);
1563 if let Some(stale) = newest.replace(frame) {
1564 handle.send(ToHost::Release {
1565 generation: stale.generation,
1566 slot: stale.slot,
1567 });
1568 }
1569 }
1570 newest
1571 }
1572
1573 pub fn retain_frame(&self, display_id: u32, frame: HostFrame) {
1576 let Some(handle) = self
1577 .output_index_for(display_id)
1578 .and_then(|i| self.outputs.get(i))
1579 else {
1580 return;
1581 };
1582 let old = handle.retained.lock().unwrap().replace(frame);
1583 if let Some(old) = old {
1584 handle.send(ToHost::Release {
1585 generation: old.generation,
1586 slot: old.slot,
1587 });
1588 }
1589 }
1590
1591 pub fn with_retained<R>(&self, display_id: u32, f: impl FnOnce(Option<&HostFrame>) -> R) -> R {
1593 match self
1594 .output_index_for(display_id)
1595 .and_then(|i| self.outputs.get(i))
1596 {
1597 Some(handle) => f(handle.retained.lock().unwrap().as_ref()),
1598 None => f(None),
1599 }
1600 }
1601
1602 pub fn set_keymap(&self, text: &str) {
1605 let mut keyboard = self.keyboard.lock().unwrap();
1606 let Some(mods) = keyboard.set_keymap(text) else {
1607 eprintln!("[HostCapture] keymap state update failed");
1608 return;
1609 };
1610 let Some(vk) = &self.vk else { return };
1611 let mut data = text.as_bytes().to_vec();
1612 data.push(0);
1613 let fd = match memfd_with(&data) {
1614 Ok(fd) => fd,
1615 Err(e) => {
1616 eprintln!("[HostCapture] keymap upload failed: {e}");
1617 return;
1618 }
1619 };
1620 vk.keymap(KEYMAP_FORMAT_XKB_V1, fd.as_fd(), data.len() as u32);
1621 vk.modifiers(mods.depressed, mods.latched, mods.locked, mods.group);
1622 let _ = self.conn.flush();
1623 }
1624
1625 pub fn key(&self, xkb_keycode: u32, pressed: bool) {
1630 if xkb_keycode < 8 {
1631 return;
1632 }
1633 let mut keyboard = self.keyboard.lock().unwrap();
1634 let Some(mods) = keyboard.update_key(xkb_keycode, pressed) else {
1635 return;
1636 };
1637 let sym = keyboard.base_keysym(xkb_keycode);
1638 drop(keyboard);
1639 if let Some(live) = self.portal_ei() {
1640 live.ei.as_ref().unwrap().key(xkb_keycode, sym, pressed);
1641 return;
1642 }
1643 if let Some(vk) = &self.vk {
1644 vk.key(0, xkb_keycode - 8, if pressed { 1 } else { 0 });
1645 vk.modifiers(mods.depressed, mods.latched, mods.locked, mods.group);
1646 let _ = self.conn.flush();
1647 return;
1648 }
1649 if let Some(u) = &self.uinput {
1650 let _ = u.keyboard.emit(
1651 crate::uinput::EV_KEY,
1652 (xkb_keycode - 8) as u16,
1653 if pressed { 1 } else { 0 },
1654 );
1655 return;
1656 }
1657 self.with_portal(|p| match sym {
1658 Some(sym) => p.keysym(sym as i32, pressed),
1659 None => p.keycode(xkb_keycode as i32 - 8, pressed),
1660 });
1661 }
1662
1663 fn with_portal(&self, f: impl FnOnce(&PortalSession)) {
1665 if let Some(live) = self.portal.as_ref().and_then(|c| c.live()) {
1666 f(&live.session);
1667 }
1668 }
1669
1670 fn portal_ei(&self) -> Option<Arc<PortalLive>> {
1673 self.portal
1674 .as_ref()
1675 .and_then(|c| c.live())
1676 .filter(|l| l.ei.as_ref().is_some_and(|e| e.alive()))
1677 }
1678
1679 fn extent(&self) -> (i32, i32) {
1682 let layout = self.layout.lock().unwrap();
1683 let mut w = 0;
1684 let mut h = 0;
1685 for (rank, (_, slot)) in layout.iter().filter(|(_, s)| s.active).enumerate() {
1686 if rank >= self.outputs.len() {
1687 break;
1688 }
1689 w = w.max(slot.pos.0 + slot.want.0);
1690 h = h.max(slot.pos.1 + slot.want.1);
1691 }
1692 (w, h)
1693 }
1694
1695 fn portal_target(&self, x: f64, y: f64) -> Option<(u32, f64, f64)> {
1699 let live = self.portal.as_ref()?.live()?;
1700 let slots: Vec<LayoutSlot> = {
1701 let layout = self.layout.lock().unwrap();
1702 layout
1703 .values()
1704 .filter(|s| s.active)
1705 .copied()
1706 .take(self.outputs.len())
1707 .collect()
1708 };
1709 let rank = nearest_slot(&slots, x, y)?;
1710 let assignment = assign_streams(&live.session.streams, &self.geoms);
1711 let stream = &live.session.streams[(*assignment.get(rank)?)?];
1712 let slot = &slots[rank];
1713 let (sx, sy) = stream_point(slot, stream.size.unwrap_or(slot.want), x, y);
1714 Some((stream.node_id, sx, sy))
1715 }
1716
1717 pub fn pointer_motion_abs(&self, x: f64, y: f64) {
1718 if let Some(live) = self.portal_ei() {
1719 let (w, h) = self.extent();
1720 if w > 0 && h > 0 {
1721 let ei = live.ei.as_ref().unwrap();
1722 ei.pointer_motion_abs(x.clamp(0.0, (w - 1) as f64), y.clamp(0.0, (h - 1) as f64));
1723 }
1724 return;
1725 }
1726 let Some(vp) = &self.vptr else {
1727 if let Some(u) = &self.uinput {
1728 let (w, h) = self.extent();
1729 if w > 1 && h > 1 {
1730 let scale = |v: f64, span: i32| {
1731 (v.clamp(0.0, (span - 1) as f64) / (span - 1) as f64
1732 * crate::uinput::ABS_RANGE as f64)
1733 .round() as i32
1734 };
1735 let _ =
1736 u.pointer
1737 .emit(crate::uinput::EV_ABS, crate::uinput::ABS_X, scale(x, w));
1738 let _ =
1739 u.pointer
1740 .emit(crate::uinput::EV_ABS, crate::uinput::ABS_Y, scale(y, h));
1741 }
1742 } else if let Some((node, sx, sy)) = self.portal_target(x, y) {
1743 self.with_portal(|p| p.pointer_motion_abs(node, sx, sy));
1744 }
1745 return;
1746 };
1747 let (w, h) = self.extent();
1748 if w <= 0 || h <= 0 {
1749 return;
1750 }
1751 let cx = x.clamp(0.0, (w - 1) as f64) as u32;
1752 let cy = y.clamp(0.0, (h - 1) as f64) as u32;
1753 vp.motion_absolute(0, cx, cy, w as u32, h as u32);
1754 vp.frame();
1755 let _ = self.conn.flush();
1756 }
1757
1758 pub fn pointer_motion_rel(&self, dx: f64, dy: f64) {
1764 if dx == 0.0 && dy == 0.0 {
1765 return;
1766 }
1767 if let Some(live) = self.portal_ei() {
1768 live.ei.as_ref().unwrap().pointer_motion(dx, dy);
1769 return;
1770 }
1771 let Some(vp) = &self.vptr else {
1772 if let Some(u) = &self.uinput {
1773 let _ = u.pointer.emit(
1774 crate::uinput::EV_REL,
1775 crate::uinput::REL_X,
1776 dx.round() as i32,
1777 );
1778 let _ = u.pointer.emit(
1779 crate::uinput::EV_REL,
1780 crate::uinput::REL_Y,
1781 dy.round() as i32,
1782 );
1783 } else {
1784 self.with_portal(|p| p.pointer_motion(dx, dy));
1785 }
1786 return;
1787 };
1788 vp.motion(0, dx, dy);
1789 vp.frame();
1790 let _ = self.conn.flush();
1791 }
1792
1793 pub fn pointer_button(&self, btn: u32, pressed: bool) {
1794 if let Some(live) = self.portal_ei() {
1795 live.ei
1796 .as_ref()
1797 .unwrap()
1798 .pointer_button(btn as i32, pressed);
1799 return;
1800 }
1801 let Some(vp) = &self.vptr else {
1802 if let Some(u) = &self.uinput {
1803 let _ = u.pointer.emit(
1804 crate::uinput::EV_KEY,
1805 btn as u16,
1806 if pressed { 1 } else { 0 },
1807 );
1808 } else {
1809 self.with_portal(|p| p.pointer_button(btn as i32, pressed));
1810 }
1811 return;
1812 };
1813 vp.button(
1814 0,
1815 btn,
1816 if pressed {
1817 wl_pointer::ButtonState::Pressed
1818 } else {
1819 wl_pointer::ButtonState::Released
1820 },
1821 );
1822 vp.frame();
1823 let _ = self.conn.flush();
1824 }
1825
1826 pub fn pointer_axis(&self, dx: f64, dy: f64) {
1831 if dx == 0.0 && dy == 0.0 {
1832 return;
1833 }
1834 let steps = |value: f64| (value * crate::SCROLL_V120_PER_UNIT / 120.0).round() as i32;
1835 let scroll =
1836 |axis: u32, value: f64, cont: &dyn Fn(f64, f64, bool), disc: &dyn Fn(u32, i32)| {
1837 match steps(value) {
1838 0 => cont(
1839 if axis == 1 { value } else { 0.0 },
1840 if axis == 0 { value } else { 0.0 },
1841 true,
1842 ),
1843 n => disc(axis, n),
1844 }
1845 };
1846 if let Some(live) = self.portal_ei() {
1847 let ei = live.ei.as_ref().unwrap();
1848 for (axis, value) in [(0u32, dy), (1u32, dx)] {
1849 if value != 0.0 {
1850 scroll(axis, value, &|x, y, f| ei.pointer_axis(x, y, f), &|a, n| {
1851 ei.pointer_axis_discrete(a, n)
1852 });
1853 }
1854 }
1855 return;
1856 }
1857 let Some(vp) = &self.vptr else {
1858 if let Some(u) = &self.uinput {
1859 for (code, value, sign) in [
1861 (crate::uinput::REL_WHEEL, dy, -1),
1862 (crate::uinput::REL_HWHEEL, dx, 1),
1863 ] {
1864 match steps(value) {
1865 0 => {}
1866 n => {
1867 let _ = u.pointer.emit(crate::uinput::EV_REL, code, n * sign);
1868 }
1869 }
1870 }
1871 } else {
1872 self.with_portal(|p| {
1873 for (axis, value) in [(0u32, dy), (1u32, dx)] {
1874 if value != 0.0 {
1875 scroll(axis, value, &|x, y, f| p.pointer_axis(x, y, f), &|a, n| {
1876 p.pointer_axis_discrete(a, n)
1877 });
1878 }
1879 }
1880 });
1881 }
1882 return;
1883 };
1884 for (axis, value) in [
1885 (wl_pointer::Axis::VerticalScroll, dy),
1886 (wl_pointer::Axis::HorizontalScroll, dx),
1887 ] {
1888 if value == 0.0 {
1889 continue;
1890 }
1891 let steps = steps(value);
1892 if steps != 0 {
1893 vp.axis_discrete(0, axis, value, steps);
1894 } else {
1895 vp.axis(0, axis, value);
1896 }
1897 vp.axis_source(wl_pointer::AxisSource::Wheel);
1898 }
1899 vp.frame();
1900 let _ = self.conn.flush();
1901 }
1902}
1903
1904const SCROLL_PX_PER_NOTCH: f64 = 100.0;
1907
1908fn scroll_notches(carry: &mut f64, delta: f64) -> i32 {
1910 *carry += delta;
1911 let notches = (*carry / SCROLL_PX_PER_NOTCH).trunc();
1912 *carry -= notches * SCROLL_PX_PER_NOTCH;
1913 notches as i32
1914}
1915
1916impl HostSession {
1917 pub fn pointer_scroll(&self, dx: f64, dy: f64) {
1921 if dx == 0.0 && dy == 0.0 {
1922 return;
1923 }
1924 if let Some(live) = self.portal_ei() {
1925 live.ei.as_ref().unwrap().pointer_axis(dx, dy, false);
1926 return;
1927 }
1928 let Some(vp) = &self.vptr else {
1929 if let Some(u) = &self.uinput {
1930 let mut carry = self.scroll_carry.lock().unwrap();
1931 let (wheel, hwheel) = (
1933 scroll_notches(&mut carry.1, dy),
1934 scroll_notches(&mut carry.0, dx),
1935 );
1936 if wheel != 0 {
1937 let _ = u
1938 .pointer
1939 .emit(crate::uinput::EV_REL, crate::uinput::REL_WHEEL, -wheel);
1940 }
1941 if hwheel != 0 {
1942 let _ =
1943 u.pointer
1944 .emit(crate::uinput::EV_REL, crate::uinput::REL_HWHEEL, hwheel);
1945 }
1946 } else {
1947 self.with_portal(|p| p.pointer_axis(dx, dy, false));
1948 }
1949 return;
1950 };
1951 vp.axis_source(wl_pointer::AxisSource::Finger);
1952 for (axis, value) in [
1953 (wl_pointer::Axis::VerticalScroll, dy),
1954 (wl_pointer::Axis::HorizontalScroll, dx),
1955 ] {
1956 if value != 0.0 {
1957 vp.axis(0, axis, value);
1958 }
1959 }
1960 vp.frame();
1961 let _ = self.conn.flush();
1962 }
1963
1964 pub fn pointer_scroll_end(&self) {
1966 if let Some(live) = self.portal_ei() {
1967 live.ei.as_ref().unwrap().pointer_axis(0.0, 0.0, true);
1968 return;
1969 }
1970 let Some(vp) = &self.vptr else {
1971 if self.uinput.is_some() {
1972 *self.scroll_carry.lock().unwrap() = (0.0, 0.0);
1973 } else {
1974 self.with_portal(|p| p.pointer_axis(0.0, 0.0, true));
1975 }
1976 return;
1977 };
1978 vp.axis_source(wl_pointer::AxisSource::Finger);
1979 vp.axis_stop(0, wl_pointer::Axis::VerticalScroll);
1980 vp.axis_stop(0, wl_pointer::Axis::HorizontalScroll);
1981 vp.frame();
1982 let _ = self.conn.flush();
1983 }
1984}
1985
1986impl Drop for HostSession {
1987 fn drop(&mut self) {
1988 if let Some(ctl) = &self.portal {
1989 ctl.close();
1990 }
1991 }
1992}
1993
1994fn fps_milli(fps: f64) -> u32 {
1996 (fps.max(1.0) * 1000.0).round() as u32
1997}
1998
1999impl LayoutSlot {
2000 fn start(&self) -> ToHost {
2002 ToHost::Start {
2003 width: self.want.0,
2004 height: self.want.1,
2005 zero_copy: self.zero_copy,
2006 paint_cursor: self.paint_cursor,
2007 fps_milli: self.fps_milli,
2008 }
2009 }
2010}
2011
2012fn nearest_slot(slots: &[LayoutSlot], x: f64, y: f64) -> Option<usize> {
2014 slots
2015 .iter()
2016 .enumerate()
2017 .map(|(rank, s)| {
2018 let cx = x.clamp(s.pos.0 as f64, (s.pos.0 + s.want.0).max(s.pos.0) as f64);
2019 let cy = y.clamp(s.pos.1 as f64, (s.pos.1 + s.want.1).max(s.pos.1) as f64);
2020 (rank, (cx - x).powi(2) + (cy - y).powi(2))
2021 })
2022 .min_by(|a, b| a.1.total_cmp(&b.1))
2023 .map(|(rank, _)| rank)
2024}
2025
2026fn stream_point(slot: &LayoutSlot, logical: (i32, i32), x: f64, y: f64) -> (f64, f64) {
2029 let scale = |v: f64, from: i32, to: i32| {
2030 let mapped = if from > 0 {
2031 v * to as f64 / from as f64
2032 } else {
2033 v
2034 };
2035 mapped.clamp(0.0, (to.max(1) - 1) as f64)
2036 };
2037 (
2038 scale(x - slot.pos.0 as f64, slot.want.0, logical.0),
2039 scale(y - slot.pos.1 as f64, slot.want.1, logical.1),
2040 )
2041}
2042
2043pub fn wait_gpu_done(dmabuf: &Dmabuf) {
2048 for fd in dmabuf.handles() {
2049 let mut pfd = libc::pollfd {
2050 fd: fd.as_raw_fd(),
2051 events: libc::POLLIN,
2052 revents: 0,
2053 };
2054 unsafe { libc::poll(&mut pfd, 1, 50) };
2055 }
2056}
2057
2058#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2064pub(crate) struct OutputGeom {
2065 pos: (i32, i32),
2067 logical: (i32, i32),
2069}
2070
2071fn assign_streams(streams: &[PortalStream], geoms: &[OutputGeom]) -> Vec<Option<usize>> {
2075 let mut taken = vec![false; streams.len()];
2076 let mut out = vec![None; geoms.len()];
2077 for (rank, g) in geoms.iter().enumerate() {
2078 if let Some(i) = streams.iter().position(|s| s.position == Some(g.pos))
2079 && !taken[i]
2080 {
2081 out[rank] = Some(i);
2082 taken[i] = true;
2083 }
2084 }
2085 for (rank, g) in geoms.iter().enumerate() {
2086 if out[rank].is_some() {
2087 continue;
2088 }
2089 let mut same_size = streams
2090 .iter()
2091 .enumerate()
2092 .filter(|(i, s)| !taken[*i] && s.position.is_none() && s.size == Some(g.logical));
2093 if let (Some((i, _)), None) = (same_size.next(), same_size.next()) {
2094 out[rank] = Some(i);
2095 taken[i] = true;
2096 }
2097 }
2098 for (rank, slot) in out.iter_mut().enumerate() {
2099 if slot.is_none() && rank < streams.len() && !taken[rank] {
2100 *slot = Some(rank);
2101 taken[rank] = true;
2102 }
2103 }
2104 out
2105}
2106
2107pub(crate) struct PortalLive {
2110 pub(crate) session: PortalSession,
2111 pub(crate) core: Option<Arc<PwCore>>,
2112 pub(crate) epoch: u64,
2114 paint: bool,
2116 ei: Option<EiInjector>,
2119}
2120
2121#[derive(Default)]
2122struct PortalInner {
2123 paint: Option<bool>,
2125 live: Option<Arc<PortalLive>>,
2126 epoch: u64,
2127 restore_token: Option<String>,
2128 dead: Option<String>,
2129 closed: bool,
2130}
2131
2132pub(crate) struct PortalCtl {
2135 inner: Mutex<PortalInner>,
2136 changed: Condvar,
2137 capture: bool,
2138 devices: u32,
2139}
2140
2141impl PortalCtl {
2142 fn spawn(capture: bool, devices: u32) -> Arc<Self> {
2143 let ctl = Arc::new(PortalCtl {
2144 inner: Mutex::new(PortalInner::default()),
2145 changed: Condvar::new(),
2146 capture,
2147 devices,
2148 });
2149 let worker = ctl.clone();
2150 let _ = std::thread::Builder::new()
2151 .name("pf-host-portal".into())
2152 .spawn(move || portal_thread(worker));
2153 ctl
2154 }
2155
2156 fn set_paint(&self, paint: bool) {
2159 let mut inner = self.inner.lock().unwrap();
2160 if inner.paint != Some(paint) {
2161 inner.paint = Some(paint);
2162 self.changed.notify_all();
2163 }
2164 }
2165
2166 pub(crate) fn live(&self) -> Option<Arc<PortalLive>> {
2167 self.inner.lock().unwrap().live.clone()
2168 }
2169
2170 fn dead(&self) -> bool {
2171 self.inner.lock().unwrap().dead.is_some()
2172 }
2173
2174 fn mark_dead(&self, why: String) {
2175 let mut inner = self.inner.lock().unwrap();
2176 if inner.dead.is_none() {
2177 eprintln!("[HostCapture] portal session lost: {why}");
2178 inner.dead = Some(why);
2179 }
2180 inner.live = None;
2181 self.changed.notify_all();
2182 }
2183
2184 fn close(&self) {
2185 let mut inner = self.inner.lock().unwrap();
2186 inner.closed = true;
2187 inner.live = None;
2188 self.changed.notify_all();
2189 }
2190}
2191
2192fn portal_thread(ctl: Arc<PortalCtl>) {
2196 loop {
2197 let (paint, token) = {
2198 let mut inner = ctl.inner.lock().unwrap();
2199 loop {
2200 if inner.closed || inner.dead.is_some() {
2201 return;
2202 }
2203 if let Some(p) = inner.paint
2204 && inner.live.as_ref().is_none_or(|l| l.paint != p)
2205 {
2206 break (
2207 p,
2208 inner
2209 .restore_token
2210 .clone()
2211 .or_else(portal::saved_restore_token),
2212 );
2213 }
2214 inner = ctl.changed.wait(inner).unwrap();
2215 }
2216 };
2217 let old = ctl.inner.lock().unwrap().live.take();
2220 drop(old);
2221 let cursor_mode = if ctl.capture && !paint {
2222 portal::CURSOR_METADATA
2223 } else {
2224 portal::CURSOR_EMBEDDED
2225 };
2226 let open = |devices: u32| {
2227 PortalSession::open(ctl.capture, devices, cursor_mode, token.as_deref()).and_then(
2228 |session| {
2229 let core = if ctl.capture {
2230 Some(PwCore::connect(session.open_pipewire_remote()?)?)
2231 } else {
2232 None
2233 };
2234 Ok((session, core))
2235 },
2236 )
2237 };
2238 let opened = match open(ctl.devices) {
2242 Err(e) if ctl.capture && ctl.devices != 0 => {
2243 eprintln!(
2244 "[HostCapture] portal refused keyboard and pointer ({e}); capturing without them."
2245 );
2246 open(0)
2247 }
2248 other => other,
2249 };
2250 let mut inner = ctl.inner.lock().unwrap();
2251 match opened {
2252 Ok((session, core)) => {
2253 inner.epoch += 1;
2254 if session.restore_token.is_some() {
2255 inner.restore_token = session.restore_token.clone();
2256 }
2257 let ei = (session.devices != 0 && session.eis_capable())
2261 .then(|| open_eis(&session))
2262 .flatten();
2263 println!(
2264 "[HostCapture] portal session open: {} stream(s), devices {}{}, cursor {}, input via {}.",
2265 session.streams.len(),
2266 if session.devices & portal::DEVICE_KEYBOARD != 0 {
2267 "keyboard "
2268 } else {
2269 ""
2270 },
2271 if session.devices & portal::DEVICE_POINTER != 0 {
2272 "pointer"
2273 } else {
2274 ""
2275 },
2276 match session.cursor_mode {
2277 portal::CURSOR_METADATA => "metadata",
2278 portal::CURSOR_EMBEDDED => "embedded",
2279 _ => "hidden",
2280 },
2281 if ei.is_some() {
2282 "libei"
2283 } else {
2284 "portal notify"
2285 }
2286 );
2287 inner.live = Some(Arc::new(PortalLive {
2288 session,
2289 core,
2290 epoch: inner.epoch,
2291 paint,
2292 ei,
2293 }));
2294 }
2295 Err(e) => {
2296 eprintln!("[HostCapture] portal session failed: {e}");
2297 inner.dead = Some(e);
2298 }
2299 }
2300 ctl.changed.notify_all();
2301 }
2302}
2303
2304fn open_eis(session: &PortalSession) -> Option<EiInjector> {
2307 let fd = match session.connect_to_eis() {
2308 Ok(fd) => fd,
2309 Err(e) => {
2310 eprintln!(
2311 "[HostCapture] portal offers no EIS socket ({e}); input goes through the portal."
2312 );
2313 return None;
2314 }
2315 };
2316 match EiInjector::spawn(fd) {
2317 Ok(ei) => Some(ei),
2318 Err(e) => {
2319 eprintln!(
2320 "[HostCapture] EIS injector unavailable ({e}); input goes through the portal."
2321 );
2322 None
2323 }
2324 }
2325}
2326
2327fn portal_capture_loop(
2332 index: usize,
2333 ctl: Arc<PortalCtl>,
2334 from_main: Receiver<ToHost>,
2335 sink: FrameSink,
2336 cursor_tx: std::sync::mpsc::Sender<CursorJob>,
2337 dma_formats: Vec<(u32, Vec<u64>)>,
2338 geoms: Vec<OutputGeom>,
2339) -> Result<(), String> {
2340 let mut want: Option<Want> = None;
2341 let mut stream: Option<(PwStream, u64)> = None;
2342 let mut warned_unshared = false;
2343 loop {
2344 let first = match from_main.recv_timeout(Duration::from_millis(100)) {
2345 Ok(m) => Some(m),
2346 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => None,
2347 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return Ok(()),
2348 };
2349 let mut pending: Vec<ToHost> = first.into_iter().collect();
2350 loop {
2351 match from_main.try_recv() {
2352 Ok(m) => pending.push(m),
2353 Err(TryRecvError::Empty) => break,
2354 Err(TryRecvError::Disconnected) => return Ok(()),
2355 }
2356 }
2357 for msg in pending {
2358 match msg {
2359 ToHost::Release { generation, slot } => {
2360 if let Some((s, _)) = &stream {
2361 s.release(generation, slot);
2362 }
2363 }
2364 ToHost::Idle => {
2365 want = None;
2366 if let Some((s, _)) = &stream {
2367 s.release_all();
2368 s.set_active(false);
2369 }
2370 }
2371 ToHost::Start {
2372 width,
2373 height,
2374 zero_copy,
2375 paint_cursor,
2376 fps_milli,
2377 } => {
2378 want = Some(Want {
2379 size: (width, height),
2380 zero_copy,
2381 paint_cursor,
2382 fps_milli,
2383 });
2384 }
2385 }
2386 }
2387 let live = ctl.live();
2388 if let Some((_, epoch)) = &stream
2389 && live.as_ref().is_none_or(|l| l.epoch != *epoch)
2390 {
2391 stream = None;
2392 }
2393 let Some(w) = want else { continue };
2394 let Some(live) = live else { continue };
2395 let Some(core) = live.core.as_ref() else {
2396 continue;
2397 };
2398 let cfg = StreamConfig {
2399 zero_copy: w.zero_copy && !dma_formats.is_empty(),
2400 dma_formats: dma_formats.clone(),
2401 fps_milli: w.fps_milli,
2402 expect: w.size,
2403 };
2404 match &stream {
2405 Some((s, _)) => {
2406 s.reconfigure(cfg);
2407 s.set_active(true);
2408 if s.errored() {
2409 ctl.mark_dead(format!("output {index}: PipeWire stream failed"));
2410 return Err("PipeWire stream failed".into());
2411 }
2412 }
2413 None => {
2414 let Some(si) = assign_streams(&live.session.streams, &geoms)
2415 .get(index)
2416 .copied()
2417 .flatten()
2418 else {
2419 if !warned_unshared {
2420 warned_unshared = true;
2421 eprintln!(
2422 "[HostCapture] output {index}: the portal shares no stream for it; nothing captured."
2423 );
2424 }
2425 continue;
2426 };
2427 match PwStream::connect(
2428 core,
2429 live.session.streams[si].node_id,
2430 sink.clone(),
2431 cursor_tx.clone(),
2432 cfg,
2433 ) {
2434 Ok(s) => stream = Some((s, live.epoch)),
2435 Err(e) => {
2436 ctl.mark_dead(format!("output {index}: {e}"));
2437 return Err(e);
2438 }
2439 }
2440 }
2441 }
2442 }
2443}
2444
2445fn control_loop(
2454 conn: Connection,
2455 mut queue: EventQueue<CtrlState>,
2456 mut state: CtrlState,
2457 ctrl_rx: Receiver<CtrlMsg>,
2458 wake_rd: OwnedFd,
2459 ledger: Arc<Mutex<LayoutLedger>>,
2460) {
2461 let qh = queue.handle();
2462 loop {
2463 let mut pending: Option<(u64, Vec<LayoutSlot>)> = None;
2464 loop {
2465 match ctrl_rx.try_recv() {
2466 Ok(CtrlMsg::Apply { epoch, slots }) => pending = Some((epoch, slots)),
2467 Err(TryRecvError::Empty) => break,
2468 Err(TryRecvError::Disconnected) => return,
2469 }
2470 }
2471 if let Some((epoch, slots)) = pending {
2472 let realized = apply_layout(&conn, &mut queue, &mut state, &qh, &slots);
2473 ledger.lock().unwrap().decide(epoch, realized);
2474 }
2475 if queue.dispatch_pending(&mut state).is_err() {
2476 return;
2477 }
2478 let _ = queue.flush();
2479 if let Some(guard) = conn.prepare_read() {
2480 match wait_readable2(
2481 guard.connection_fd().as_raw_fd(),
2482 wake_rd.as_raw_fd(),
2483 Some(Duration::from_secs(1)),
2484 ) {
2485 Ok((wl, wake)) => {
2486 if wake {
2487 drain_pipe(wake_rd.as_raw_fd());
2488 }
2489 if wl {
2490 let _ = guard.read();
2491 } else {
2492 drop(guard);
2493 }
2494 }
2495 Err(_) => return,
2496 }
2497 }
2498 }
2499}
2500
2501fn apply_layout(
2509 conn: &Connection,
2510 queue: &mut EventQueue<CtrlState>,
2511 state: &mut CtrlState,
2512 qh: &QueueHandle<CtrlState>,
2513 slots: &[LayoutSlot],
2514) -> bool {
2515 let Some(mgr) = state.output_mgr.clone() else {
2516 eprintln!(
2517 "[HostCapture] host lacks zwlr_output_manager_v1; capture follows the host's own size."
2518 );
2519 return false;
2520 };
2521 let deadline = Instant::now() + LAYOUT_DEADLINE;
2522 for _ in 0..3 {
2523 while state.om_serial.is_none() && Instant::now() < deadline {
2524 if !pump_ctrl(conn, queue, state) {
2525 return false;
2526 }
2527 }
2528 let Some(serial) = state.om_serial else {
2529 eprintln!("[HostCapture] output-management serial never arrived; resize skipped.");
2530 return false;
2531 };
2532 state.cfg_result = None;
2533 state.cfg_canceled = false;
2534 let cfg = mgr.create_configuration(serial, qh, ());
2535 let mut any = false;
2536 for (i, slot) in slots.iter().enumerate() {
2537 if !slot.active {
2538 continue;
2539 }
2540 let want_name = state
2541 .order
2542 .get(i)
2543 .and_then(|&oi| state.outputs.get(oi))
2544 .and_then(|(_, n)| n.clone());
2545 let head = match want_name
2546 .as_ref()
2547 .and_then(|n| state.heads.iter().find(|(_, hn)| hn.as_ref() == Some(n)))
2548 .or_else(|| state.heads.get(i))
2549 {
2550 Some((h, _)) => h.clone(),
2551 None => {
2552 eprintln!(
2553 "[HostCapture] no output-management head for output {i}; not resized."
2554 );
2555 continue;
2556 }
2557 };
2558 let cfg_head = cfg.enable_head(&head, qh, ());
2559 cfg_head.set_custom_mode(slot.want.0, slot.want.1, 0);
2560 cfg_head.set_position(slot.pos.0, slot.pos.1);
2561 any = true;
2562 }
2563 if !any {
2564 cfg.destroy();
2565 return false;
2566 }
2567 cfg.apply();
2568 let _ = queue.flush();
2569 while state.cfg_result.is_none() && !state.cfg_canceled && Instant::now() < deadline {
2570 if !pump_ctrl(conn, queue, state) {
2571 cfg.destroy();
2572 return false;
2573 }
2574 }
2575 cfg.destroy();
2576 if state.cfg_canceled {
2577 state.om_serial = None;
2579 continue;
2580 }
2581 if state.cfg_result != Some(true) {
2582 eprintln!(
2583 "[HostCapture] host refused the layout; capture follows the host's own size."
2584 );
2585 return false;
2586 }
2587 return true;
2588 }
2589 false
2590}
2591
2592fn pump_ctrl(conn: &Connection, queue: &mut EventQueue<CtrlState>, state: &mut CtrlState) -> bool {
2594 if queue.dispatch_pending(state).is_err() {
2595 return false;
2596 }
2597 let _ = queue.flush();
2598 let Some(guard) = conn.prepare_read() else {
2599 return true;
2600 };
2601 match crate::wayland::wlclient::wait_readable(
2602 guard.connection_fd().as_raw_fd(),
2603 Duration::from_millis(200),
2604 ) {
2605 Ok(readable) => {
2606 if readable {
2607 let _ = guard.read();
2608 } else {
2609 drop(guard);
2610 }
2611 true
2612 }
2613 Err(_) => false,
2614 }
2615}
2616
2617fn fourcc_to_gbm(fourcc: u32) -> GbmFormat {
2622 match fourcc {
2625 0x34325258 => GbmFormat::Xrgb8888,
2626 _ => GbmFormat::Argb8888,
2627 }
2628}
2629
2630#[allow(clippy::too_many_arguments)]
2637fn alloc_gpu_slot(
2638 conn: &Connection,
2639 queue: &mut EventQueue<CaptureState>,
2640 state: &mut CaptureState,
2641 wake: RawFd,
2642 dev: &GbmDevice<File>,
2643 dmabuf_global: &ZwpLinuxDmabufV1,
2644 fourcc: u32,
2645 w: i32,
2646 h: i32,
2647 modifiers: &[u64],
2648) -> Result<Option<SlotBuffer>, String> {
2649 let qh = queue.handle();
2650 let format = fourcc_to_gbm(fourcc);
2651 let bo = if modifiers.is_empty() {
2652 crate::alloc_render_target(dev, w as u32, h as u32, format)
2653 } else {
2654 dev.create_buffer_object_with_modifiers::<()>(
2658 w as u32,
2659 h as u32,
2660 format,
2661 modifiers.iter().map(|&m| gbm::Modifier::from(m)),
2662 )
2663 .or_else(|_| crate::alloc_render_target(dev, w as u32, h as u32, format))
2664 }
2665 .map_err(|e| format!("GBM allocation {w}x{h}: {e:?}"))?;
2666 let dmabuf = crate::create_dmabuf_from_bo(&bo);
2667 let params = dmabuf_global.create_params(&qh, ());
2668 let modifier: u64 = dmabuf.format().modifier.into();
2669 for (i, handle) in dmabuf.handles().enumerate() {
2670 params.add(
2671 handle,
2672 i as u32,
2673 dmabuf.offsets().nth(i).unwrap_or(0),
2674 dmabuf.strides().nth(i).unwrap_or(0),
2675 (modifier >> 32) as u32,
2676 (modifier & 0xffff_ffff) as u32,
2677 );
2678 }
2679 state.params_created = None;
2680 state.params_failed = false;
2681 params.create(w, h, fourcc, zwp_linux_buffer_params_v1::Flags::empty());
2682 loop {
2683 match pump_until(
2684 conn,
2685 queue,
2686 state,
2687 wake,
2688 Some(Duration::from_secs(2)),
2689 |s| s.params_created.is_some() || s.params_failed,
2690 )? {
2691 Pump::Done => break,
2692 Pump::Control => continue,
2693 Pump::Timeout => {
2694 params.destroy();
2695 return Err("host answered neither created nor failed for the dmabuf".into());
2696 }
2697 }
2698 }
2699 params.destroy();
2700 match state.params_created.take() {
2701 Some(wl) => Ok(Some(SlotBuffer::Gpu {
2702 _bo: bo,
2703 dmabuf,
2704 wl,
2705 })),
2706 None => Ok(None),
2707 }
2708}
2709
2710fn alloc_cpu_slot(
2712 shm: &wl_shm::WlShm,
2713 qh: &QueueHandle<CaptureState>,
2714 format: u32,
2715 w: i32,
2716 h: i32,
2717 stride: i32,
2718) -> Result<SlotBuffer, String> {
2719 let size = (stride * h) as usize;
2720 let fd = memfd_with(&vec![0u8; size])?;
2721 let pool = shm.create_pool(fd.as_fd(), size as i32, qh, ());
2722 let wl = pool.create_buffer(
2723 0,
2724 w,
2725 h,
2726 stride,
2727 WEnum::<wl_shm::Format>::from(format)
2728 .into_result()
2729 .unwrap_or(wl_shm::Format::Xrgb8888),
2730 qh,
2731 (),
2732 );
2733 let file = File::from(fd);
2734 let map = unsafe { memmap2::MmapMut::map_mut(&file) }.map_err(|e| format!("shm map: {e}"))?;
2735 Ok(SlotBuffer::Cpu {
2736 _pool: pool,
2737 map: Arc::new(map),
2738 stride,
2739 format,
2740 wl,
2741 })
2742}
2743
2744enum Ctl {
2746 None,
2747 Renegotiate,
2748 Idle,
2749 Dead,
2750}
2751
2752enum Pump {
2753 Done,
2754 Control,
2755 Timeout,
2756}
2757
2758fn pump_until(
2763 conn: &Connection,
2764 queue: &mut EventQueue<CaptureState>,
2765 state: &mut CaptureState,
2766 wake_rd: RawFd,
2767 timeout: Option<Duration>,
2768 done: impl Fn(&CaptureState) -> bool,
2769) -> Result<Pump, String> {
2770 let deadline = timeout.map(|t| Instant::now() + t);
2771 loop {
2772 queue
2773 .dispatch_pending(state)
2774 .map_err(|e| format!("dispatch: {e}"))?;
2775 if done(state) {
2776 return Ok(Pump::Done);
2777 }
2778 queue.flush().map_err(|e| format!("flush: {e}"))?;
2779 let remaining = match deadline {
2780 Some(d) => match d.checked_duration_since(Instant::now()) {
2781 Some(r) => Some(r),
2782 None => return Ok(Pump::Timeout),
2783 },
2784 None => None,
2785 };
2786 let Some(guard) = conn.prepare_read() else {
2787 continue;
2788 };
2789 let (wl, wake) = wait_readable2(guard.connection_fd().as_raw_fd(), wake_rd, remaining)?;
2790 if wake {
2791 drop(guard);
2792 drain_pipe(wake_rd);
2793 return Ok(Pump::Control);
2794 }
2795 if wl {
2796 let _ = guard.read();
2797 } else {
2798 drop(guard);
2799 if deadline.is_some_and(|d| Instant::now() >= d) {
2800 return Ok(Pump::Timeout);
2801 }
2802 }
2803 }
2804}
2805
2806fn capture_loop(
2807 display: &str,
2808 index: usize,
2809 expect_name: Option<String>,
2810 gbm_path: Option<std::path::PathBuf>,
2811 from_main: Receiver<ToHost>,
2812 wake_rd: OwnedFd,
2813 frames: FrameSink,
2814) -> Result<(), String> {
2815 let path = socket_path(display).ok_or("XDG_RUNTIME_DIR is unset")?;
2816 let stream = UnixStream::connect(&path).map_err(|e| format!("connect {path}: {e}"))?;
2817 let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
2818 let mut queue = conn.new_event_queue();
2819 let qh = queue.handle();
2820 let _registry = conn.display().get_registry(&qh, ());
2821 let mut state = CaptureState::default();
2822 bounded_roundtrip(&conn, &mut queue, &mut state)?;
2823 bounded_roundtrip(&conn, &mut queue, &mut state)?;
2825
2826 let by_name = expect_name.as_ref().and_then(|expect| {
2829 state
2830 .outputs
2831 .iter()
2832 .find(|(_, n)| n.as_ref() == Some(expect))
2833 .cloned()
2834 });
2835 let (output, _name) = match by_name {
2836 Some(o) => o,
2837 None => {
2838 let mut order: Vec<usize> = (0..state.outputs.len()).collect();
2839 order.sort_by_key(|&i| output_order_key(state.outputs[i].1.as_ref(), i));
2840 let oi = *order
2841 .get(index)
2842 .ok_or_else(|| format!("host has no wl_output {index}"))?;
2843 state.outputs[oi].clone()
2844 }
2845 };
2846 let gbm = gbm_path
2847 .as_ref()
2848 .and_then(|p| File::options().read(true).write(true).open(p).ok())
2849 .and_then(|f| GbmDevice::new(f).ok());
2850 let wake = wake_rd.as_raw_fd();
2851
2852 let mut want: Option<Want> = None;
2859 let force = std::env::var("PIXELFLUX_HOST_CAPTURE").unwrap_or_default();
2860 if force != "zwlr" && state.ext_capture.is_some() && state.ext_source_mgr.is_some() {
2861 match capture_loop_ext(
2862 &conn,
2863 &mut queue,
2864 &mut state,
2865 &output,
2866 gbm.as_ref(),
2867 wake,
2868 index,
2869 &from_main,
2870 &frames,
2871 &mut want,
2872 ) {
2873 ExtOutcome::Finished => return Ok(()),
2874 ExtOutcome::Unavailable(e) => {
2875 eprintln!(
2876 "[HostCapture] output {index}: ext capture unavailable ({e}); using wlr-screencopy."
2877 );
2878 }
2879 }
2880 }
2881
2882 let screencopy = state
2883 .screencopy
2884 .clone()
2885 .ok_or("no zwlr_screencopy_manager_v1")?;
2886
2887 let mut slots: Vec<Option<SlotBuffer>> = (0..SLOTS).map(|_| None).collect();
2888 let mut free: Vec<usize> = (0..SLOTS).collect();
2889 let mut generation: u64 = 0;
2890 let mut announced: Option<(i32, i32)> = None;
2891 let mut warned_mismatch = false;
2892 let mut consecutive_failures = 0u32;
2893 let mut slot_zero_copy: Option<bool> = None;
2895 let mut gpu_refused = false;
2896
2897 'main: loop {
2898 loop {
2900 let blocking = want.is_none() || free.is_empty();
2901 let msg = if blocking {
2902 match from_main.recv() {
2903 Ok(m) => m,
2904 Err(_) => return Ok(()),
2905 }
2906 } else {
2907 match from_main.try_recv() {
2908 Ok(m) => m,
2909 Err(TryRecvError::Empty) => break,
2910 Err(TryRecvError::Disconnected) => return Ok(()),
2911 }
2912 };
2913 match msg {
2914 ToHost::Release {
2915 generation: g,
2916 slot,
2917 } => {
2918 if g == generation {
2919 free.push(slot);
2920 }
2921 }
2922 ToHost::Idle => want = None,
2923 ToHost::Start {
2924 width,
2925 height,
2926 zero_copy,
2927 paint_cursor,
2928 fps_milli,
2929 } => {
2930 let next = Want {
2931 size: (width, height),
2932 zero_copy,
2933 paint_cursor,
2934 fps_milli,
2935 };
2936 if want != Some(next) {
2937 warned_mismatch = false;
2938 }
2939 want = Some(next);
2940 }
2941 }
2942 }
2943 let (want_w, want_h, want_zero_copy, want_paint) = match want {
2944 Some(w) => (w.size.0, w.size.1, w.zero_copy, w.paint_cursor),
2945 None => continue,
2946 };
2947
2948 state.reset_frame();
2951 let frame = screencopy.capture_output(i32::from(want_paint), &output, &qh, ());
2952 loop {
2953 match pump_until(&conn, &mut queue, &mut state, wake, None, |s| {
2954 s.buffer_done || s.failed
2955 })? {
2956 Pump::Done => break,
2957 Pump::Control => match drain_ctl(&from_main, generation, &mut free, &mut want) {
2958 Ctl::None => {}
2959 Ctl::Renegotiate | Ctl::Idle => {
2960 frame.destroy();
2961 continue 'main;
2962 }
2963 Ctl::Dead => {
2964 frame.destroy();
2965 return Ok(());
2966 }
2967 },
2968 Pump::Timeout => {}
2969 }
2970 }
2971 if state.failed {
2972 frame.destroy();
2973 consecutive_failures += 1;
2974 if consecutive_failures == 3 {
2975 eprintln!(
2976 "[HostCapture] output {index}: repeated screencopy failures; is the output alive?"
2977 );
2978 }
2979 let _ = pump_until(
2980 &conn,
2981 &mut queue,
2982 &mut state,
2983 wake,
2984 Some(Duration::from_millis(100)),
2985 |_| false,
2986 );
2987 continue;
2988 }
2989
2990 let (fw, fh) = state
2991 .announce_dmabuf
2992 .map(|(_, w, h)| (w, h))
2993 .or(state.announce_shm.map(|(_, w, h, _)| (w, h)))
2994 .ok_or("screencopy announced no buffer type")?;
2995 let mut renegotiate = false;
2996 if announced != Some((fw, fh)) {
2997 announced = Some((fw, fh));
2998 eprintln!(
2999 "[HostCapture] output {index} negotiated: {fw}x{fh} gbm={} dmabuf_global={} dmabuf_announce={:?} shm={:?}",
3000 gbm.is_some(),
3001 state.dmabuf.is_some(),
3002 state.announce_dmabuf,
3003 state.announce_shm,
3004 );
3005 renegotiate = true;
3006 }
3007 if slot_zero_copy.is_some_and(|z| z != want_zero_copy) {
3008 renegotiate = true;
3012 }
3013 if renegotiate {
3014 for s in slots.iter_mut() {
3017 *s = None;
3018 }
3019 free = (0..SLOTS).collect();
3020 slot_zero_copy = None;
3021 gpu_refused = false;
3022 generation += 1;
3023 }
3024 if (fw, fh) != (want_w, want_h) {
3025 frame.destroy();
3028 if !warned_mismatch {
3029 warned_mismatch = true;
3030 eprintln!(
3031 "[HostCapture] output {index}: waiting for host {want_w}x{want_h} (currently {fw}x{fh})."
3032 );
3033 }
3034 let _ = pump_until(
3035 &conn,
3036 &mut queue,
3037 &mut state,
3038 wake,
3039 Some(Duration::from_millis(150)),
3040 |_| false,
3041 );
3042 continue;
3043 }
3044 warned_mismatch = false;
3045
3046 let slot_idx = *free.last().unwrap();
3047 if slots[slot_idx].is_none() {
3048 slot_zero_copy = Some(want_zero_copy);
3051 let mut built: Option<SlotBuffer> = None;
3052 if want_zero_copy && !gpu_refused {
3053 let dmabuf_global = state.dmabuf.clone();
3054 if let (Some(dev), Some(dmabuf_global), Some((fourcc, w, h))) =
3055 (gbm.as_ref(), dmabuf_global.as_ref(), state.announce_dmabuf)
3056 {
3057 match alloc_gpu_slot(
3058 &conn,
3059 &mut queue,
3060 &mut state,
3061 wake,
3062 dev,
3063 dmabuf_global,
3064 fourcc,
3065 w,
3066 h,
3067 &[],
3068 )? {
3069 Some(slot) => built = Some(slot),
3070 None => {
3071 gpu_refused = true;
3072 eprintln!(
3073 "[HostCapture] output {index}: host refused the dmabuf import; capturing via shm."
3074 );
3075 }
3076 }
3077 }
3078 }
3079 slots[slot_idx] = Some(match built {
3080 Some(slot) => slot,
3081 None => {
3082 let (format, w, h, stride) =
3083 state.announce_shm.ok_or("no shm fallback announced")?;
3084 let shm = state.shm.clone().ok_or("host compositor lacks wl_shm")?;
3085 alloc_cpu_slot(&shm, &qh, format, w, h, stride)?
3086 }
3087 });
3088 }
3089 free.pop();
3090
3091 {
3092 let slot = slots[slot_idx].as_ref().unwrap();
3093 let wl = match slot {
3094 SlotBuffer::Gpu { wl, .. } => wl,
3095 SlotBuffer::Cpu { wl, .. } => wl,
3096 };
3097 frame.copy_with_damage(wl);
3098 }
3099 queue.flush().map_err(|e| format!("flush: {e}"))?;
3100 let mut aborted = false;
3101 loop {
3102 match pump_until(&conn, &mut queue, &mut state, wake, None, |s| {
3103 s.ready || s.failed
3104 })? {
3105 Pump::Done => break,
3106 Pump::Control => match drain_ctl(&from_main, generation, &mut free, &mut want) {
3107 Ctl::None => {}
3108 Ctl::Renegotiate | Ctl::Idle => {
3109 aborted = true;
3110 break;
3111 }
3112 Ctl::Dead => {
3113 frame.destroy();
3114 return Ok(());
3115 }
3116 },
3117 Pump::Timeout => {}
3118 }
3119 }
3120 frame.destroy();
3121 if aborted {
3122 free.push(slot_idx);
3123 continue 'main;
3124 }
3125 if state.failed {
3126 free.push(slot_idx);
3127 consecutive_failures += 1;
3128 if consecutive_failures == 3 {
3129 eprintln!(
3130 "[HostCapture] output {index}: repeated screencopy failures; is the output alive?"
3131 );
3132 }
3133 let _ = pump_until(
3134 &conn,
3135 &mut queue,
3136 &mut state,
3137 wake,
3138 Some(Duration::from_millis(50)),
3139 |_| false,
3140 );
3141 continue;
3142 }
3143 consecutive_failures = 0;
3144
3145 let damage = std::mem::take(&mut state.damage);
3146 let out = match slots[slot_idx].as_ref().unwrap() {
3147 SlotBuffer::Gpu { dmabuf, .. } => HostFrame {
3148 generation,
3149 slot: slot_idx,
3150 dmabuf: Some(dmabuf.clone()),
3151 cpu: None,
3152 width: fw,
3153 height: fh,
3154 damage,
3155 stamp_ns: now_ns(),
3156 },
3157 SlotBuffer::Cpu {
3158 map,
3159 stride,
3160 format,
3161 ..
3162 } => HostFrame {
3163 generation,
3164 slot: slot_idx,
3165 dmabuf: None,
3166 cpu: Some(HostCpuFrame {
3167 map: map.clone() as Arc<dyn AsRef<[u8]> + Send + Sync>,
3168 offset: 0,
3169 stride: *stride as usize,
3170 format: *format,
3171 }),
3172 width: fw,
3173 height: fh,
3174 damage,
3175 stamp_ns: now_ns(),
3176 },
3177 };
3178 if !frames.send(out) {
3179 return Ok(());
3180 }
3181 }
3182}
3183
3184enum ExtOutcome {
3187 Finished,
3188 Unavailable(String),
3189}
3190
3191#[allow(clippy::too_many_arguments)]
3196fn capture_loop_ext(
3197 conn: &Connection,
3198 queue: &mut EventQueue<CaptureState>,
3199 state: &mut CaptureState,
3200 output: &wl_output::WlOutput,
3201 gbm: Option<&GbmDevice<File>>,
3202 wake: RawFd,
3203 index: usize,
3204 from_main: &Receiver<ToHost>,
3205 frames: &FrameSink,
3206 want: &mut Option<Want>,
3207) -> ExtOutcome {
3208 let qh = queue.handle();
3209 let mut slots: Vec<Option<SlotBuffer>> = (0..SLOTS).map(|_| None).collect();
3210 let mut free: Vec<usize> = (0..SLOTS).collect();
3211 let mut generation: u64 = 0;
3212 let mut slot_zero_copy: Option<bool> = None;
3213 let mut gpu_refused = false;
3214 let mut seen_serial: u64 = 0;
3215 let mut warned_mismatch = false;
3216 let mut consecutive_failures = 0u32;
3217
3218 let teardown = |session: &ExtImageCopyCaptureSessionV1, source: &ExtImageCaptureSourceV1| {
3219 session.destroy();
3220 source.destroy();
3221 };
3222
3223 let mut session_paints = false;
3226 let (mut source, mut session) = match open_ext_session(
3227 conn,
3228 queue,
3229 state,
3230 output,
3231 wake,
3232 index,
3233 from_main,
3234 session_paints,
3235 generation,
3236 &mut free,
3237 want,
3238 ) {
3239 Ok(opened) => opened,
3240 Err(outcome) => return outcome,
3241 };
3242
3243 'main: loop {
3244 loop {
3246 let blocking = want.is_none() || free.is_empty();
3247 let msg = if blocking {
3248 match from_main.recv() {
3249 Ok(m) => m,
3250 Err(_) => {
3251 teardown(&session, &source);
3252 return ExtOutcome::Finished;
3253 }
3254 }
3255 } else {
3256 match from_main.try_recv() {
3257 Ok(m) => m,
3258 Err(TryRecvError::Empty) => break,
3259 Err(TryRecvError::Disconnected) => {
3260 teardown(&session, &source);
3261 return ExtOutcome::Finished;
3262 }
3263 }
3264 };
3265 match msg {
3266 ToHost::Release {
3267 generation: g,
3268 slot,
3269 } => {
3270 if g == generation {
3271 free.push(slot);
3272 }
3273 }
3274 ToHost::Idle => *want = None,
3275 ToHost::Start {
3276 width,
3277 height,
3278 zero_copy,
3279 paint_cursor,
3280 fps_milli,
3281 } => {
3282 let next = Want {
3283 size: (width, height),
3284 zero_copy,
3285 paint_cursor,
3286 fps_milli,
3287 };
3288 if *want != Some(next) {
3289 warned_mismatch = false;
3290 }
3291 *want = Some(next);
3292 }
3293 }
3294 }
3295 let (want_w, want_h, want_zero_copy, want_paint) = match *want {
3296 Some(w) => (w.size.0, w.size.1, w.zero_copy, w.paint_cursor),
3297 None => continue,
3298 };
3299 if state.ext_stopped {
3300 teardown(&session, &source);
3302 return ExtOutcome::Finished;
3303 }
3304 if want_paint != session_paints {
3305 teardown(&session, &source);
3308 (source, session) = match open_ext_session(
3309 conn, queue, state, output, wake, index, from_main, want_paint, generation,
3310 &mut free, want,
3311 ) {
3312 Ok(opened) => opened,
3313 Err(outcome) => return outcome,
3314 };
3315 session_paints = want_paint;
3316 continue;
3317 }
3318
3319 if seen_serial != state.ext_serial || slot_zero_copy.is_some_and(|z| z != want_zero_copy) {
3321 seen_serial = state.ext_serial;
3322 for s in slots.iter_mut() {
3323 *s = None;
3324 }
3325 free = (0..SLOTS).collect();
3326 slot_zero_copy = None;
3327 gpu_refused = false;
3328 generation += 1;
3329 }
3330
3331 let Some((cw, ch)) = state.ext_size else {
3332 teardown(&session, &source);
3333 return ExtOutcome::Unavailable("constraints carried no size".into());
3334 };
3335 if (cw, ch) != (want_w, want_h) {
3336 if !warned_mismatch {
3339 warned_mismatch = true;
3340 eprintln!(
3341 "[HostCapture] output {index}: waiting for host {want_w}x{want_h} (currently {cw}x{ch})."
3342 );
3343 }
3344 let _ = pump_until(
3345 conn,
3346 queue,
3347 state,
3348 wake,
3349 Some(Duration::from_millis(150)),
3350 |s| s.ext_serial != seen_serial,
3351 );
3352 continue;
3353 }
3354 warned_mismatch = false;
3355
3356 let slot_idx = *free.last().unwrap();
3357 if slots[slot_idx].is_none() {
3358 let dma_choice = [0x3432_5258u32, 0x3432_5241]
3363 .iter()
3364 .find_map(|f| state.ext_dma_formats.iter().find(|(code, _)| code == f))
3365 .cloned();
3366 slot_zero_copy = Some(want_zero_copy);
3367 let mut slot: Option<SlotBuffer> = None;
3368 if want_zero_copy && !gpu_refused {
3369 let dmabuf_global = state.dmabuf.clone();
3370 if let (Some(dev), Some(dmabuf_global), Some((fourcc, modifiers))) =
3371 (gbm, dmabuf_global.as_ref(), dma_choice)
3372 {
3373 match alloc_gpu_slot(
3374 conn,
3375 queue,
3376 state,
3377 wake,
3378 dev,
3379 dmabuf_global,
3380 fourcc,
3381 cw,
3382 ch,
3383 &modifiers,
3384 ) {
3385 Ok(Some(built)) => slot = Some(built),
3386 Ok(None) => {
3387 gpu_refused = true;
3388 eprintln!(
3389 "[HostCapture] output {index}: host refused the dmabuf import; capturing via shm."
3390 );
3391 }
3392 Err(e) => {
3393 teardown(&session, &source);
3394 return ExtOutcome::Unavailable(e);
3395 }
3396 }
3397 }
3398 }
3399 let built = match slot {
3400 Some(slot) => Ok(slot),
3401 None => {
3402 let format = [1u32, 0]
3403 .iter()
3404 .find(|f| state.ext_shm_formats.contains(f))
3405 .copied()
3406 .or_else(|| state.ext_shm_formats.first().copied());
3407 match (state.shm.clone(), format) {
3408 (Some(shm), Some(format)) => {
3409 alloc_cpu_slot(&shm, &qh, format, cw, ch, cw * 4)
3410 }
3411 _ => Err("host offers no usable shm format".into()),
3412 }
3413 }
3414 };
3415 match built {
3416 Ok(slot) => slots[slot_idx] = Some(slot),
3417 Err(e) => {
3418 teardown(&session, &source);
3419 return ExtOutcome::Unavailable(e);
3420 }
3421 }
3422 }
3423 free.pop();
3424
3425 state.reset_frame();
3426 let frame = {
3427 let slot = slots[slot_idx].as_ref().unwrap();
3428 let wl = match slot {
3429 SlotBuffer::Gpu { wl, .. } => wl,
3430 SlotBuffer::Cpu { wl, .. } => wl,
3431 };
3432 let frame = session.create_frame(&qh, ());
3433 frame.attach_buffer(wl);
3434 frame.damage_buffer(0, 0, cw, ch);
3437 frame.capture();
3438 frame
3439 };
3440 if let Err(e) = queue.flush() {
3441 frame.destroy();
3442 teardown(&session, &source);
3443 return ExtOutcome::Unavailable(format!("flush: {e}"));
3444 }
3445
3446 let mut aborted = false;
3447 loop {
3448 match pump_until(conn, queue, state, wake, None, |s| {
3449 s.ready || s.failed || s.ext_stopped || s.ext_serial != seen_serial
3450 }) {
3451 Ok(Pump::Done) => break,
3452 Ok(Pump::Control) => match drain_ctl(from_main, generation, &mut free, want) {
3453 Ctl::None => {}
3454 Ctl::Renegotiate | Ctl::Idle => {
3455 aborted = true;
3456 break;
3457 }
3458 Ctl::Dead => {
3459 frame.destroy();
3460 teardown(&session, &source);
3461 return ExtOutcome::Finished;
3462 }
3463 },
3464 Ok(Pump::Timeout) => {}
3465 Err(e) => {
3466 frame.destroy();
3467 teardown(&session, &source);
3468 return ExtOutcome::Unavailable(e);
3469 }
3470 }
3471 }
3472 frame.destroy();
3473 if aborted || state.ext_serial != seen_serial {
3474 free.push(slot_idx);
3475 continue 'main;
3476 }
3477 if state.ext_stopped {
3478 teardown(&session, &source);
3479 return ExtOutcome::Finished;
3480 }
3481 if state.failed {
3482 free.push(slot_idx);
3483 if state.ext_fail_reason
3484 == Some(ext_image_copy_capture_frame_v1::FailureReason::BufferConstraints)
3485 {
3486 continue;
3489 }
3490 consecutive_failures += 1;
3491 if consecutive_failures == 3 {
3492 eprintln!(
3493 "[HostCapture] output {index}: repeated capture failures; is the output alive?"
3494 );
3495 }
3496 let _ = pump_until(
3497 conn,
3498 queue,
3499 state,
3500 wake,
3501 Some(Duration::from_millis(50)),
3502 |_| false,
3503 );
3504 continue;
3505 }
3506 consecutive_failures = 0;
3507
3508 let damage = std::mem::take(&mut state.damage);
3509 let out = match slots[slot_idx].as_ref().unwrap() {
3510 SlotBuffer::Gpu { dmabuf, .. } => HostFrame {
3511 generation,
3512 slot: slot_idx,
3513 dmabuf: Some(dmabuf.clone()),
3514 cpu: None,
3515 width: cw,
3516 height: ch,
3517 damage,
3518 stamp_ns: now_ns(),
3519 },
3520 SlotBuffer::Cpu {
3521 map,
3522 stride,
3523 format,
3524 ..
3525 } => HostFrame {
3526 generation,
3527 slot: slot_idx,
3528 dmabuf: None,
3529 cpu: Some(HostCpuFrame {
3530 map: map.clone() as Arc<dyn AsRef<[u8]> + Send + Sync>,
3531 offset: 0,
3532 stride: *stride as usize,
3533 format: *format,
3534 }),
3535 width: cw,
3536 height: ch,
3537 damage,
3538 stamp_ns: now_ns(),
3539 },
3540 };
3541 if !frames.send(out) {
3542 teardown(&session, &source);
3543 return ExtOutcome::Finished;
3544 }
3545 }
3546}
3547
3548#[allow(clippy::too_many_arguments)]
3553fn open_ext_session(
3554 conn: &Connection,
3555 queue: &mut EventQueue<CaptureState>,
3556 state: &mut CaptureState,
3557 output: &wl_output::WlOutput,
3558 wake: RawFd,
3559 index: usize,
3560 from_main: &Receiver<ToHost>,
3561 paint_cursor: bool,
3562 generation: u64,
3563 free: &mut Vec<usize>,
3564 want: &mut Option<Want>,
3565) -> Result<(ExtImageCaptureSourceV1, ExtImageCopyCaptureSessionV1), ExtOutcome> {
3566 let qh = queue.handle();
3567 let src_mgr = state.ext_source_mgr.clone().expect("checked by caller");
3568 let mgr = state.ext_capture.clone().expect("checked by caller");
3569 let source = src_mgr.create_source(output, &qh, ());
3570 let options = if paint_cursor {
3571 ext_image_copy_capture_manager_v1::Options::PaintCursors
3572 } else {
3573 ext_image_copy_capture_manager_v1::Options::empty()
3574 };
3575 let session = mgr.create_session(&source, options, &qh, ());
3576 let teardown = |session: &ExtImageCopyCaptureSessionV1, source: &ExtImageCaptureSourceV1| {
3577 session.destroy();
3578 source.destroy();
3579 };
3580 state.ext_stopped = false;
3582 state.ext_pending_size = None;
3583 state.ext_pending_shm.clear();
3584 state.ext_pending_dma.clear();
3585 let before = state.ext_serial;
3586 loop {
3587 match pump_until(
3588 conn,
3589 queue,
3590 state,
3591 wake,
3592 Some(Duration::from_secs(5)),
3593 |s| s.ext_serial > before || s.ext_stopped,
3594 ) {
3595 Ok(Pump::Done) => break,
3596 Ok(Pump::Control) => match drain_ctl(from_main, generation, free, want) {
3597 Ctl::Dead => {
3598 teardown(&session, &source);
3599 return Err(ExtOutcome::Finished);
3600 }
3601 _ => continue,
3602 },
3603 Ok(Pump::Timeout) => {
3604 teardown(&session, &source);
3605 return Err(ExtOutcome::Unavailable(
3606 "no buffer constraints within 5s".into(),
3607 ));
3608 }
3609 Err(e) => {
3610 teardown(&session, &source);
3611 return Err(ExtOutcome::Unavailable(e));
3612 }
3613 }
3614 }
3615 if state.ext_stopped {
3616 teardown(&session, &source);
3617 return Err(ExtOutcome::Unavailable(
3618 "session stopped before constraints".into(),
3619 ));
3620 }
3621 eprintln!(
3622 "[HostCapture] output {index} ext session: {:?} dma_formats={} shm_formats={} cursor={}",
3623 state.ext_size,
3624 state.ext_dma_formats.len(),
3625 state.ext_shm_formats.len(),
3626 if paint_cursor { "painted" } else { "consumer" },
3627 );
3628 Ok((source, session))
3629}
3630
3631fn drain_ctl(
3634 rx: &Receiver<ToHost>,
3635 generation: u64,
3636 free: &mut Vec<usize>,
3637 want: &mut Option<Want>,
3638) -> Ctl {
3639 let mut out = Ctl::None;
3640 loop {
3641 match rx.try_recv() {
3642 Ok(ToHost::Release {
3643 generation: g,
3644 slot,
3645 }) => {
3646 if g == generation {
3647 free.push(slot);
3648 }
3649 }
3650 Ok(ToHost::Start {
3651 width,
3652 height,
3653 zero_copy,
3654 paint_cursor,
3655 fps_milli,
3656 }) => {
3657 let next = Want {
3658 size: (width, height),
3659 zero_copy,
3660 paint_cursor,
3661 fps_milli,
3662 };
3663 if *want != Some(next) {
3664 *want = Some(next);
3665 out = Ctl::Renegotiate;
3666 }
3667 }
3668 Ok(ToHost::Idle) => {
3669 *want = None;
3670 out = Ctl::Idle;
3671 }
3672 Err(TryRecvError::Empty) => return out,
3673 Err(TryRecvError::Disconnected) => return Ctl::Dead,
3674 }
3675 }
3676}
3677
3678#[cfg(test)]
3679mod tests {
3680 #[test]
3682 fn a_finger_scroll_turns_into_whole_notches_with_the_rest_carried() {
3683 let mut carry = 0.0;
3684 assert_eq!(super::scroll_notches(&mut carry, 60.0), 0);
3685 assert_eq!(super::scroll_notches(&mut carry, 60.0), 1);
3686 assert!((carry - 20.0).abs() < 1e-9);
3687 assert_eq!(super::scroll_notches(&mut carry, -260.0), -2);
3688 assert!((carry + 40.0).abs() < 1e-9);
3689 }
3690
3691 use super::*;
3692
3693 fn us_host_keyboard() -> HostKeyboardState {
3694 let text = crate::wayland::keymap::compile_rmlvo("", "", "us", "", "").unwrap();
3695 let mut keyboard = HostKeyboardState::default();
3696 assert!(keyboard.set_keymap(&text).is_some());
3697 keyboard
3698 }
3699
3700 #[test]
3702 fn shm_rows_reach_bgra_from_each_announced_layout() {
3703 let cases = [
3704 (
3705 wl_shm::Format::Xrgb8888 as u32,
3706 vec![1u8, 2, 3, 4],
3707 vec![1u8, 2, 3, 4],
3708 ),
3709 (
3710 wl_shm::Format::Xbgr8888 as u32,
3711 vec![1u8, 2, 3, 9],
3712 vec![3u8, 2, 1, 9],
3713 ),
3714 (
3715 wl_shm::Format::Bgr888 as u32,
3716 vec![1u8, 2, 3],
3717 vec![3u8, 2, 1, 0xff],
3718 ),
3719 (
3720 wl_shm::Format::Rgb888 as u32,
3721 vec![1u8, 2, 3],
3722 vec![1u8, 2, 3, 0xff],
3723 ),
3724 ];
3725 for (format, src, want) in cases {
3726 let (bpp, swap) = shm_src_layout(format);
3727 let mut dst = vec![0u8; 4];
3728 convert_shm_row(&src, &mut dst, bpp, swap);
3729 assert_eq!(dst, want, "format {format}");
3730 }
3731 }
3732
3733 #[test]
3735 fn shm_rows_convert_every_pixel_of_the_row() {
3736 let src: Vec<u8> = (0..12u8).collect();
3737 let mut dst = vec![0u8; 16];
3738 convert_shm_row(&src, &mut dst, 3, true);
3739 assert_eq!(
3740 dst,
3741 vec![2, 1, 0, 0xff, 5, 4, 3, 0xff, 8, 7, 6, 0xff, 11, 10, 9, 0xff]
3742 );
3743 let src: Vec<u8> = (0..8u8).collect();
3744 let mut dst = vec![0u8; 8];
3745 convert_shm_row(&src, &mut dst, 4, true);
3746 assert_eq!(dst, vec![2, 1, 0, 3, 6, 5, 4, 7]);
3747 }
3748
3749 #[test]
3750 fn host_keyboard_serializes_control_modifier() {
3751 let mut keyboard = us_host_keyboard();
3752 let control_down = keyboard.update_key(37, true).unwrap();
3753 assert_ne!(control_down.depressed, 0);
3754 let control_up = keyboard.update_key(37, false).unwrap();
3755 assert_eq!(control_up.depressed, 0);
3756 }
3757
3758 #[test]
3759 fn host_keyboard_ignores_repeated_key_down() {
3760 let mut keyboard = us_host_keyboard();
3761 keyboard.update_key(37, true).unwrap();
3762 assert!(keyboard.update_key(37, true).is_none());
3763 let control_up = keyboard.update_key(37, false).unwrap();
3764 assert_eq!(control_up.depressed, 0);
3765 }
3766
3767 #[test]
3768 fn host_keyboard_keeps_lock_across_keymap_changes() {
3769 let text = crate::wayland::keymap::compile_rmlvo("", "", "us", "", "").unwrap();
3770 let mut keyboard = us_host_keyboard();
3771 keyboard.update_key(66, true).unwrap();
3772 let locked = keyboard.update_key(66, false).unwrap();
3773 assert_ne!(locked.locked, 0);
3774 let after_keymap = keyboard.set_keymap(&text).unwrap();
3777 assert_ne!(after_keymap.locked, 0);
3778 }
3779
3780 #[test]
3781 fn host_keyboard_preserves_pressed_modifiers_across_keymap_changes() {
3782 let text = crate::wayland::keymap::compile_rmlvo("", "", "us", "", "").unwrap();
3783 let mut keyboard = us_host_keyboard();
3784 keyboard.update_key(37, true).unwrap();
3785 let after_keymap = keyboard.set_keymap(&text).unwrap();
3786 assert_ne!(after_keymap.depressed, 0);
3787 let control_up = keyboard.update_key(37, false).unwrap();
3788 assert_eq!(control_up.depressed, 0);
3789 }
3790
3791 #[test]
3792 fn host_keyboard_keeps_control_active_until_both_keys_are_released() {
3793 let mut keyboard = us_host_keyboard();
3794 keyboard.update_key(37, true).unwrap();
3795 keyboard.update_key(105, true).unwrap();
3796 let after_left_release = keyboard.update_key(37, false).unwrap();
3797 assert_ne!(after_left_release.depressed, 0);
3798 let after_right_release = keyboard.update_key(105, false).unwrap();
3799 assert_eq!(after_right_release.depressed, 0);
3800 }
3801
3802 #[test]
3803 fn host_keyboard_ignores_repeated_lock_key_down() {
3804 let mut keyboard = us_host_keyboard();
3805 keyboard.update_key(66, true).unwrap();
3806 assert!(keyboard.update_key(66, true).is_none());
3807 let caps_lock = keyboard.update_key(66, false).unwrap();
3808 assert_eq!(caps_lock.depressed, 0);
3809 assert_ne!(caps_lock.locked, 0);
3810 }
3811
3812 #[test]
3816 fn shm_layout_matches_wl_shm_definitions() {
3817 assert_eq!(shm_src_layout(wl_shm::Format::Bgr888 as u32), (3, true));
3818 assert_eq!(shm_src_layout(wl_shm::Format::Rgb888 as u32), (3, false));
3819 assert_eq!(shm_src_layout(wl_shm::Format::Xbgr8888 as u32), (4, true));
3820 assert_eq!(shm_src_layout(wl_shm::Format::Abgr8888 as u32), (4, true));
3821 assert_eq!(shm_src_layout(wl_shm::Format::Xrgb8888 as u32), (4, false));
3822 assert_eq!(shm_src_layout(wl_shm::Format::Argb8888 as u32), (4, false));
3823 }
3824
3825 #[test]
3830 fn bgr888_known_pixel_swaps_red_and_blue() {
3831 let (bpp, swap) = shm_src_layout(wl_shm::Format::Bgr888 as u32);
3832 let src = [0x11u8, 0x22, 0x33];
3833 let mut dst = [0u8; 4];
3834 convert_shm_row(&src, &mut dst, bpp, swap);
3835 assert_eq!(dst, [0x33, 0x22, 0x11, 0xff]);
3836 }
3837
3838 #[test]
3841 fn rgb888_known_pixel_copies_straight() {
3842 let (bpp, swap) = shm_src_layout(wl_shm::Format::Rgb888 as u32);
3843 let src = [0x33u8, 0x22, 0x11];
3844 let mut dst = [0u8; 4];
3845 convert_shm_row(&src, &mut dst, bpp, swap);
3846 assert_eq!(dst, [0x33, 0x22, 0x11, 0xff]);
3847 }
3848
3849 fn convert(format: wl_shm::Format, src: &[u8]) -> [u8; 4] {
3852 let (bpp, swap) = shm_src_layout(format as u32);
3853 let mut dst = [0u8; 4];
3854 convert_shm_row(src, &mut dst, bpp, swap);
3855 dst
3856 }
3857
3858 #[test]
3859 fn four_byte_formats_convert_to_bgra() {
3860 assert_eq!(
3861 convert(wl_shm::Format::Xbgr8888, &[0x11, 0x22, 0x33, 0x44]),
3862 [0x33, 0x22, 0x11, 0x44]
3863 );
3864 assert_eq!(
3865 convert(wl_shm::Format::Xrgb8888, &[0x33, 0x22, 0x11, 0x44]),
3866 [0x33, 0x22, 0x11, 0x44]
3867 );
3868 }
3869
3870 #[test]
3875 fn layout_ledger_answers_by_epoch() {
3876 let mut ledger = LayoutLedger::default();
3877 let first = ledger.issue();
3878 let second = ledger.issue();
3879 assert_eq!((first, second), (1, 2));
3880 assert_eq!(ledger.outcome(first), None);
3881 assert_eq!(ledger.outcome(second), None);
3882
3883 ledger.decide(first, true);
3884 assert_eq!(ledger.outcome(first), Some(true));
3885 assert_eq!(ledger.outcome(second), None);
3886
3887 let third = ledger.issue();
3890 ledger.decide(third, false);
3891 assert_eq!(ledger.outcome(second), Some(false));
3892 assert_eq!(ledger.outcome(third), Some(false));
3893
3894 ledger.decide(second, true);
3896 assert_eq!(ledger.outcome(third), Some(false));
3897 let fourth = ledger.issue();
3898 assert_eq!(ledger.outcome(fourth), None);
3899 }
3900}