1use anyhow::{Context, Result, bail};
9#[cfg(feature = "gpu")]
10use gbm::{BufferObject, BufferObjectFlags, Device as GbmDevice, Format as GbmFormat, Modifier};
11use rustix::event::{PollFd, PollFlags, Timespec};
12use std::collections::HashMap;
13use std::ffi::c_void;
14#[cfg(feature = "gpu")]
15use std::fs::File;
16use std::os::fd::{AsFd, OwnedFd};
17use std::time::{Duration, Instant};
18use wayland_client::{
19 Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum,
20 backend::ObjectId,
21 delegate_noop, event_created_child,
22 globals::{GlobalListContents, registry_queue_init},
23 protocol::{
24 wl_buffer::WlBuffer,
25 wl_output::{self, Transform, WlOutput},
26 wl_registry::WlRegistry,
27 wl_seat::WlSeat,
28 wl_shm::{self, WlShm},
29 wl_shm_pool::WlShmPool,
30 },
31};
32#[cfg(feature = "gpu")]
33use wayland_protocols::wp::linux_dmabuf::zv1::client::{
34 zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1},
35 zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1,
36};
37use wayland_protocols_wlr::foreign_toplevel::v1::client::{
38 zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1},
39 zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
40};
41
42#[cfg(feature = "gpu")]
44const DRM_MOD_INVALID: u64 = 0x00ff_ffff_ffff_ffff;
45use wayland_protocols::ext::{
46 foreign_toplevel_list::v1::client::{
47 ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1},
48 ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1},
49 },
50 image_capture_source::v1::client::{
51 ext_foreign_toplevel_image_capture_source_manager_v1::ExtForeignToplevelImageCaptureSourceManagerV1,
52 ext_image_capture_source_v1::ExtImageCaptureSourceV1,
53 ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1,
54 },
55 image_copy_capture::v1::client::{
56 ext_image_copy_capture_frame_v1::{self, ExtImageCopyCaptureFrameV1, FailureReason},
57 ext_image_copy_capture_manager_v1::{ExtImageCopyCaptureManagerV1, Options},
58 ext_image_copy_capture_session_v1::{self, ExtImageCopyCaptureSessionV1},
59 },
60};
61use wayland_protocols::xdg::xdg_output::zv1::client::{
62 zxdg_output_manager_v1::ZxdgOutputManagerV1,
63 zxdg_output_v1::{self, ZxdgOutputV1},
64};
65
66#[derive(Clone)]
68pub struct Toplevel {
69 pub handle: ExtForeignToplevelHandleV1,
71 pub identifier: String,
73 pub title: String,
75 pub app_id: String,
77}
78
79#[derive(Clone)]
87pub struct Output {
88 pub wl_output: WlOutput,
90 pub name: String,
92 pub logical_x: i32,
94 pub logical_y: i32,
96 pub logical_w: i32,
98 pub logical_h: i32,
100 pub phys_width: i32,
102 pub phys_height: i32,
104 pub scale: i32,
106 pub transform: Transform,
108 pub have_xdg: bool,
110}
111
112fn logical_dims(phys_w: i32, phys_h: i32, scale: i32, transform: Transform) -> (i32, i32) {
116 let s = scale.max(1);
117 let (w, h) = (phys_w / s, phys_h / s);
118 if matches!(
119 transform,
120 Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270
121 ) {
122 (h, w)
123 } else {
124 (w, h)
125 }
126}
127
128impl Output {
129 pub fn logical_size(&self) -> (i32, i32) {
133 if self.have_xdg && self.logical_w > 0 && self.logical_h > 0 {
134 (self.logical_w, self.logical_h)
135 } else {
136 logical_dims(
137 self.phys_width,
138 self.phys_height,
139 self.scale,
140 self.transform,
141 )
142 }
143 }
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub struct Region {
151 pub x: i32,
153 pub y: i32,
155 pub w: u32,
157 pub h: u32,
159}
160
161impl Region {
162 pub fn is_empty(&self) -> bool {
164 self.w == 0 || self.h == 0
165 }
166
167 pub fn intersect(&self, o: &Region) -> Option<Region> {
169 let x0 = self.x.max(o.x);
170 let y0 = self.y.max(o.y);
171 let x1 = (self.x + self.w as i32).min(o.x + o.w as i32);
172 let y1 = (self.y + self.h as i32).min(o.y + o.h as i32);
173 (x1 > x0 && y1 > y0).then_some(Region {
174 x: x0,
175 y: y0,
176 w: (x1 - x0) as u32,
177 h: (y1 - y0) as u32,
178 })
179 }
180}
181
182impl Output {
183 pub fn logical_rect(&self) -> Region {
185 let (w, h) = self.logical_size();
186 Region {
187 x: self.logical_x,
188 y: self.logical_y,
189 w: w.max(0) as u32,
190 h: h.max(0) as u32,
191 }
192 }
193}
194
195pub struct CapturedImage {
197 pub width: u32,
199 pub height: u32,
201 pub rgba: Vec<u8>,
203}
204
205impl CapturedImage {
206 pub fn pixel(&self, x: u32, y: u32) -> Option<[u8; 4]> {
208 if x >= self.width || y >= self.height {
209 return None;
210 }
211 let i = ((y * self.width + x) * 4) as usize;
212 self.rgba.get(i..i + 4).map(|s| [s[0], s[1], s[2], s[3]])
213 }
214
215 pub fn crop(&self, rect: Region) -> CapturedImage {
218 let bounds = Region {
219 x: 0,
220 y: 0,
221 w: self.width,
222 h: self.height,
223 };
224 let Some(r) = rect.intersect(&bounds) else {
225 return CapturedImage {
226 width: 0,
227 height: 0,
228 rgba: Vec::new(),
229 };
230 };
231 let row_bytes = (r.w * 4) as usize;
232 let mut out = vec![0u8; row_bytes * r.h as usize];
233 for row in 0..r.h {
234 let sy = r.y as u32 + row;
235 let src = ((sy * self.width + r.x as u32) * 4) as usize;
236 let dst = row as usize * row_bytes;
237 out[dst..dst + row_bytes].copy_from_slice(&self.rgba[src..src + row_bytes]);
238 }
239 CapturedImage {
240 width: r.w,
241 height: r.h,
242 rgba: out,
243 }
244 }
245
246 pub fn blit_into(&self, dst: &mut [u8], dst_w: u32, dst_h: u32, at_x: i32, at_y: i32) {
250 let dst_rect = Region {
251 x: 0,
252 y: 0,
253 w: dst_w,
254 h: dst_h,
255 };
256 let src_rect = Region {
257 x: at_x,
258 y: at_y,
259 w: self.width,
260 h: self.height,
261 };
262 let Some(r) = src_rect.intersect(&dst_rect) else {
263 return;
264 };
265 let row_bytes = (r.w * 4) as usize;
266 for row in 0..r.h {
267 let dy = r.y as u32 + row;
268 let sy = (r.y - at_y) as u32 + row;
269 let sx = (r.x - at_x) as u32;
270 let src = ((sy * self.width + sx) * 4) as usize;
271 let dpos = ((dy * dst_w + r.x as u32) * 4) as usize;
272 dst[dpos..dpos + row_bytes].copy_from_slice(&self.rgba[src..src + row_bytes]);
273 }
274 }
275}
276
277struct PixelLayout {
280 bpp: usize,
281 r: usize,
282 g: usize,
283 b: usize,
284 a: Option<usize>,
285}
286
287impl PixelLayout {
288 fn of(f: wl_shm::Format) -> Option<Self> {
289 use wl_shm::Format::*;
290 Some(match f {
291 Argb8888 => Self {
292 bpp: 4,
293 r: 2,
294 g: 1,
295 b: 0,
296 a: Some(3),
297 },
298 Xrgb8888 => Self {
299 bpp: 4,
300 r: 2,
301 g: 1,
302 b: 0,
303 a: None,
304 },
305 Abgr8888 => Self {
306 bpp: 4,
307 r: 0,
308 g: 1,
309 b: 2,
310 a: Some(3),
311 },
312 Xbgr8888 => Self {
313 bpp: 4,
314 r: 0,
315 g: 1,
316 b: 2,
317 a: None,
318 },
319 Bgr888 => Self {
320 bpp: 3,
321 r: 0,
322 g: 1,
323 b: 2,
324 a: None,
325 },
326 Rgb888 => Self {
327 bpp: 3,
328 r: 2,
329 g: 1,
330 b: 0,
331 a: None,
332 },
333 _ => return None,
334 })
335 }
336
337 #[cfg(feature = "gpu")]
340 fn of_fourcc(f: u32) -> Option<Self> {
341 Some(match f {
342 f if f == fourcc(b'X', b'R', b'2', b'4') => Self::of(wl_shm::Format::Xrgb8888)?,
344 f if f == fourcc(b'A', b'R', b'2', b'4') => Self::of(wl_shm::Format::Argb8888)?,
345 f if f == fourcc(b'X', b'B', b'2', b'4') => Self::of(wl_shm::Format::Xbgr8888)?,
347 f if f == fourcc(b'A', b'B', b'2', b'4') => Self::of(wl_shm::Format::Abgr8888)?,
348 _ => return None,
349 })
350 }
351}
352
353#[derive(Default)]
354struct PendingToplevel {
355 identifier: String,
356 title: String,
357 app_id: String,
358}
359
360pub type SessionId = ObjectId;
362
363#[derive(Default)]
366struct SessionData {
367 width: u32,
369 height: u32,
370 format: Option<wl_shm::Format>,
371 #[cfg(feature = "gpu")]
373 dmabuf_dev: Option<u64>,
374 #[cfg(feature = "gpu")]
376 dmabuf_formats: Vec<(u32, Vec<u64>)>,
377 constraints_done: bool,
379 dirty: bool,
381 ready: bool,
383 frame_failed: Option<FailureReason>,
386 stopped: bool,
388}
389
390enum Buf {
393 Shm(ShmBuf),
394 #[cfg(feature = "gpu")]
395 Dmabuf(DmaBuf),
396}
397
398impl Buf {
399 fn wl_buffer(&self) -> &WlBuffer {
400 match self {
401 Buf::Shm(b) => &b.buffer,
402 #[cfg(feature = "gpu")]
403 Buf::Dmabuf(b) => &b.buffer,
404 }
405 }
406 fn matches(&self, w: u32, h: u32) -> bool {
408 match self {
409 Buf::Shm(b) => b.width == w && b.height == h,
410 #[cfg(feature = "gpu")]
411 Buf::Dmabuf(b) => b.width == w && b.height == h,
412 }
413 }
414}
415
416struct ShmBuf {
418 pool: WlShmPool,
419 buffer: WlBuffer,
420 _fd: OwnedFd,
421 map: *mut c_void,
422 size: usize,
423 width: u32,
424 height: u32,
425 stride: usize,
426 format: wl_shm::Format,
427}
428
429impl Drop for ShmBuf {
430 fn drop(&mut self) {
431 self.buffer.destroy();
432 self.pool.destroy();
433 unsafe {
434 let _ = rustix::mm::munmap(self.map, self.size);
435 }
436 }
437}
438
439#[cfg(feature = "gpu")]
449struct DmaBuf {
450 buffer: WlBuffer,
451 bo: BufferObject<()>,
452 width: u32,
453 height: u32,
454 fourcc: u32,
455 modifier: u64,
456 stride: u32,
457 offset: u32,
458}
459
460#[cfg(feature = "gpu")]
461impl Drop for DmaBuf {
462 fn drop(&mut self) {
463 self.buffer.destroy();
464 }
466}
467
468pub enum Frame {
471 Shm(CapturedImage),
473 #[cfg_attr(not(feature = "gpu"), allow(dead_code))]
477 Dmabuf(DmabufFrame),
478}
479
480pub struct DmabufFrame {
484 pub fd: OwnedFd,
486 pub width: u32,
488 pub height: u32,
490 pub fourcc: u32,
492 pub modifier: u64,
494 pub stride: u32,
496 pub offset: u32,
498}
499
500struct OpenSession {
505 frame: Option<ExtImageCopyCaptureFrameV1>, buf: Option<Buf>, session: ExtImageCopyCaptureSessionV1,
508 src: ExtImageCaptureSourceV1,
509}
510
511impl Drop for OpenSession {
512 fn drop(&mut self) {
513 if let Some(frame) = &self.frame {
514 frame.destroy();
515 }
516 self.session.destroy();
517 self.src.destroy();
518 }
519}
520
521#[derive(Default)]
522struct State {
523 toplevels: Vec<Toplevel>,
524 pending: Vec<(ExtForeignToplevelHandleV1, PendingToplevel)>,
525 outputs: Vec<Output>,
526 shm: Option<WlShm>,
527 tl_src: Option<ExtForeignToplevelImageCaptureSourceManagerV1>,
528 out_src: Option<ExtOutputImageCaptureSourceManagerV1>,
529 copy: Option<ExtImageCopyCaptureManagerV1>,
530 #[cfg(feature = "gpu")]
532 dmabuf: Option<ZwpLinuxDmabufV1>,
533 sessions: HashMap<ObjectId, SessionData>,
535}
536
537pub struct Client {
540 queue: EventQueue<State>,
541 qh: QueueHandle<State>,
542 state: State,
543 open: HashMap<ObjectId, OpenSession>,
545 #[cfg(feature = "gpu")]
549 gbm: Option<GbmDevice<File>>,
550}
551
552impl Client {
553 pub fn connect() -> Result<Self> {
555 let conn = Connection::connect_to_env().context("Wayland connection")?;
556 let (globals, mut queue) =
557 registry_queue_init::<State>(&conn).context("registre Wayland")?;
558 let qh = queue.handle();
559
560 let shm = globals.bind(&qh, 1..=1, ()).context("wl_shm")?;
561 let copy = globals
562 .bind(&qh, 1..=1, ())
563 .context("ext_image_copy_capture_manager_v1 missing")?;
564 let tl_src = globals.bind(&qh, 1..=1, ()).context(
565 "ext_foreign_toplevel_image_capture_source_manager_v1 missing: \
566 this compositor cannot capture individual windows. The foreign-toplevel \
567 capture source requires wlroots >= 0.20 (Sway >= 1.12); wlroots 0.19 / \
568 Sway 1.11 only expose output capture. Run `wlr-peek doctor` to see what \
569 your compositor supports.",
570 )?;
571 let out_src = globals
572 .bind(&qh, 1..=1, ())
573 .context("ext_output_image_capture_source_manager_v1 missing")?;
574 let _list: ExtForeignToplevelListV1 = globals
575 .bind(&qh, 1..=1, ())
576 .context("ext_foreign_toplevel_list_v1 missing")?;
577
578 let xdg_mgr: Option<ZxdgOutputManagerV1> = globals.bind(&qh, 1..=3, ()).ok();
582
583 globals.contents().with_list(|list| {
584 for g in list {
585 if g.interface == WlOutput::interface().name {
586 let out: WlOutput = globals.registry().bind(g.name, g.version.min(4), &qh, ());
587 if let Some(mgr) = &xdg_mgr {
588 mgr.get_xdg_output(&out, &qh, out.clone());
591 }
592 }
593 }
594 });
595
596 let mut state = State {
597 shm: Some(shm),
598 copy: Some(copy),
599 tl_src: Some(tl_src),
600 out_src: Some(out_src),
601 ..Default::default()
602 };
603 #[cfg(feature = "gpu")]
605 {
606 state.dmabuf = globals.bind(&qh, 3..=4, ()).ok();
607 }
608 queue.roundtrip(&mut state)?;
609 queue.roundtrip(&mut state)?;
610
611 Ok(Self {
612 queue,
613 qh,
614 state,
615 open: HashMap::new(),
616 #[cfg(feature = "gpu")]
617 gbm: None,
618 })
619 }
620
621 pub fn toplevels(&self) -> &[Toplevel] {
623 &self.state.toplevels
624 }
625 pub fn outputs(&self) -> &[Output] {
627 &self.state.outputs
628 }
629
630 pub fn refresh(&mut self) -> Result<()> {
633 self.queue.roundtrip(&mut self.state)?;
634 Ok(())
635 }
636
637 pub fn open_toplevel_session(&mut self, t: &Toplevel) -> Result<SessionId> {
641 let src = self
642 .state
643 .tl_src
644 .as_ref()
645 .unwrap()
646 .create_source(&t.handle, &self.qh, ());
647 self.open_session(src)
648 }
649
650 pub fn open_output_session(&mut self, o: &Output) -> Result<SessionId> {
652 let src = self
653 .state
654 .out_src
655 .as_ref()
656 .unwrap()
657 .create_source(&o.wl_output, &self.qh, ());
658 self.open_session(src)
659 }
660
661 fn open_session(&mut self, src: ExtImageCaptureSourceV1) -> Result<SessionId> {
662 let session =
663 self.state
664 .copy
665 .as_ref()
666 .unwrap()
667 .create_session(&src, Options::empty(), &self.qh, ());
668 let id = session.id();
669 self.state
670 .sessions
671 .insert(id.clone(), SessionData::default());
672
673 loop {
675 self.queue.blocking_dispatch(&mut self.state)?;
676 let d = self.state.sessions.get(&id).unwrap();
677 if d.constraints_done || d.stopped {
678 break;
679 }
680 }
681 if self.state.sessions.get(&id).unwrap().stopped {
682 self.state.sessions.remove(&id);
683 session.destroy();
684 src.destroy();
685 bail!("capture session stopped before first frame");
686 }
687
688 self.open.insert(
689 id.clone(),
690 OpenSession {
691 frame: None,
692 buf: None,
693 session,
694 src,
695 },
696 );
697 Ok(id)
698 }
699
700 pub fn close_session(&mut self, id: &SessionId) {
702 self.open.remove(id); self.state.sessions.remove(id);
704 }
705
706 pub fn capture_output_once(&mut self, output: &Output, budget: Duration) -> Result<Frame> {
709 let id = self.open_output_session(output)?;
710 let r = self.poll_one(&id, budget);
711 self.close_session(&id);
712 r
713 }
714
715 pub fn capture_toplevel_once(
717 &mut self,
718 toplevel: &Toplevel,
719 budget: Duration,
720 ) -> Result<Frame> {
721 let id = self.open_toplevel_session(toplevel)?;
722 let r = self.poll_one(&id, budget);
723 self.close_session(&id);
724 r
725 }
726
727 fn poll_one(&mut self, id: &SessionId, budget: Duration) -> Result<Frame> {
730 let deadline = Instant::now() + budget;
731 loop {
732 let now = Instant::now();
733 if now >= deadline {
734 bail!("capture: timed out");
735 }
736 let step = Duration::from_millis(50).min(deadline - now);
737 let (frames, stopped) = self.poll(step);
738 for (sid, frame) in frames {
739 if &sid == id {
740 return Ok(frame);
741 }
742 }
743 if stopped.iter().any(|s| s == id) {
744 bail!("capture: session stopped before first frame");
745 }
746 }
747 }
748
749 pub fn poll(&mut self, budget: Duration) -> (Vec<(SessionId, Frame)>, Vec<SessionId>) {
758 let ids: Vec<ObjectId> = self.open.keys().cloned().collect();
760 for id in &ids {
761 let armed = self.open.get(id).is_some_and(|o| o.frame.is_some());
762 let dead = self.state.sessions.get(id).is_some_and(|d| d.stopped);
763 if armed || dead {
764 continue;
765 }
766 if self.ensure_buffer(id).is_err() {
767 continue;
768 }
769 if let Some(d) = self.state.sessions.get_mut(id) {
770 d.ready = false;
771 }
772 let os = self.open.get_mut(id).unwrap();
773 let wl_buffer = os.buf.as_ref().unwrap().wl_buffer().clone();
774 let frame = os.session.create_frame(&self.qh, id.clone());
775 frame.attach_buffer(&wl_buffer);
776 frame.capture();
777 os.frame = Some(frame);
778 }
779
780 let _ = self.dispatch_timeout(budget);
782
783 let mut frames = Vec::new();
785 let mut stopped = Vec::new();
786 for id in self.open.keys().cloned().collect::<Vec<_>>() {
787 let (ready, is_stopped, frame_failed) = self
788 .state
789 .sessions
790 .get(&id)
791 .map(|d| (d.ready, d.stopped, d.frame_failed))
792 .unwrap_or((false, false, None));
793
794 if is_stopped {
796 if let Some(os) = self.open.get_mut(&id)
797 && let Some(frame) = os.frame.take()
798 {
799 frame.destroy();
800 }
801 stopped.push(id);
802 continue;
803 }
804
805 if ready {
806 let frame = harvest(self.open[&id].buf.as_ref().unwrap());
807 if let Some(os) = self.open.get_mut(&id)
808 && let Some(f) = os.frame.take()
809 {
810 f.destroy();
811 }
812 if let Some(d) = self.state.sessions.get_mut(&id) {
813 d.ready = false;
814 d.frame_failed = None;
815 }
816 if let Some(frame) = frame {
817 frames.push((id, frame));
818 }
819 } else if let Some(reason) = frame_failed {
820 if let Some(os) = self.open.get_mut(&id)
823 && let Some(f) = os.frame.take()
824 {
825 f.destroy();
826 }
827 if let Some(d) = self.state.sessions.get_mut(&id) {
828 d.frame_failed = None;
829 if matches!(reason, FailureReason::BufferConstraints) {
830 d.dirty = true; }
832 }
833 }
834 }
835 (frames, stopped)
836 }
837
838 fn dispatch_timeout(&mut self, budget: Duration) -> Result<()> {
842 self.queue.dispatch_pending(&mut self.state)?;
843 self.queue.flush()?;
844 let deadline = Instant::now() + budget;
845 loop {
846 let remaining = deadline.saturating_duration_since(Instant::now());
847 if remaining.is_zero() {
848 break;
849 }
850 let Some(guard) = self.queue.prepare_read() else {
851 self.queue.dispatch_pending(&mut self.state)?;
853 continue;
854 };
855 let ts = Timespec {
856 tv_sec: remaining.as_secs() as _,
857 tv_nsec: remaining.subsec_nanos() as _,
858 };
859 let poll_res = {
862 let fd = guard.connection_fd();
863 let mut fds = [PollFd::new(&fd, PollFlags::IN | PollFlags::ERR)];
864 rustix::event::poll(&mut fds, Some(&ts))
865 };
866 match poll_res {
867 Ok(0) => break, Ok(_) => {
869 guard.read().context("reading Wayland events")?;
870 self.queue.dispatch_pending(&mut self.state)?;
871 }
872 Err(rustix::io::Errno::INTR) => continue,
873 Err(e) => return Err(anyhow::anyhow!("poll: {e}")),
874 }
875 }
876 Ok(())
877 }
878
879 fn ensure_buffer(&mut self, id: &SessionId) -> Result<()> {
883 let (w, h, dirty) = {
884 let d = self.state.sessions.get(id).context("session inconnue")?;
885 (d.width, d.height, d.dirty)
886 };
887 let fits = self
888 .open
889 .get(id)
890 .and_then(|o| o.buf.as_ref())
891 .is_some_and(|b| b.matches(w, h));
892 if fits && !dirty {
895 return Ok(());
896 }
897 if w == 0 || h == 0 {
898 bail!("dimensions de capture nulles");
899 }
900
901 #[cfg(feature = "gpu")]
903 let buf = match self.alloc_dmabuf(id, w, h) {
904 Some(b) => b,
905 None => self.alloc_shm(id, w, h)?,
906 };
907 #[cfg(not(feature = "gpu"))]
908 let buf = self.alloc_shm(id, w, h)?;
909 self.open.get_mut(id).context("session non ouverte")?.buf = Some(buf);
911 self.state.sessions.get_mut(id).unwrap().dirty = false;
912 Ok(())
913 }
914
915 fn alloc_shm(&mut self, id: &SessionId, w: u32, h: u32) -> Result<Buf> {
917 let format = self
918 .state
919 .sessions
920 .get(id)
921 .and_then(|d| d.format)
922 .context("compositor offered no shm format")?;
923 let layout = PixelLayout::of(format)
924 .with_context(|| format!("unsupported shm format: {format:?}"))?;
925 let stride = w as usize * layout.bpp; let size = stride * h as usize;
927
928 let fd = rustix::fs::memfd_create("wlr-chooser-shm", rustix::fs::MemfdFlags::CLOEXEC)
929 .context("memfd_create")?;
930 rustix::fs::ftruncate(&fd, size as u64).context("ftruncate")?;
931 let map = unsafe {
932 rustix::mm::mmap(
933 std::ptr::null_mut(),
934 size,
935 rustix::mm::ProtFlags::READ | rustix::mm::ProtFlags::WRITE,
936 rustix::mm::MapFlags::SHARED,
937 &fd,
938 0,
939 )
940 .context("mmap")?
941 };
942 let pool =
943 self.state
944 .shm
945 .as_ref()
946 .unwrap()
947 .create_pool(fd.as_fd(), size as i32, &self.qh, ());
948 let buffer = pool.create_buffer(0, w as i32, h as i32, stride as i32, format, &self.qh, ());
949 Ok(Buf::Shm(ShmBuf {
950 pool,
951 buffer,
952 _fd: fd,
953 map,
954 size,
955 width: w,
956 height: h,
957 stride,
958 format,
959 }))
960 }
961
962 #[cfg(feature = "gpu")]
966 fn alloc_dmabuf(&mut self, id: &SessionId, w: u32, h: u32) -> Option<Buf> {
967 let dmabuf_mgr = self.state.dmabuf.as_ref().cloned()?;
968 let (formats, dev) = {
969 let d = self.state.sessions.get(id)?;
970 (d.dmabuf_formats.clone(), d.dmabuf_dev)
971 };
972 let Some((fourcc, mods)) = pick_dmabuf_format(&formats) else {
973 if debug() {
974 eprintln!("wlr-capture: no usable dma-buf format");
975 }
976 return None;
977 };
978 self.ensure_gbm(dev)?;
979 let gbm = self.gbm.as_ref()?;
980 let gfmt = GbmFormat::try_from(fourcc).ok()?;
981 let qh = &self.qh;
982
983 let alloc_slot = || -> Option<DmaBuf> {
985 let bo = gbm
986 .create_buffer_object_with_modifiers2::<()>(
987 w,
988 h,
989 gfmt,
990 mods.iter().map(|&m| Modifier::from(m)),
991 BufferObjectFlags::RENDERING,
992 )
993 .ok()?;
994 let stride = bo.stride();
995 let offset = bo.offset(0);
996 let modifier: u64 = bo.modifier().into();
997 let fd = bo.fd().ok()?;
998
999 let params = dmabuf_mgr.create_params(qh, ());
1000 params.add(
1001 fd.as_fd(),
1002 0,
1003 offset,
1004 stride,
1005 (modifier >> 32) as u32,
1006 (modifier & 0xffff_ffff) as u32,
1007 );
1008 let buffer = params.create_immed(
1009 w as i32,
1010 h as i32,
1011 fourcc,
1012 zwp_linux_buffer_params_v1::Flags::empty(),
1013 qh,
1014 (),
1015 );
1016 params.destroy();
1017 Some(DmaBuf {
1018 buffer,
1019 bo,
1020 width: w,
1021 height: h,
1022 fourcc,
1023 modifier,
1024 stride,
1025 offset,
1026 })
1027 };
1028
1029 let buf = alloc_slot()?;
1030 if debug() {
1031 eprintln!(
1032 "wlr-chooser: dma-buf {w}x{h} fourcc={fourcc:#010x} modifier={}",
1033 buf.modifier
1034 );
1035 }
1036 Some(Buf::Dmabuf(buf))
1037 }
1038
1039 #[cfg(feature = "gpu")]
1042 fn ensure_gbm(&mut self, dev: Option<u64>) -> Option<()> {
1043 if self.gbm.is_some() {
1044 return Some(());
1045 }
1046 let path = render_node_for(dev);
1047 let file = std::fs::OpenOptions::new()
1048 .read(true)
1049 .write(true)
1050 .open(&path)
1051 .ok()?;
1052 let device = GbmDevice::new(file).ok()?;
1053 if debug() {
1054 eprintln!("wlr-chooser: gbm device {}", path.display());
1055 }
1056 self.gbm = Some(device);
1057 Some(())
1058 }
1059}
1060
1061#[cfg(feature = "gpu")]
1063const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 {
1064 (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24)
1065}
1066
1067#[cfg(feature = "gpu")]
1070fn pick_dmabuf_format(formats: &[(u32, Vec<u64>)]) -> Option<(u32, Vec<u64>)> {
1071 let preferred = [
1072 fourcc(b'X', b'R', b'2', b'4'), fourcc(b'A', b'R', b'2', b'4'), fourcc(b'X', b'B', b'2', b'4'), fourcc(b'A', b'B', b'2', b'4'), ];
1077 for want in preferred {
1078 if PixelLayout::of_fourcc(want).is_none() {
1079 continue;
1080 }
1081 if let Some((_, mods)) = formats.iter().find(|(f, _)| *f == want) {
1082 let usable: Vec<u64> = mods
1083 .iter()
1084 .copied()
1085 .filter(|&m| m != DRM_MOD_INVALID)
1086 .collect();
1087 if !usable.is_empty() {
1088 return Some((want, usable));
1089 }
1090 }
1091 }
1092 None
1093}
1094
1095#[cfg(feature = "gpu")]
1098fn render_node_for(dev: Option<u64>) -> std::path::PathBuf {
1099 use std::path::PathBuf;
1100 let render_nodes = || -> Vec<PathBuf> {
1101 let mut v: Vec<PathBuf> = std::fs::read_dir("/dev/dri")
1102 .into_iter()
1103 .flatten()
1104 .flatten()
1105 .map(|e| e.path())
1106 .filter(|p| {
1107 p.file_name()
1108 .and_then(|n| n.to_str())
1109 .is_some_and(|n| n.starts_with("renderD"))
1110 })
1111 .collect();
1112 v.sort();
1113 v
1114 };
1115 let nodes = render_nodes();
1116 if let Some(dev) = dev {
1117 for p in &nodes {
1118 if rustix::fs::stat(p).is_ok_and(|st| st.st_rdev == dev) {
1119 return p.clone();
1120 }
1121 }
1122 }
1123 nodes
1124 .into_iter()
1125 .next()
1126 .unwrap_or_else(|| PathBuf::from("/dev/dri/renderD128"))
1127}
1128
1129#[cfg(feature = "gpu")]
1131fn debug() -> bool {
1132 std::env::var_os("WLR_CHOOSER_DEBUG").is_some()
1133}
1134
1135fn harvest(buf: &Buf) -> Option<Frame> {
1139 match buf {
1140 Buf::Shm(b) => {
1141 let layout = PixelLayout::of(b.format).expect("format validated at alloc time");
1142 let raw = unsafe { std::slice::from_raw_parts(b.map as *const u8, b.size) };
1143 Some(Frame::Shm(convert(
1144 raw, b.width, b.height, b.stride, &layout,
1145 )))
1146 }
1147 #[cfg(feature = "gpu")]
1148 Buf::Dmabuf(b) => {
1149 let fd = b.bo.fd().ok()?;
1150 Some(Frame::Dmabuf(DmabufFrame {
1151 fd,
1152 width: b.width,
1153 height: b.height,
1154 fourcc: b.fourcc,
1155 modifier: b.modifier,
1156 stride: b.stride,
1157 offset: b.offset,
1158 }))
1159 }
1160 }
1161}
1162
1163fn convert(raw: &[u8], w: u32, h: u32, stride: usize, layout: &PixelLayout) -> CapturedImage {
1165 let (w, h) = (w as usize, h as usize);
1166 let mut rgba = vec![0u8; w * h * 4];
1167 for y in 0..h {
1168 for x in 0..w {
1169 let s = y * stride + x * layout.bpp;
1170 let d = (y * w + x) * 4;
1171 rgba[d] = raw[s + layout.r];
1172 rgba[d + 1] = raw[s + layout.g];
1173 rgba[d + 2] = raw[s + layout.b];
1174 rgba[d + 3] = match layout.a {
1175 Some(a) => raw[s + a],
1176 None => 255,
1177 };
1178 }
1179 }
1180 CapturedImage {
1181 width: w as u32,
1182 height: h as u32,
1183 rgba,
1184 }
1185}
1186
1187#[derive(Default)]
1197struct ActState {
1198 toplevels: Vec<(ZwlrForeignToplevelHandleV1, String, String)>,
1200}
1201
1202pub fn activate_window(app_id: &str, title: &str, dup_index: usize) -> Result<()> {
1207 let conn = Connection::connect_to_env().context("Wayland connection")?;
1208 let (globals, mut queue) =
1209 registry_queue_init::<ActState>(&conn).context("registre Wayland")?;
1210 let qh = queue.handle();
1211 let _mgr: ZwlrForeignToplevelManagerV1 = globals
1212 .bind(&qh, 1..=3, ())
1213 .context("zwlr_foreign_toplevel_manager_v1 missing (unsupported compositor)")?;
1214 let seat: WlSeat = globals.bind(&qh, 1..=8, ()).context("wl_seat missing")?;
1215
1216 let mut st = ActState::default();
1218 queue.roundtrip(&mut st)?;
1219 queue.roundtrip(&mut st)?;
1220
1221 let handle = st
1222 .toplevels
1223 .iter()
1224 .filter(|(_, a, t)| a == app_id && t == title)
1225 .nth(dup_index)
1226 .or_else(|| {
1227 st.toplevels
1228 .iter()
1229 .find(|(_, a, t)| a == app_id && t == title)
1230 })
1231 .map(|(h, _, _)| h.clone())
1232 .with_context(|| format!("window to activate not found: {app_id} / {title}"))?;
1233 handle.activate(&seat);
1234 queue.roundtrip(&mut st)?; Ok(())
1236}
1237
1238pub fn advertised_globals() -> Result<Vec<(String, u32)>> {
1242 let conn = Connection::connect_to_env().context("Wayland connection")?;
1243 let (globals, _queue) = registry_queue_init::<ActState>(&conn).context("registre Wayland")?;
1244 let mut list = Vec::new();
1245 globals.contents().with_list(|globals| {
1246 for g in globals {
1247 list.push((g.interface.clone(), g.version));
1248 }
1249 });
1250 Ok(list)
1251}
1252
1253impl Dispatch<WlRegistry, GlobalListContents> for ActState {
1254 fn event(
1255 _: &mut Self,
1256 _: &WlRegistry,
1257 _: <WlRegistry as Proxy>::Event,
1258 _: &GlobalListContents,
1259 _: &Connection,
1260 _: &QueueHandle<Self>,
1261 ) {
1262 }
1263}
1264
1265impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for ActState {
1266 fn event(
1267 state: &mut Self,
1268 _: &ZwlrForeignToplevelManagerV1,
1269 event: zwlr_foreign_toplevel_manager_v1::Event,
1270 _: &(),
1271 _: &Connection,
1272 _: &QueueHandle<Self>,
1273 ) {
1274 if let zwlr_foreign_toplevel_manager_v1::Event::Toplevel { toplevel } = event {
1275 state
1276 .toplevels
1277 .push((toplevel, String::new(), String::new()));
1278 }
1279 }
1280
1281 event_created_child!(ActState, ZwlrForeignToplevelManagerV1, [
1282 zwlr_foreign_toplevel_manager_v1::EVT_TOPLEVEL_OPCODE => (ZwlrForeignToplevelHandleV1, ()),
1283 ]);
1284}
1285
1286impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for ActState {
1287 fn event(
1288 state: &mut Self,
1289 handle: &ZwlrForeignToplevelHandleV1,
1290 event: zwlr_foreign_toplevel_handle_v1::Event,
1291 _: &(),
1292 _: &Connection,
1293 _: &QueueHandle<Self>,
1294 ) {
1295 use zwlr_foreign_toplevel_handle_v1::Event;
1296 let Some(e) = state.toplevels.iter_mut().find(|(h, _, _)| h == handle) else {
1297 return;
1298 };
1299 match event {
1300 Event::AppId { app_id } => e.1 = app_id,
1301 Event::Title { title } => e.2 = title,
1302 _ => {}
1303 }
1304 }
1305}
1306
1307delegate_noop!(ActState: ignore WlSeat);
1308
1309impl Dispatch<WlRegistry, GlobalListContents> for State {
1312 fn event(
1313 _: &mut Self,
1314 _: &WlRegistry,
1315 _: <WlRegistry as Proxy>::Event,
1316 _: &GlobalListContents,
1317 _: &Connection,
1318 _: &QueueHandle<Self>,
1319 ) {
1320 }
1321}
1322
1323impl Dispatch<ExtForeignToplevelListV1, ()> for State {
1324 fn event(
1325 state: &mut Self,
1326 _: &ExtForeignToplevelListV1,
1327 event: ext_foreign_toplevel_list_v1::Event,
1328 _: &(),
1329 _: &Connection,
1330 _: &QueueHandle<Self>,
1331 ) {
1332 if let ext_foreign_toplevel_list_v1::Event::Toplevel { toplevel } = event {
1333 state.pending.push((toplevel, PendingToplevel::default()));
1334 }
1335 }
1336
1337 event_created_child!(State, ExtForeignToplevelListV1, [
1338 ext_foreign_toplevel_list_v1::EVT_TOPLEVEL_OPCODE => (ExtForeignToplevelHandleV1, ()),
1339 ]);
1340}
1341
1342impl Dispatch<ExtForeignToplevelHandleV1, ()> for State {
1343 fn event(
1344 state: &mut Self,
1345 handle: &ExtForeignToplevelHandleV1,
1346 event: ext_foreign_toplevel_handle_v1::Event,
1347 _: &(),
1348 _: &Connection,
1349 _: &QueueHandle<Self>,
1350 ) {
1351 use ext_foreign_toplevel_handle_v1::Event;
1352 let Some((_, p)) = state.pending.iter_mut().find(|(h, _)| h == handle) else {
1353 return;
1354 };
1355 match event {
1356 Event::Identifier { identifier } => p.identifier = identifier,
1357 Event::Title { title } => p.title = title,
1358 Event::AppId { app_id } => p.app_id = app_id,
1359 Event::Done => {
1360 if let Some(pos) = state.pending.iter().position(|(h, _)| h == handle) {
1361 let (h, p) = state.pending.remove(pos);
1362 state.toplevels.push(Toplevel {
1363 handle: h,
1364 identifier: p.identifier,
1365 title: p.title,
1366 app_id: p.app_id,
1367 });
1368 }
1369 }
1370 Event::Closed => {
1371 state.pending.retain(|(h, _)| h != handle);
1372 state.toplevels.retain(|t| &t.handle != handle);
1373 }
1374 _ => {}
1375 }
1376 }
1377}
1378
1379impl State {
1380 fn output_entry(&mut self, output: &WlOutput) -> &mut Output {
1383 if let Some(i) = self.outputs.iter().position(|o| &o.wl_output == output) {
1384 return &mut self.outputs[i];
1385 }
1386 self.outputs.push(Output {
1387 wl_output: output.clone(),
1388 name: String::new(),
1389 logical_x: 0,
1390 logical_y: 0,
1391 logical_w: 0,
1392 logical_h: 0,
1393 phys_width: 0,
1394 phys_height: 0,
1395 scale: 1,
1396 transform: Transform::Normal,
1397 have_xdg: false,
1398 });
1399 self.outputs.last_mut().unwrap()
1400 }
1401}
1402
1403impl Dispatch<WlOutput, ()> for State {
1404 fn event(
1405 state: &mut Self,
1406 output: &WlOutput,
1407 event: <WlOutput as Proxy>::Event,
1408 _: &(),
1409 _: &Connection,
1410 _: &QueueHandle<Self>,
1411 ) {
1412 use wayland_client::protocol::wl_output::Event;
1413 match event {
1414 Event::Geometry {
1415 x, y, transform, ..
1416 } => {
1417 let o = state.output_entry(output);
1418 o.transform = transform.into_result().unwrap_or(Transform::Normal);
1419 if !o.have_xdg {
1421 o.logical_x = x;
1422 o.logical_y = y;
1423 }
1424 }
1425 Event::Mode {
1427 flags,
1428 width,
1429 height,
1430 ..
1431 } => {
1432 if flags
1433 .into_result()
1434 .is_ok_and(|f| f.contains(wl_output::Mode::Current))
1435 {
1436 let o = state.output_entry(output);
1437 o.phys_width = width;
1438 o.phys_height = height;
1439 }
1440 }
1441 Event::Scale { factor } => {
1442 state.output_entry(output).scale = factor.max(1);
1443 }
1444 Event::Name { name } => {
1445 state.output_entry(output).name = name;
1446 }
1447 _ => {}
1448 }
1449 }
1450}
1451
1452impl Dispatch<ZxdgOutputV1, WlOutput> for State {
1453 fn event(
1454 state: &mut Self,
1455 _: &ZxdgOutputV1,
1456 event: <ZxdgOutputV1 as Proxy>::Event,
1457 wl_output: &WlOutput,
1458 _: &Connection,
1459 _: &QueueHandle<Self>,
1460 ) {
1461 use zxdg_output_v1::Event;
1462 match event {
1463 Event::LogicalPosition { x, y } => {
1464 let o = state.output_entry(wl_output);
1465 o.logical_x = x;
1466 o.logical_y = y;
1467 o.have_xdg = true;
1468 }
1469 Event::LogicalSize { width, height } => {
1470 let o = state.output_entry(wl_output);
1471 o.logical_w = width;
1472 o.logical_h = height;
1473 o.have_xdg = true;
1474 }
1475 _ => {}
1476 }
1477 }
1478}
1479
1480impl Dispatch<ExtImageCopyCaptureSessionV1, ()> for State {
1481 fn event(
1482 state: &mut Self,
1483 session: &ExtImageCopyCaptureSessionV1,
1484 event: ext_image_copy_capture_session_v1::Event,
1485 _: &(),
1486 _: &Connection,
1487 _: &QueueHandle<Self>,
1488 ) {
1489 use ext_image_copy_capture_session_v1::Event;
1490 let Some(d) = state.sessions.get_mut(&session.id()) else {
1491 return;
1492 };
1493 match event {
1494 Event::BufferSize { width, height } => {
1495 d.width = width;
1496 d.height = height;
1497 }
1498 Event::ShmFormat {
1499 format: WEnum::Value(f),
1500 } => d.format = Some(f),
1501 #[cfg(feature = "gpu")]
1502 Event::DmabufDevice { device } => {
1503 if device.len() == 8 {
1505 let mut b = [0u8; 8];
1506 b.copy_from_slice(&device);
1507 d.dmabuf_dev = Some(u64::from_ne_bytes(b));
1508 }
1509 }
1510 #[cfg(feature = "gpu")]
1511 Event::DmabufFormat { format, modifiers } => {
1512 let mods = modifiers
1514 .chunks_exact(8)
1515 .map(|c| u64::from_ne_bytes(c.try_into().unwrap()))
1516 .collect();
1517 d.dmabuf_formats.push((format, mods));
1518 }
1519 Event::Done => {
1522 d.constraints_done = true;
1523 d.dirty = true;
1524 }
1525 Event::Stopped => d.stopped = true,
1526 _ => {}
1527 }
1528 }
1529}
1530
1531impl Dispatch<ExtImageCopyCaptureFrameV1, ObjectId> for State {
1532 fn event(
1533 state: &mut Self,
1534 _: &ExtImageCopyCaptureFrameV1,
1535 event: ext_image_copy_capture_frame_v1::Event,
1536 session_id: &ObjectId,
1537 _: &Connection,
1538 _: &QueueHandle<Self>,
1539 ) {
1540 use ext_image_copy_capture_frame_v1::Event;
1541 let Some(d) = state.sessions.get_mut(session_id) else {
1542 return;
1543 };
1544 match event {
1545 Event::Ready => d.ready = true,
1546 Event::Failed { reason } => {
1547 let reason = match reason {
1548 WEnum::Value(r) => r,
1549 _ => FailureReason::Unknown,
1550 };
1551 if matches!(reason, FailureReason::Stopped) {
1554 d.stopped = true;
1555 } else {
1556 d.frame_failed = Some(reason);
1557 }
1558 }
1559 _ => {}
1560 }
1561 }
1562}
1563
1564#[cfg(test)]
1565mod tests {
1566 use super::*;
1567 use wayland_client::protocol::wl_shm::Format;
1568
1569 #[test]
1572 fn pixel_layout_stride_and_alpha() {
1573 assert_eq!(PixelLayout::of(Format::Bgr888).unwrap().bpp, 3);
1574 assert_eq!(PixelLayout::of(Format::Rgb888).unwrap().bpp, 3);
1575 assert_eq!(PixelLayout::of(Format::Xrgb8888).unwrap().bpp, 4);
1576 assert_eq!(PixelLayout::of(Format::Argb8888).unwrap().bpp, 4);
1577
1578 assert!(PixelLayout::of(Format::Bgr888).unwrap().a.is_none());
1579 assert!(PixelLayout::of(Format::Xrgb8888).unwrap().a.is_none());
1580 assert_eq!(PixelLayout::of(Format::Argb8888).unwrap().a, Some(3));
1581 assert_eq!(PixelLayout::of(Format::Abgr8888).unwrap().a, Some(3));
1582 }
1583
1584 #[test]
1585 fn pixel_layout_unknown_format_is_none() {
1586 assert!(PixelLayout::of(Format::C8).is_none());
1588 }
1589
1590 #[test]
1591 fn region_intersect() {
1592 let a = Region {
1593 x: 0,
1594 y: 0,
1595 w: 10,
1596 h: 10,
1597 };
1598 let b = Region {
1599 x: 5,
1600 y: 5,
1601 w: 10,
1602 h: 10,
1603 };
1604 assert_eq!(
1605 a.intersect(&b),
1606 Some(Region {
1607 x: 5,
1608 y: 5,
1609 w: 5,
1610 h: 5
1611 })
1612 );
1613 let c = Region {
1615 x: -3,
1616 y: -3,
1617 w: 6,
1618 h: 6,
1619 };
1620 assert_eq!(
1621 a.intersect(&c),
1622 Some(Region {
1623 x: 0,
1624 y: 0,
1625 w: 3,
1626 h: 3
1627 })
1628 );
1629 let d = Region {
1631 x: 100,
1632 y: 100,
1633 w: 1,
1634 h: 1,
1635 };
1636 assert_eq!(a.intersect(&d), None);
1637 }
1638
1639 fn img_2x2() -> CapturedImage {
1641 CapturedImage {
1642 width: 2,
1643 height: 2,
1644 rgba: vec![
1645 1, 1, 1, 255, 2, 2, 2, 255, 3, 3, 3, 255, 4, 4, 4, 255, ],
1648 }
1649 }
1650
1651 #[test]
1652 fn captured_pixel_and_crop() {
1653 let img = img_2x2();
1654 assert_eq!(img.pixel(0, 0), Some([1, 1, 1, 255]));
1655 assert_eq!(img.pixel(1, 1), Some([4, 4, 4, 255]));
1656 assert_eq!(img.pixel(2, 0), None); let c = img.crop(Region {
1660 x: 1,
1661 y: 1,
1662 w: 1,
1663 h: 1,
1664 });
1665 assert_eq!((c.width, c.height), (1, 1));
1666 assert_eq!(c.rgba, vec![4, 4, 4, 255]);
1667
1668 let c2 = img.crop(Region {
1670 x: 1,
1671 y: 0,
1672 w: 5,
1673 h: 5,
1674 });
1675 assert_eq!((c2.width, c2.height), (1, 2));
1676 assert_eq!(c2.rgba, vec![2, 2, 2, 255, 4, 4, 4, 255]);
1677 }
1678
1679 #[test]
1680 fn captured_blit_into() {
1681 let img = img_2x2();
1683 let (dw, dh) = (3u32, 2u32);
1684 let mut dst = vec![0u8; (dw * dh * 4) as usize];
1685 img.blit_into(&mut dst, dw, dh, 1, 0);
1686 assert_eq!(&dst[0..4], &[0, 0, 0, 0]); assert_eq!(&dst[4..8], &[1, 1, 1, 255]); assert_eq!(&dst[8..12], &[2, 2, 2, 255]); assert_eq!(&dst[12..16], &[0, 0, 0, 0]); assert_eq!(&dst[16..20], &[3, 3, 3, 255]); }
1693
1694 #[test]
1695 fn output_logical_dims_transform() {
1696 assert_eq!(logical_dims(3840, 2160, 2, Transform::Normal), (1920, 1080));
1698 assert_eq!(logical_dims(3840, 2160, 2, Transform::_90), (1080, 1920));
1700 assert_eq!(
1701 logical_dims(3840, 2160, 2, Transform::Flipped270),
1702 (1080, 1920)
1703 );
1704 assert_eq!(logical_dims(1000, 500, 0, Transform::_180), (1000, 500));
1706 }
1707}
1708
1709delegate_noop!(State: ignore ZxdgOutputManagerV1);
1711delegate_noop!(State: ignore WlShm);
1712delegate_noop!(State: ignore WlShmPool);
1713delegate_noop!(State: ignore WlBuffer);
1714delegate_noop!(State: ignore ExtImageCaptureSourceV1);
1715delegate_noop!(State: ignore ExtForeignToplevelImageCaptureSourceManagerV1);
1716delegate_noop!(State: ignore ExtOutputImageCaptureSourceManagerV1);
1717delegate_noop!(State: ignore ExtImageCopyCaptureManagerV1);
1718#[cfg(feature = "gpu")]
1722delegate_noop!(State: ignore ZwpLinuxDmabufV1);
1723#[cfg(feature = "gpu")]
1724delegate_noop!(State: ignore ZwpLinuxBufferParamsV1);