Copy texture to a CPU-mappable buffer, handling wgpu's 256-byte row-pitch alignment, and return the unpadded pixel bytes.
(
device: &wgpu::Device,
queue: &wgpu::Queue,
tex: &wgpu::Texture,
width: u32,
height: u32,
bytes_per_pixel: u32,
)
| 382 | /// Copy texture to a CPU-mappable buffer, handling wgpu's 256-byte |
| 383 | /// row-pitch alignment, and return the unpadded pixel bytes. |
| 384 | async fn read_atlas( |
| 385 | device: &wgpu::Device, |
| 386 | queue: &wgpu::Queue, |
| 387 | tex: &wgpu::Texture, |
| 388 | width: u32, |
| 389 | height: u32, |
| 390 | bytes_per_pixel: u32, |
| 391 | ) -> Result<Vec<u8>, String> { |
| 392 | let unpadded_bpr = width * bytes_per_pixel; |
| 393 | let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; |
| 394 | let padded_bpr = ((unpadded_bpr + align - 1) / align) * align; |
| 395 | let buf_size = (padded_bpr * height) as u64; |
| 396 | |
| 397 | let buf = device.create_buffer(&wgpu::BufferDescriptor { |
| 398 | label: Some("readback"), |
| 399 | size: buf_size, |
| 400 | usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, |
| 401 | mapped_at_creation: false, |
| 402 | }); |
| 403 | |
| 404 | let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("readback-enc") }); |
| 405 | enc.copy_texture_to_buffer( |
| 406 | wgpu::TexelCopyTextureInfo { texture: tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All }, |
| 407 | wgpu::TexelCopyBufferInfo { |
| 408 | buffer: &buf, |
| 409 | layout: wgpu::TexelCopyBufferLayout { |
| 410 | offset: 0, |
| 411 | bytes_per_row: Some(padded_bpr), |
| 412 | rows_per_image: Some(height), |
| 413 | }, |
| 414 | }, |
| 415 | wgpu::Extent3d { width, height, depth_or_array_layers: 1 }, |
| 416 | ); |
| 417 | queue.submit(Some(enc.finish())); |
| 418 | |
| 419 | let slice = buf.slice(..); |
| 420 | let (tx, rx) = std::sync::mpsc::channel(); |
| 421 | slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); }); |
| 422 | let _ = device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None }); |
| 423 | rx.recv() |
| 424 | .map_err(|e| format!("map_async send: {e}"))? |
| 425 | .map_err(|e| format!("map_async: {e}"))?; |
| 426 | |
| 427 | let view = slice.get_mapped_range(); |
| 428 | // Strip per-row padding. |
| 429 | let mut out = Vec::with_capacity((unpadded_bpr * height) as usize); |
| 430 | for row in 0..height { |
| 431 | let src = (row * padded_bpr) as usize; |
| 432 | out.extend_from_slice(&view[src..src + unpadded_bpr as usize]); |
| 433 | } |
| 434 | drop(view); |
| 435 | buf.unmap(); |
| 436 | Ok(out) |
| 437 | } |
| 438 | |
| 439 | // ──────────────────────────────────────────────────────────────────── |
| 440 | // Tiny matrix helpers (column-major, row-vector × matrix multiplication |