miniscreenshot_wgpu/
lib.rs1#[cfg(all(feature = "wgpu-28", feature = "wgpu-29"))]
31compile_error!("features `wgpu-28` and `wgpu-29` are mutually exclusive; enable exactly one");
32#[cfg(not(any(feature = "wgpu-28", feature = "wgpu-29")))]
33compile_error!("one of `wgpu-28` or `wgpu-29` must be enabled for miniscreenshot-wgpu");
34
35#[cfg(feature = "wgpu-28")]
40pub use wgpu_28 as wgpu;
41#[cfg(feature = "wgpu-29")]
42pub use wgpu_29 as wgpu;
43
44pub use miniscreenshot::{Capture, CaptureError, Screenshot};
45
46#[derive(Debug)]
50pub enum WgpuCaptureError {
51 UnsupportedFormat(wgpu::TextureFormat),
56
57 MapFailed(wgpu::BufferAsyncError),
59
60 PollFailed(wgpu::PollError),
62}
63
64impl std::fmt::Display for WgpuCaptureError {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 Self::UnsupportedFormat(fmt) => {
68 write!(f, "unsupported texture format for screenshot: {fmt:?}")
69 }
70 Self::MapFailed(e) => write!(f, "staging buffer map failed: {e}"),
71 Self::PollFailed(e) => write!(f, "device poll failed: {e}"),
72 }
73 }
74}
75
76impl std::error::Error for WgpuCaptureError {
77 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
78 match self {
79 Self::MapFailed(e) => Some(e),
80 Self::PollFailed(e) => Some(e),
81 _ => None,
82 }
83 }
84}
85
86impl From<WgpuCaptureError> for CaptureError {
87 fn from(e: WgpuCaptureError) -> Self {
88 match e {
89 WgpuCaptureError::UnsupportedFormat(fmt) => CaptureError::new(
90 miniscreenshot::CaptureErrorKind::Unsupported,
91 format!("unsupported texture format: {fmt:?}"),
92 )
93 .with_source(WgpuCaptureError::UnsupportedFormat(fmt)),
94 WgpuCaptureError::MapFailed(e) => CaptureError::new(
95 miniscreenshot::CaptureErrorKind::Backend,
96 format!("staging buffer map failed: {e}"),
97 )
98 .with_source(WgpuCaptureError::MapFailed(e)),
99 WgpuCaptureError::PollFailed(e) => CaptureError::new(
100 miniscreenshot::CaptureErrorKind::Backend,
101 format!("device poll failed: {e}"),
102 )
103 .with_source(WgpuCaptureError::PollFailed(e)),
104 }
105 }
106}
107
108pub struct WgpuCapture<'a> {
120 device: &'a wgpu::Device,
121 queue: &'a wgpu::Queue,
122 texture: &'a wgpu::Texture,
123}
124
125impl<'a> WgpuCapture<'a> {
126 pub fn new(
128 device: &'a wgpu::Device,
129 queue: &'a wgpu::Queue,
130 texture: &'a wgpu::Texture,
131 ) -> Self {
132 Self {
133 device,
134 queue,
135 texture,
136 }
137 }
138}
139
140impl Capture for WgpuCapture<'_> {
141 type Error = CaptureError;
142
143 fn capture(&mut self) -> Result<Screenshot, CaptureError> {
144 capture(self.device, self.queue, self.texture).map_err(CaptureError::from)
145 }
146}
147
148pub fn capture(
166 device: &wgpu::Device,
167 queue: &wgpu::Queue,
168 texture: &wgpu::Texture,
169) -> Result<Screenshot, WgpuCaptureError> {
170 let size = texture.size();
171 let width = size.width;
172 let height = size.height;
173 let format = texture.format();
174
175 let is_bgra = match format {
177 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => false,
178 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => true,
179 _ => return Err(WgpuCaptureError::UnsupportedFormat(format)),
180 };
181
182 let bytes_per_row = padded_bytes_per_row(width);
183 let buffer_size = u64::from(bytes_per_row) * u64::from(height);
184
185 let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
187 label: Some("miniscreenshot_staging_buffer"),
188 size: buffer_size,
189 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
190 mapped_at_creation: false,
191 });
192
193 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
195 label: Some("miniscreenshot_encoder"),
196 });
197 encoder.copy_texture_to_buffer(
198 texture.as_image_copy(),
199 wgpu::TexelCopyBufferInfo {
200 buffer: &staging_buffer,
201 layout: wgpu::TexelCopyBufferLayout {
202 offset: 0,
203 bytes_per_row: Some(bytes_per_row),
204 rows_per_image: Some(height),
205 },
206 },
207 size,
208 );
209 queue.submit(std::iter::once(encoder.finish()));
210
211 let buffer_slice = staging_buffer.slice(..);
213 let (tx, rx) = std::sync::mpsc::channel();
214 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
215 let _ = tx.send(result);
216 });
217 device
218 .poll(wgpu::PollType::wait_indefinitely())
219 .map_err(WgpuCaptureError::PollFailed)?;
220 rx.recv()
221 .expect("map_async callback channel closed unexpectedly")
222 .map_err(WgpuCaptureError::MapFailed)?;
223
224 let mapped = buffer_slice.get_mapped_range();
226 let raw: &[u8] = &mapped;
227 let mut rgba = Vec::with_capacity(width as usize * height as usize * 4);
228 for row_idx in 0..height as usize {
229 let row_start = row_idx * bytes_per_row as usize;
230 let row_end = row_start + width as usize * 4;
231 let row = &raw[row_start..row_end];
232 if is_bgra {
233 for pixel in row.chunks_exact(4) {
234 rgba.push(pixel[2]); rgba.push(pixel[1]); rgba.push(pixel[0]); rgba.push(pixel[3]); }
239 } else {
240 rgba.extend_from_slice(row);
241 }
242 }
243 drop(mapped);
244 staging_buffer.unmap();
245
246 Ok(Screenshot::from_rgba(width, height, rgba))
247}
248
249fn padded_bytes_per_row(width: u32) -> u32 {
254 let unpadded = width * 4;
255 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
256 unpadded.div_ceil(align) * align
257}
258
259#[cfg(test)]
260mod tests {
261 use super::padded_bytes_per_row;
262
263 #[test]
264 fn padding_aligns_to_256() {
265 assert_eq!(padded_bytes_per_row(1), 256);
267 assert_eq!(padded_bytes_per_row(64), 256);
269 assert_eq!(padded_bytes_per_row(65), 512);
271 assert_eq!(padded_bytes_per_row(128), 512);
273 }
274}