diff --git a/wgpu/examples/capture/main.rs b/wgpu/examples/capture/main.rs index 41e0f9a595..e85f9f83fd 100644 --- a/wgpu/examples/capture/main.rs +++ b/wgpu/examples/capture/main.rs @@ -5,8 +5,26 @@ use std::env; use std::fs::File; use std::io::Write; use std::mem::size_of; +use wgpu::{Buffer, Device}; -async fn run() { +async fn run(png_output_path: &str) { + let args: Vec<_> = env::args().collect(); + let (width, height) = match args.len() { + // 0 on wasm, 1 on desktop + 0 | 1 => (100usize, 200usize), + 3 => (args[1].parse().unwrap(), args[2].parse().unwrap()), + _ => { + println!("Incorrect number of arguments, possible usages:"); + println!("* 0 arguments - uses default width and height of (100, 200)"); + println!("* 2 arguments - uses specified width and height values"); + return; + } + }; + let (device, buffer, buffer_dimensions) = create_red_image_with_dimensions(width, height).await; + create_png(png_output_path, device, buffer, &buffer_dimensions).await; +} + +async fn create_red_image_with_dimensions(width: usize, height: usize) -> (Device, Buffer, BufferDimensions) { let adapter = wgpu::Instance::new(wgpu::BackendBit::PRIMARY) .request_adapter( &wgpu::RequestAdapterOptions { @@ -30,40 +48,22 @@ async fn run() { .await .unwrap(); - let args: Vec<_> = env::args().collect(); - let (width, height) = match args.len() { - // 0 on wasm, 1 on desktop - 0 | 1 => (100usize, 200usize), - 3 => (args[1].parse().unwrap(), args[2].parse().unwrap()), - _ => { - println!("Incorrect number of arguments, possible usages:"); - println!("* 0 arguments - uses default width and height of (100, 200)"); - println!("* 2 arguments - uses specified width and height values"); - return; - } - }; - // It is a webgpu requirement that BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0 // So we calculate padded_bytes_per_row by rounding unpadded_bytes_per_row // up to the next multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT. // https://en.wikipedia.org/wiki/Data_structure_alignment#Computing_padding - let bytes_per_pixel = size_of::(); - let unpadded_bytes_per_row = width * bytes_per_pixel; - let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; - let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align; - let padded_bytes_per_row = unpadded_bytes_per_row + padded_bytes_per_row_padding; - + let buffer_dimensions = BufferDimensions::new(width, height); // The output buffer lets us retrieve the data as an array let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: None, - size: (padded_bytes_per_row * height) as u64, + size: (buffer_dimensions.padded_bytes_per_row * buffer_dimensions.height) as u64, usage: wgpu::BufferUsage::MAP_READ | wgpu::BufferUsage::COPY_DST, mapped_at_creation: false, }); let texture_extent = wgpu::Extent3d { - width: width as u32, - height: height as u32, + width: buffer_dimensions.width as u32, + height: buffer_dimensions.height as u32, depth: 1, }; @@ -105,7 +105,7 @@ async fn run() { buffer: &output_buffer, layout: wgpu::TextureDataLayout { offset: 0, - bytes_per_row: padded_bytes_per_row as u32, + bytes_per_row: buffer_dimensions.padded_bytes_per_row as u32, rows_per_image: 0, }, }, @@ -116,8 +116,11 @@ async fn run() { }; queue.submit(Some(command_buffer)); + (device, output_buffer, buffer_dimensions) +} - // Note that we're not calling `.await` here. +async fn create_png(png_output_path: &str, device: Device, output_buffer: Buffer, buffer_dimensions: &BufferDimensions) { +// Note that we're not calling `.await` here. let buffer_slice = output_buffer.slice(..); let buffer_future = buffer_slice.map_async(wgpu::MapMode::Read); @@ -125,7 +128,6 @@ async fn run() { // In an actual application, `device.poll(...)` should // be called in an event loop or on another thread. device.poll(wgpu::Maintain::Wait); - // If a file system is available, write the buffer as a PNG let has_file_system_available = cfg!(not(target_arch = "wasm32")); if !has_file_system_available { @@ -136,20 +138,20 @@ async fn run() { let padded_buffer = buffer_slice.get_mapped_range(); let mut png_encoder = png::Encoder::new( - File::create("red.png").unwrap(), - width as u32, - height as u32, + File::create(png_output_path).unwrap(), + buffer_dimensions.width as u32, + buffer_dimensions.height as u32, ); png_encoder.set_depth(png::BitDepth::Eight); png_encoder.set_color(png::ColorType::RGBA); let mut png_writer = png_encoder .write_header() .unwrap() - .into_stream_writer_with_size(unpadded_bytes_per_row); + .into_stream_writer_with_size(buffer_dimensions.unpadded_bytes_per_row); // from the padded_buffer we write just the unpadded bytes into the image - for chunk in padded_buffer.chunks(padded_bytes_per_row) { - png_writer.write(&chunk[..unpadded_bytes_per_row]).unwrap(); + for chunk in padded_buffer.chunks(buffer_dimensions.padded_bytes_per_row) { + png_writer.write(&chunk[..buffer_dimensions.unpadded_bytes_per_row]).unwrap(); } png_writer.finish().unwrap(); @@ -161,16 +163,69 @@ async fn run() { } } +struct BufferDimensions { + width: usize, + height: usize, + unpadded_bytes_per_row: usize, + padded_bytes_per_row: usize, +} + +impl BufferDimensions { + fn new(width: usize, height: usize) -> Self { + let bytes_per_pixel = size_of::(); + let unpadded_bytes_per_row = width * bytes_per_pixel; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; + let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align; + let padded_bytes_per_row = unpadded_bytes_per_row + padded_bytes_per_row_padding; + Self { width, height, unpadded_bytes_per_row, padded_bytes_per_row } + } +} + fn main() { #[cfg(not(target_arch = "wasm32"))] - { - env_logger::init(); - futures::executor::block_on(run()); - } + { + env_logger::init(); + futures::executor::block_on(run("red.png")); + } #[cfg(target_arch = "wasm32")] - { - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - console_log::init().expect("could not initialize logger"); - wasm_bindgen_futures::spawn_local(run()); - } + { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init().expect("could not initialize logger"); + wasm_bindgen_futures::spawn_local(run("red.png")); + } } + + +#[cfg(test)] +mod tests { + use super::*; + use wgpu::BufferView; + + #[test] + fn ensure_generated_data_matches_expected() { + futures::executor::block_on(assert_generated_data_matches_expected()); + } + + async fn assert_generated_data_matches_expected() { + let (device, output_buffer, dimensions) = create_red_image_with_dimensions(100usize, 200usize).await; + let buffer_slice = output_buffer.slice(..); + let buffer_future = buffer_slice.map_async(wgpu::MapMode::Read); + device.poll(wgpu::Maintain::Wait); + buffer_future.await.expect("failed to map buffer slice for capture test"); + let padded_buffer = buffer_slice.get_mapped_range(); + let expected_buffer_size = dimensions.padded_bytes_per_row*dimensions.height; + assert_eq!(padded_buffer.len(),expected_buffer_size); + assert_that_content_is_all_red(&dimensions, padded_buffer); + } + + fn assert_that_content_is_all_red(dimensions: &BufferDimensions, padded_buffer: BufferView) { + let red = [0xFFu8, 0, 0, 0xFFu8]; + let single_rgba = 4; + padded_buffer.chunks(dimensions.padded_bytes_per_row) + .map(|padded_buffer_row| &padded_buffer_row[..dimensions.unpadded_bytes_per_row]) + .for_each(|unpadded_row| + unpadded_row.chunks(single_rgba) + .for_each(|chunk| assert_eq!(chunk, &red)) + ); + } +} \ No newline at end of file diff --git a/wgpu/examples/capture/screenshot.png b/wgpu/examples/capture/screenshot.png index 7606d23c84..4021a58f6e 100644 Binary files a/wgpu/examples/capture/screenshot.png and b/wgpu/examples/capture/screenshot.png differ