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