agent_seat_linux/seat/
capture.rs1use std::os::fd::AsRawFd;
12
13use super::proxy::Conn;
14
15const WL_SHM_FORMAT_ARGB8888: u32 = 0;
17const WL_SHM_FORMAT_XRGB8888: u32 = 1;
18
19#[derive(Debug)]
21pub struct CapturedFrame {
22 pub image: image::DynamicImage,
24 #[allow(dead_code)]
26 pub width: u32,
27 #[allow(dead_code)]
29 pub height: u32,
30}
31
32struct ShmFrameRef<'a> {
34 fd: &'a std::os::fd::OwnedFd,
35 pool_size: i32,
36 offset: i32,
37 width: i32,
38 height: i32,
39 stride: i32,
40 format: u32,
41}
42
43fn primary_shm_frame(conn: &Conn) -> Option<ShmFrameRef<'_>> {
52 let mut best: Option<(i64, u64, ShmFrameRef<'_>)> = None;
53 for state in conn.surfaces.values() {
54 let Some(attached) = state.attached.as_ref() else {
55 continue;
56 };
57 let frame = ShmFrameRef {
58 fd: &attached.fd,
59 pool_size: attached.pool_size,
60 offset: attached.offset,
61 width: attached.width,
62 height: attached.height,
63 stride: attached.stride,
64 format: attached.format,
65 };
66 let area = i64::from(attached.width.max(0)) * i64::from(attached.height.max(0));
67 match &best {
68 Some((best_area, best_commits, _))
69 if (*best_area, *best_commits) >= (area, state.commit_count) => {}
70 _ => best = Some((area, state.commit_count, frame)),
71 }
72 }
73 best.map(|(_, _, frame)| frame)
74}
75
76pub(crate) fn capture_frame(conn: &Conn) -> Result<CapturedFrame, String> {
80 let frame = primary_shm_frame(conn).ok_or_else(|| {
81 "the app has no readable shm frame yet (it may use GPU buffers)".to_string()
82 })?;
83 let ShmFrameRef {
84 fd,
85 pool_size,
86 offset,
87 width,
88 height,
89 stride,
90 format,
91 } = frame;
92
93 if offset < 0 || width <= 0 || height <= 0 || stride <= 0 || pool_size <= 0 {
94 return Err(format!(
95 "invalid shm frame offset={offset}, geometry={width}x{height}, stride={stride}, pool={pool_size}"
96 ));
97 }
98
99 let need = (offset as i64) + (stride as i64) * (height as i64);
100 if need > pool_size as i64 {
101 return Err(format!(
102 "frame ({need} bytes) exceeds shm pool size ({} bytes)",
103 pool_size
104 ));
105 }
106
107 let fd = fd.as_raw_fd();
109 let mapped = unsafe {
112 libc::mmap(
113 std::ptr::null_mut(),
114 pool_size as usize,
115 libc::PROT_READ,
116 libc::MAP_SHARED,
117 fd,
118 0,
119 )
120 };
121 if mapped == libc::MAP_FAILED {
122 return Err(format!(
123 "mmap of the shm pool failed: {}",
124 std::io::Error::last_os_error()
125 ));
126 }
127
128 let result = decode_frame(mapped as *const u8, offset, width, height, stride, format);
129
130 unsafe {
133 libc::munmap(mapped, pool_size as usize);
134 }
135
136 let image = result?;
137 Ok(CapturedFrame {
138 width: width as u32,
139 height: height as u32,
140 image,
141 })
142}
143
144fn decode_frame(
146 base: *const u8,
147 offset: i32,
148 width: i32,
149 height: i32,
150 stride: i32,
151 format: u32,
152) -> Result<image::DynamicImage, String> {
153 match format {
154 WL_SHM_FORMAT_ARGB8888 | WL_SHM_FORMAT_XRGB8888 => {}
155 other => {
156 return Err(format!(
157 "unsupported wl_shm format {other:#x} (only ARGB8888/XRGB8888 are decoded)"
158 ))
159 }
160 }
161
162 let w = width as usize;
163 let h = height as usize;
164 let stride = stride as usize;
165 let mut rgb = vec![0u8; w * h * 3];
166 for y in 0..h {
167 let row = unsafe {
171 std::slice::from_raw_parts(base.add(offset as usize + y * stride), stride.min(w * 4))
172 };
173 for x in 0..w {
174 let px = x * 4;
175 if px + 2 >= row.len() {
176 break;
177 }
178 let b = row[px];
180 let g = row[px + 1];
181 let r = row[px + 2];
182 let dst = (y * w + x) * 3;
183 rgb[dst] = r;
184 rgb[dst + 1] = g;
185 rgb[dst + 2] = b;
186 }
187 }
188 let img = image::RgbImage::from_raw(w as u32, h as u32, rgb)
189 .ok_or_else(|| "failed to assemble the captured frame".to_string())?;
190 Ok(image::DynamicImage::ImageRgb8(img))
191}