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 context: Arc<GraphicsContext>,
62 buffer: wgpu::Buffer,
64 dimensions: (u32, u32),
66 bytes_per_row: u32,
68 format: wgpu::TextureFormat,
70}
71
72impl GpuReadback {
73 pub fn from_texture(context: Arc<GraphicsContext>, texture: &wgpu::Texture) -> Result<Self, ReadbackError> {
77 let size = texture.size();
78 let dimensions = (size.width, size.height);
79 let format = texture.format();
80
81 if dimensions.0 == 0 || dimensions.1 == 0 {
83 return Err(ReadbackError::InvalidDimensions);
84 }
85
86 let bytes_per_pixel = match format {
88 wgpu::TextureFormat::Rgba8Unorm
89 | wgpu::TextureFormat::Rgba8UnormSrgb
90 | wgpu::TextureFormat::Bgra8Unorm
91 | wgpu::TextureFormat::Bgra8UnormSrgb => 4,
92 wgpu::TextureFormat::Rgb10a2Unorm => 4,
93 _ => return Err(ReadbackError::UnsupportedFormat),
94 };
95
96 let unpadded_bytes_per_row = dimensions.0 * bytes_per_pixel;
97 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
98 let bytes_per_row = ((unpadded_bytes_per_row + align - 1) / align) * align;
99
100 let buffer_size = (bytes_per_row * dimensions.1) as wgpu::BufferAddress;
102 let buffer = context.device.create_buffer(&wgpu::BufferDescriptor {
103 label: Some("readback_buffer"),
104 size: buffer_size,
105 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
106 mapped_at_creation: false,
107 });
108
109 let mut encoder = context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
111 label: Some("readback_encoder"),
112 });
113
114 encoder.copy_texture_to_buffer(
115 wgpu::TexelCopyTextureInfo {
116 texture,
117 mip_level: 0,
118 origin: wgpu::Origin3d::ZERO,
119 aspect: wgpu::TextureAspect::All,
120 },
121 wgpu::TexelCopyBufferInfo {
122 buffer: &buffer,
123 layout: wgpu::TexelCopyBufferLayout {
124 offset: 0,
125 bytes_per_row: Some(bytes_per_row),
126 rows_per_image: Some(dimensions.1),
127 },
128 },
129 size,
130 );
131
132 context.queue.submit(Some(encoder.finish()));
133
134 Ok(Self {
135 context,
136 buffer,
137 dimensions,
138 bytes_per_row,
139 format,
140 })
141 }
142
143 pub fn read(&self) -> Result<Vec<u8>, ReadbackError> {
149 let buffer_slice = self.buffer.slice(..);
150
151 buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
153
154 let data = buffer_slice.get_mapped_range();
159 let bytes_per_pixel = 4; let mut result = Vec::with_capacity((self.dimensions.0 * self.dimensions.1 * bytes_per_pixel) as usize);
161
162 for y in 0..self.dimensions.1 {
164 let row_start = (y * self.bytes_per_row) as usize;
165 let row_end = row_start + (self.dimensions.0 * bytes_per_pixel) as usize;
166 result.extend_from_slice(&data[row_start..row_end]);
167 }
168
169 drop(data);
170 self.buffer.unmap();
171
172 Ok(result)
173 }
174
175 #[cfg(feature = "image")]
177 pub fn save_png(&self, path: impl AsRef<std::path::Path>) -> Result<(), ReadbackError> {
178 let data = self.read()?;
179
180 let img = image::RgbaImage::from_raw(self.dimensions.0, self.dimensions.1, data)
182 .ok_or(ReadbackError::EncodeFailed(
183 "Failed to create image from raw data".to_string(),
184 ))?;
185
186 img.save(path)
188 .map_err(|e| ReadbackError::IoError(format!("{}", e)))?;
189
190 Ok(())
191 }
192
193 pub fn dimensions(&self) -> (u32, u32) {
195 self.dimensions
196 }
197
198 pub fn format(&self) -> wgpu::TextureFormat {
200 self.format
201 }
202}
203
204pub trait ReadbackExt {
206 fn capture_texture(&self, texture: &wgpu::Texture) -> Result<GpuReadback, ReadbackError>;
208}
209
210impl ReadbackExt for Arc<GraphicsContext> {
211 fn capture_texture(&self, texture: &wgpu::Texture) -> Result<GpuReadback, ReadbackError> {
212 GpuReadback::from_texture(self.clone(), texture)
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_readback_error_display() {
222 let err = ReadbackError::MapFailed("test".to_string());
223 assert!(format!("{}", err).contains("Buffer mapping failed"));
224
225 let err = ReadbackError::InvalidDimensions;
226 assert!(format!("{}", err).contains("Invalid dimensions"));
227 }
228
229 #[test]
230 fn test_bytes_per_row_alignment() {
231 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
233
234 let unpadded = 100 * 4;
236 let padded = ((unpadded + align - 1) / align) * align;
237
238 assert_eq!(padded, 512);
240 assert_eq!(padded % align, 0);
241 }
242
243 #[test]
244 fn test_readback_dimensions() {
245 assert!(matches!(
248 ReadbackError::InvalidDimensions,
249 ReadbackError::InvalidDimensions
250 ));
251 }
252}