1use wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as COPY_ROW_ALIGNMENT;
16
17pub struct HeadlessTarget {
20 width: u32,
21 height: u32,
22 format: wgpu::TextureFormat,
23 texture: wgpu::Texture,
24 view: wgpu::TextureView,
25}
26
27impl HeadlessTarget {
28 pub fn new(
30 device: &wgpu::Device,
31 width: u32,
32 height: u32,
33 format: wgpu::TextureFormat,
34 ) -> Self {
35 let texture = device.create_texture(&wgpu::TextureDescriptor {
36 label: Some("frust-gpu headless target"),
37 size: wgpu::Extent3d {
38 width,
39 height,
40 depth_or_array_layers: 1,
41 },
42 mip_level_count: 1,
43 sample_count: 1,
44 dimension: wgpu::TextureDimension::D2,
45 format,
46 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
47 view_formats: &[],
48 });
49 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
50 Self {
51 width,
52 height,
53 format,
54 texture,
55 view,
56 }
57 }
58
59 pub fn width(&self) -> u32 {
61 self.width
62 }
63
64 pub fn height(&self) -> u32 {
66 self.height
67 }
68
69 pub fn format(&self) -> wgpu::TextureFormat {
71 self.format
72 }
73
74 pub fn view(&self) -> &wgpu::TextureView {
76 &self.view
77 }
78
79 pub fn texture(&self) -> &wgpu::Texture {
81 &self.texture
82 }
83
84 pub fn read_back(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> Vec<u8> {
95 let bytes_per_pixel = self
96 .format
97 .block_copy_size(None)
98 .expect("a headless render target's format must have a defined block size");
99 let bytes_per_row = padded_bytes_per_row(self.width, bytes_per_pixel);
100 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
101 label: Some("frust-gpu headless readback"),
102 size: u64::from(bytes_per_row) * u64::from(self.height),
103 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
104 mapped_at_creation: false,
105 });
106 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
107 label: Some("frust-gpu headless readback copy"),
108 });
109 encoder.copy_texture_to_buffer(
110 wgpu::TexelCopyTextureInfo {
111 texture: &self.texture,
112 mip_level: 0,
113 origin: wgpu::Origin3d::ZERO,
114 aspect: wgpu::TextureAspect::All,
115 },
116 wgpu::TexelCopyBufferInfo {
117 buffer: &buffer,
118 layout: wgpu::TexelCopyBufferLayout {
119 offset: 0,
120 bytes_per_row: Some(bytes_per_row),
121 rows_per_image: Some(self.height),
122 },
123 },
124 wgpu::Extent3d {
125 width: self.width,
126 height: self.height,
127 depth_or_array_layers: 1,
128 },
129 );
130 queue.submit([encoder.finish()]);
131
132 let slice = buffer.slice(..);
133 let (tx, rx) = std::sync::mpsc::channel();
134 slice.map_async(wgpu::MapMode::Read, move |res| {
135 let _ = tx.send(res);
136 });
137 device
138 .poll(wgpu::PollType::wait_indefinitely())
139 .expect("frust-gpu headless: device poll for readback map must succeed");
140 rx.recv()
141 .expect("frust-gpu headless: readback map channel closed before a result arrived")
142 .expect("frust-gpu headless: readback buffer map failed");
143
144 let mapped = slice
145 .get_mapped_range()
146 .expect("frust-gpu headless: the readback buffer is mapped after map_async succeeded");
147 let pixels = strip_row_padding(&mapped, self.width, self.height, bytes_per_pixel);
148 drop(mapped);
149 buffer.unmap();
150 pixels
151 }
152}
153
154fn padded_bytes_per_row(width: u32, bytes_per_pixel: u32) -> u32 {
158 (width * bytes_per_pixel).next_multiple_of(COPY_ROW_ALIGNMENT)
159}
160
161fn strip_row_padding(padded: &[u8], width: u32, height: u32, bytes_per_pixel: u32) -> Vec<u8> {
164 let row = width as usize * bytes_per_pixel as usize;
165 let stride = padded_bytes_per_row(width, bytes_per_pixel) as usize;
166 let mut out = Vec::with_capacity(row * height as usize);
167 for y in 0..height as usize {
168 let start = y * stride;
169 out.extend_from_slice(&padded[start..start + row]);
170 }
171 out
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 const RGBA8_BPP: u32 = 4;
180
181 #[test]
182 fn padded_row_is_the_tight_row_when_already_aligned() {
183 assert_eq!(padded_bytes_per_row(64, RGBA8_BPP), 256);
184 assert_eq!(padded_bytes_per_row(128, RGBA8_BPP), 512);
185 assert_eq!(padded_bytes_per_row(256, RGBA8_BPP), 1024);
186 }
187
188 #[test]
189 fn padded_row_rounds_an_unaligned_width_up_to_the_alignment() {
190 assert_eq!(padded_bytes_per_row(1, RGBA8_BPP), 256);
191 assert_eq!(padded_bytes_per_row(63, RGBA8_BPP), 256);
192 assert_eq!(padded_bytes_per_row(65, RGBA8_BPP), 512);
193 assert_eq!(padded_bytes_per_row(97, RGBA8_BPP), 512);
194 assert_eq!(padded_bytes_per_row(129, RGBA8_BPP), 768);
195 for width in 1..600u32 {
196 let padded = padded_bytes_per_row(width, RGBA8_BPP);
197 assert!(
198 padded >= width * RGBA8_BPP,
199 "padding must never truncate a row"
200 );
201 assert_eq!(padded % COPY_ROW_ALIGNMENT, 0, "width {width}");
202 assert!(
203 padded - width * RGBA8_BPP < COPY_ROW_ALIGNMENT,
204 "padding must be minimal, width {width}"
205 );
206 }
207 }
208
209 #[test]
210 fn stripping_padding_keeps_every_rows_own_pixels() {
211 let width = 3;
214 let height = 4;
215 let stride = padded_bytes_per_row(width, RGBA8_BPP) as usize;
216 let mut padded = vec![0xEE_u8; stride * height as usize];
217 for y in 0..height as usize {
218 for byte in 0..(width as usize * RGBA8_BPP as usize) {
219 padded[y * stride + byte] = (y * 16 + byte) as u8;
220 }
221 }
222 let stripped = strip_row_padding(&padded, width, height, RGBA8_BPP);
223 assert_eq!(stripped.len(), (width * height * RGBA8_BPP) as usize);
224 for y in 0..height as usize {
225 for byte in 0..(width as usize * RGBA8_BPP as usize) {
226 assert_eq!(
227 stripped[y * width as usize * RGBA8_BPP as usize + byte],
228 (y * 16 + byte) as u8,
229 "row {y} byte {byte}"
230 );
231 }
232 }
233 assert!(
234 !stripped.contains(&0xEE),
235 "no padding byte may survive the strip"
236 );
237 }
238
239 #[test]
240 fn stripping_an_already_aligned_width_is_a_plain_copy() {
241 let width = 64;
242 let height = 2;
243 let padded: Vec<u8> = (0..(width * height * RGBA8_BPP))
244 .map(|i| (i % 251) as u8)
245 .collect();
246 assert_eq!(strip_row_padding(&padded, width, height, RGBA8_BPP), padded);
247 }
248}