use std::env;
use std::fs::File;
use std::io::Write;
use std::mem::size_of;
use wgpu::{Buffer, Device};
async fn run(png_output_path: &str) {
let args: Vec<_> = env::args().collect();
let (width, height) = match args.len() {
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::Backends::all())
.request_adapter(&wgpu::RequestAdapterOptions::default())
.await
.unwrap();
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::empty(),
limits: wgpu::Limits::downlevel_defaults(),
},
None,
)
.await
.unwrap();
let buffer_dimensions = BufferDimensions::new(width, height);
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: (buffer_dimensions.padded_bytes_per_row * buffer_dimensions.height) as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let texture_extent = wgpu::Extent3d {
width: buffer_dimensions.width as u32,
height: buffer_dimensions.height as u32,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
size: texture_extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
label: None,
});
let command_buffer = {
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[wgpu::RenderPassColorAttachment {
view: &texture.create_view(&wgpu::TextureViewDescriptor::default()),
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::RED),
store: true,
},
}],
depth_stencil_attachment: None,
});
encoder.copy_texture_to_buffer(
texture.as_image_copy(),
wgpu::ImageCopyBuffer {
buffer: &output_buffer,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(
std::num::NonZeroU32::new(buffer_dimensions.padded_bytes_per_row as u32)
.unwrap(),
),
rows_per_image: None,
},
},
texture_extent,
);
encoder.finish()
};
queue.submit(Some(command_buffer));
(device, output_buffer, buffer_dimensions)
}
async fn create_png(
png_output_path: &str,
device: Device,
output_buffer: Buffer,
buffer_dimensions: &BufferDimensions,
) {
let buffer_slice = output_buffer.slice(..);
let buffer_future = buffer_slice.map_async(wgpu::MapMode::Read);
device.poll(wgpu::Maintain::Wait);
let has_file_system_available = cfg!(not(target_arch = "wasm32"));
if !has_file_system_available {
return;
}
if let Ok(()) = buffer_future.await {
let padded_buffer = buffer_slice.get_mapped_range();
let mut png_encoder = png::Encoder::new(
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(buffer_dimensions.unpadded_bytes_per_row);
for chunk in padded_buffer.chunks(buffer_dimensions.padded_bytes_per_row) {
png_writer
.write_all(&chunk[..buffer_dimensions.unpadded_bytes_per_row])
.unwrap();
}
png_writer.finish().unwrap();
drop(padded_buffer);
output_buffer.unmap();
}
}
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::<u32>();
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();
pollster::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("red.png"));
}
}
#[cfg(test)]
mod tests {
use super::*;
use wgpu::BufferView;
#[test]
fn ensure_generated_data_matches_expected() {
pollster::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))
});
}
}