#[must_use]
pub fn map_read_all(device: &wgpu::Device, buffer: &wgpu::Buffer) -> Vec<u8> {
let slice = buffer.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
device.poll(wgpu::PollType::wait_indefinitely()).ok();
rx.recv().ok();
let data = slice.get_mapped_range();
let out = data.to_vec();
drop(data);
buffer.unmap();
out
}
#[must_use]
pub fn read_buffer_range(
device: &wgpu::Device,
queue: &wgpu::Queue,
src: &wgpu::Buffer,
offset: u64,
len: u64,
) -> Vec<u8> {
if len == 0 || offset.saturating_add(len) > src.size() {
return Vec::new();
}
let padded = len.div_ceil(wgpu::COPY_BUFFER_ALIGNMENT) * wgpu::COPY_BUFFER_ALIGNMENT;
if offset.saturating_add(padded) > src.size() {
return Vec::new();
}
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("l0_readback_buffer"),
size: padded,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("l0_readback_buffer_enc"),
});
enc.copy_buffer_to_buffer(src, offset, &staging, 0, padded);
queue.submit(Some(enc.finish()));
let mut out = map_read_all(device, &staging);
out.truncate(len as usize);
out
}
#[must_use]
pub fn read_texture_region(
device: &wgpu::Device,
queue: &wgpu::Queue,
tex: &wgpu::Texture,
bytes_per_texel: u32,
x: u32,
y: u32,
w: u32,
h: u32,
) -> Vec<u8> {
let (tw, th) = (tex.width(), tex.height());
if x >= tw || y >= th || w == 0 || h == 0 || bytes_per_texel == 0 {
return Vec::new();
}
let w = w.min(tw - x);
let h = h.min(th - y);
let unpadded = w * bytes_per_texel;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded = unpadded.div_ceil(align) * align;
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("l0_readback_texture"),
size: u64::from(padded) * u64::from(h),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("l0_readback_texture_enc"),
});
enc.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: tex,
mip_level: 0,
origin: wgpu::Origin3d { x, y, z: 0 },
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &staging,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded),
rows_per_image: Some(h),
},
},
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
);
queue.submit(Some(enc.finish()));
let data = map_read_all(device, &staging);
strip_row_padding(&data, unpadded as usize, padded as usize, h as usize)
}
#[must_use]
pub fn strip_row_padding(data: &[u8], unpadded: usize, padded: usize, rows: usize) -> Vec<u8> {
if unpadded == 0 || padded < unpadded {
return Vec::new();
}
let mut out = Vec::with_capacity(unpadded * rows);
for row in 0..rows {
let base = row * padded;
let end = base + unpadded;
if end > data.len() {
break;
}
out.extend_from_slice(&data[base..end]);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn row_padding_is_stripped_and_the_naive_read_would_be_wrong() {
let (w, bpp, rows) = (5usize, 4usize, 3usize);
let unpadded = w * bpp; let padded = 256usize;
let mut raw = vec![0xEEu8; padded * rows];
for r in 0..rows {
for b in 0..unpadded {
raw[r * padded + b] = ((r + 1) * 10) as u8;
}
}
let out = strip_row_padding(&raw, unpadded, padded, rows);
assert_eq!(out.len(), unpadded * rows, "one unpadded row per row");
assert!(out.iter().all(|&b| b != 0xEE), "no padding byte survives the strip");
for r in 0..rows {
let row = &out[r * unpadded..(r + 1) * unpadded];
assert!(row.iter().all(|&b| b == ((r + 1) * 10) as u8), "row {r} is its own bytes");
}
let naive = &raw[..unpadded * rows];
assert_ne!(naive, &out[..], "the naive read differs, i.e. the padding is load-bearing");
}
#[test]
fn an_already_aligned_row_passes_through_unchanged() {
let (unpadded, rows) = (256usize, 4usize);
let raw: Vec<u8> = (0..unpadded * rows).map(|i| (i % 251) as u8).collect();
assert_eq!(strip_row_padding(&raw, unpadded, unpadded, rows), raw);
}
#[test]
fn degenerate_shapes_return_empty_rather_than_panicking() {
assert!(strip_row_padding(&[1, 2, 3], 0, 256, 1).is_empty(), "zero-width");
assert!(strip_row_padding(&[1, 2, 3], 8, 4, 1).is_empty(), "padded < unpadded is nonsense");
assert!(strip_row_padding(&[1, 2], 8, 8, 1).is_empty(), "short input truncates");
}
}