astrelis_render/
readback.rs1use std::sync::Arc;
23
24use crate::GraphicsContext;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ReadbackError {
29 MapFailed(String),
31 CopyFailed(String),
33 EncodeFailed(String),
35 IoError(String),
37 InvalidDimensions,
39 UnsupportedFormat,
41}
42
43impl std::fmt::Display for ReadbackError {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::MapFailed(msg) => write!(f, "Buffer mapping failed: {}", msg),
47 Self::CopyFailed(msg) => write!(f, "Texture copy failed: {}", msg),
48 Self::EncodeFailed(msg) => write!(f, "Image encoding failed: {}", msg),
49 Self::IoError(msg) => write!(f, "IO error: {}", msg),
50 Self::InvalidDimensions => write!(f, "Invalid dimensions for readback"),
51 Self::UnsupportedFormat => write!(f, "Unsupported texture format for readback"),
52 }
53 }
54}
55
56impl std::error::Error for ReadbackError {}
57
58pub struct GpuReadback {
60 buffer: wgpu::Buffer,
62 dimensions: (u32, u32),
64 bytes_per_row: u32,
66 format: wgpu::TextureFormat,
68}
69
70impl GpuReadback {
71 pub fn from_texture(
75 context: Arc<GraphicsContext>,
76 texture: &wgpu::Texture,
77 ) -> Result<Self, ReadbackError> {
78 let size = texture.size();
79 let dimensions = (size.width, size.height);
80 let format = texture.format();
81
82 if dimensions.0 == 0 || dimensions.1 == 0 {
84 return Err(ReadbackError::InvalidDimensions);
85 }
86
87 let bytes_per_pixel = match format {
89 wgpu::TextureFormat::Rgba8Unorm
90 | wgpu::TextureFormat::Rgba8UnormSrgb
91 | wgpu::TextureFormat::Bgra8Unorm
92 | wgpu::TextureFormat::Bgra8UnormSrgb => 4,
93 wgpu::TextureFormat::Rgb10a2Unorm => 4,
94 _ => return Err(ReadbackError::UnsupportedFormat),
95 };
96
97 let unpadded_bytes_per_row = dimensions.0 * bytes_per_pixel;
98 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
99 let bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
100
101 let buffer_size = (bytes_per_row * dimensions.1) as wgpu::BufferAddress;
103 let buffer = context.device().create_buffer(&wgpu::BufferDescriptor {
104 label: Some("readback_buffer"),
105 size: buffer_size,
106 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
107 mapped_at_creation: false,
108 });
109
110 let mut encoder =
112 context
113 .device()
114 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
115 label: Some("readback_encoder"),
116 });
117
118 encoder.copy_texture_to_buffer(
119 wgpu::TexelCopyTextureInfo {
120 texture,
121 mip_level: 0,
122 origin: wgpu::Origin3d::ZERO,
123 aspect: wgpu::TextureAspect::All,
124 },
125 wgpu::TexelCopyBufferInfo {
126 buffer: &buffer,
127 layout: wgpu::TexelCopyBufferLayout {
128 offset: 0,
129 bytes_per_row: Some(bytes_per_row),
130 rows_per_image: Some(dimensions.1),
131 },
132 },
133 size,
134 );
135
136 context.queue().submit(Some(encoder.finish()));
137
138 Ok(Self {
139 buffer,
140 dimensions,
141 bytes_per_row,
142 format,
143 })
144 }
145
146 pub fn read(&self) -> Result<Vec<u8>, ReadbackError> {
152 let buffer_slice = self.buffer.slice(..);
153
154 buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
156
157 let data = buffer_slice.get_mapped_range();
162 let bytes_per_pixel = 4; let mut result =
164 Vec::with_capacity((self.dimensions.0 * self.dimensions.1 * bytes_per_pixel) as usize);
165
166 for y in 0..self.dimensions.1 {
168 let row_start = (y * self.bytes_per_row) as usize;
169 let row_end = row_start + (self.dimensions.0 * bytes_per_pixel) as usize;
170 result.extend_from_slice(&data[row_start..row_end]);
171 }
172
173 drop(data);
174 self.buffer.unmap();
175
176 Ok(result)
177 }
178
179 #[cfg(feature = "image")]
181 pub fn save_png(&self, path: impl AsRef<std::path::Path>) -> Result<(), ReadbackError> {
182 let data = self.read()?;
183
184 let img = image::RgbaImage::from_raw(self.dimensions.0, self.dimensions.1, data).ok_or(
186 ReadbackError::EncodeFailed("Failed to create image from raw data".to_string()),
187 )?;
188
189 img.save(path)
191 .map_err(|e| ReadbackError::IoError(format!("{}", e)))?;
192
193 Ok(())
194 }
195
196 pub fn dimensions(&self) -> (u32, u32) {
198 self.dimensions
199 }
200
201 pub fn format(&self) -> wgpu::TextureFormat {
203 self.format
204 }
205}
206
207pub trait ReadbackExt {
209 fn capture_texture(&self, texture: &wgpu::Texture) -> Result<GpuReadback, ReadbackError>;
211}
212
213impl ReadbackExt for Arc<GraphicsContext> {
214 fn capture_texture(&self, texture: &wgpu::Texture) -> Result<GpuReadback, ReadbackError> {
215 GpuReadback::from_texture(self.clone(), texture)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_readback_error_display() {
225 let err = ReadbackError::MapFailed("test".to_string());
226 assert!(format!("{}", err).contains("Buffer mapping failed"));
227
228 let err = ReadbackError::InvalidDimensions;
229 assert!(format!("{}", err).contains("Invalid dimensions"));
230 }
231
232 #[test]
233 fn test_bytes_per_row_alignment() {
234 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
236
237 let unpadded: u32 = 100 * 4;
239 let padded = unpadded.div_ceil(align) * align;
240
241 assert_eq!(padded, 512);
243 assert_eq!(padded % align, 0);
244 }
245
246 #[test]
247 fn test_readback_dimensions() {
248 assert!(matches!(
251 ReadbackError::InvalidDimensions,
252 ReadbackError::InvalidDimensions
253 ));
254 }
255}