diff --git a/.gitignore b/.gitignore index 2f516ba67..aefc86aec 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ # Output from capture example wgpu/red.png + +# Output from invalid comparison tests +**/screenshot-difference.png diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 3e47b5cb7..b607b699d 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -95,6 +95,10 @@ test = true name="texture-arrays" required-features = ["spirv"] +[[example]] +name="mipmap" +test = true + [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2.73" # remember to change version in wiki as well web-sys = { version = "=0.3.50", features = [ diff --git a/wgpu/examples/boids/main.rs b/wgpu/examples/boids/main.rs index 9bafcfe75..6392792d1 100644 --- a/wgpu/examples/boids/main.rs +++ b/wgpu/examples/boids/main.rs @@ -254,14 +254,14 @@ impl framework::Example for Example { /// a TriangleList draw call for all NUM_PARTICLES at 3 vertices each fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, _spawner: &framework::Spawner, ) { // create render pass descriptor and its color attachments let color_attachments = [wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), diff --git a/wgpu/examples/bunnymark/main.rs b/wgpu/examples/bunnymark/main.rs index cec3e5fe6..18c2efc89 100644 --- a/wgpu/examples/bunnymark/main.rs +++ b/wgpu/examples/bunnymark/main.rs @@ -284,7 +284,7 @@ impl framework::Example for Example { fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, _spawner: &framework::Spawner, @@ -323,7 +323,7 @@ impl framework::Example for Example { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(clear_color), diff --git a/wgpu/examples/conservative-raster/main.rs b/wgpu/examples/conservative-raster/main.rs index d0a668454..43f90b7a6 100644 --- a/wgpu/examples/conservative-raster/main.rs +++ b/wgpu/examples/conservative-raster/main.rs @@ -253,7 +253,7 @@ impl framework::Example for Example { fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, _spawner: &framework::Spawner, @@ -285,7 +285,7 @@ impl framework::Example for Example { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("full resolution"), color_attachments: &[wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), diff --git a/wgpu/examples/cube/main.rs b/wgpu/examples/cube/main.rs index c2a5d2430..c890cee86 100644 --- a/wgpu/examples/cube/main.rs +++ b/wgpu/examples/cube/main.rs @@ -333,7 +333,7 @@ impl framework::Example for Example { fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, _spawner: &framework::Spawner, @@ -344,7 +344,7 @@ impl framework::Example for Example { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { diff --git a/wgpu/examples/framework.rs b/wgpu/examples/framework.rs index 1606dc69a..79653dea2 100644 --- a/wgpu/examples/framework.rs +++ b/wgpu/examples/framework.rs @@ -6,6 +6,9 @@ use winit::{ event_loop::{ControlFlow, EventLoop}, }; +#[path = "../tests/common/mod.rs"] +mod test_common; + #[rustfmt::skip] #[allow(unused)] pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( @@ -54,7 +57,7 @@ pub trait Example: 'static + Sized { fn update(&mut self, event: WindowEvent); fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, spawner: &Spawner, @@ -280,7 +283,7 @@ fn start( } }; - example.render(&frame.output, &device, &queue, &spawner); + example.render(&frame.output.view, &device, &queue, &spawner); } _ => {} } @@ -361,6 +364,108 @@ pub fn run(title: &str) { }); } +#[cfg(test)] +pub fn test(image_path: &str, width: u32, height: u32, tollerance: u8, max_outliers: usize) { + use std::num::NonZeroU32; + + assert_eq!(width % 64, 0, "width needs to be aligned 64"); + + let _optional = E::optional_features(); + let features = E::required_features(); + let mut limits = E::required_limits(); + if limits == wgpu::Limits::default() { + limits = test_common::lowest_reasonable_limits(); + } + + test_common::initialize_test( + test_common::TestParameters::default() + .features(features) + .limits(limits), + |ctx| { + let spawner = Spawner::new(); + + let dst_texture = ctx.device.create_texture(&wgpu::TextureDescriptor { + label: Some("destination"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsage::RENDER_ATTACHMENT | wgpu::TextureUsage::COPY_SRC, + }); + + let dst_view = dst_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let dst_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("image map buffer"), + size: width as u64 * height as u64 * 4, + usage: wgpu::BufferUsage::COPY_DST | wgpu::BufferUsage::MAP_READ, + mapped_at_creation: false, + }); + + let mut example = E::init( + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::RENDER_ATTACHMENT, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + width, + height, + present_mode: wgpu::PresentMode::Fifo, + }, + &ctx.adapter, + &ctx.device, + &ctx.queue, + ); + + example.render(&dst_view, &ctx.device, &ctx.queue, &spawner); + + let mut cmd_buf = ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + + cmd_buf.copy_texture_to_buffer( + wgpu::ImageCopyTexture { + texture: &dst_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + }, + wgpu::ImageCopyBuffer { + buffer: &dst_buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: NonZeroU32::new(width * 4), + rows_per_image: None, + }, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + + ctx.queue.submit(Some(cmd_buf.finish())); + + let dst_buffer_slice = dst_buffer.slice(..); + let _ = dst_buffer_slice.map_async(wgpu::MapMode::Read); + ctx.device.poll(wgpu::Maintain::Wait); + let bytes = dst_buffer_slice.get_mapped_range().to_vec(); + + test_common::image::compare_image_output( + &image_path, + width, + height, + &bytes, + tollerance, + max_outliers, + ); + }, + ); +} + // This allows treating the framework as a standalone example, // thus avoiding listing the example names in `Cargo.toml`. #[allow(dead_code)] diff --git a/wgpu/examples/mipmap/main.rs b/wgpu/examples/mipmap/main.rs index 0bda5fafb..6b7c45962 100644 --- a/wgpu/examples/mipmap/main.rs +++ b/wgpu/examples/mipmap/main.rs @@ -436,7 +436,7 @@ impl framework::Example for Example { fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, _spawner: &framework::Spawner, @@ -453,7 +453,7 @@ impl framework::Example for Example { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(clear_color), @@ -474,3 +474,17 @@ impl framework::Example for Example { fn main() { framework::run::("mipmap"); } + +#[test] +fn mipmap() { + framework::test::( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/examples/mipmap/screenshot.png" + ), + 1024, + 768, + 10, // Mipmap sampling is highly variant between impls. + 10, + ); +} diff --git a/wgpu/examples/mipmap/screenshot.png b/wgpu/examples/mipmap/screenshot.png index 977732afe..2369b8fe7 100644 Binary files a/wgpu/examples/mipmap/screenshot.png and b/wgpu/examples/mipmap/screenshot.png differ diff --git a/wgpu/examples/msaa-line/main.rs b/wgpu/examples/msaa-line/main.rs index 9f90365e3..9f9d61973 100644 --- a/wgpu/examples/msaa-line/main.rs +++ b/wgpu/examples/msaa-line/main.rs @@ -225,7 +225,7 @@ impl framework::Example for Example { fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, _spawner: &framework::Spawner, @@ -254,14 +254,14 @@ impl framework::Example for Example { }; let rpass_color_attachment = if self.sample_count == 1 { wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops, } } else { wgpu::RenderPassColorAttachment { view: &self.multisampled_framebuffer, - resolve_target: Some(&frame.view), + resolve_target: Some(&view), ops, } }; diff --git a/wgpu/examples/shadow/main.rs b/wgpu/examples/shadow/main.rs index 2ea56d544..6fed1b54a 100644 --- a/wgpu/examples/shadow/main.rs +++ b/wgpu/examples/shadow/main.rs @@ -688,7 +688,7 @@ impl framework::Example for Example { fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, _spawner: &framework::Spawner, @@ -781,7 +781,7 @@ impl framework::Example for Example { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { diff --git a/wgpu/examples/skybox/main.rs b/wgpu/examples/skybox/main.rs index 60a117e66..b25844ab7 100644 --- a/wgpu/examples/skybox/main.rs +++ b/wgpu/examples/skybox/main.rs @@ -389,7 +389,7 @@ impl framework::Example for Skybox { fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, spawner: &framework::Spawner, @@ -415,7 +415,7 @@ impl framework::Example for Skybox { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { diff --git a/wgpu/examples/texture-arrays/main.rs b/wgpu/examples/texture-arrays/main.rs index 5bd76fd3e..ae452ed53 100644 --- a/wgpu/examples/texture-arrays/main.rs +++ b/wgpu/examples/texture-arrays/main.rs @@ -278,7 +278,7 @@ impl framework::Example for Example { } fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, _spawner: &framework::Spawner, @@ -290,7 +290,7 @@ impl framework::Example for Example { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), diff --git a/wgpu/examples/water/main.rs b/wgpu/examples/water/main.rs index f3711a119..f6290798c 100644 --- a/wgpu/examples/water/main.rs +++ b/wgpu/examples/water/main.rs @@ -663,7 +663,7 @@ impl framework::Example for Example { #[allow(clippy::eq_op)] fn render( &mut self, - frame: &wgpu::SwapChainTexture, + view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, _spawner: &framework::Spawner, @@ -734,7 +734,7 @@ impl framework::Example for Example { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(back_color), @@ -761,7 +761,7 @@ impl framework::Example for Example { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[wgpu::RenderPassColorAttachment { - view: &frame.view, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, diff --git a/wgpu/tests/common/image.rs b/wgpu/tests/common/image.rs new file mode 100644 index 000000000..7e24630b0 --- /dev/null +++ b/wgpu/tests/common/image.rs @@ -0,0 +1,125 @@ +use std::{ + ffi::{OsStr, OsString}, + fs::File, + io::{BufWriter, Cursor}, + path::Path, + str::FromStr, +}; + +fn read_png(path: impl AsRef, width: u32, height: u32) -> Option> { + let data = match std::fs::read(&path) { + Ok(f) => f, + Err(e) => { + log::warn!( + "image comparison invalid: file io error when comparing {}: {}", + path.as_ref().display(), + e + ); + return None; + } + }; + let decoder = png::Decoder::new(Cursor::new(data)); + let (info, mut reader) = decoder.read_info().ok()?; + if info.width != width { + log::warn!("image comparison invalid: size mismatch"); + return None; + } + if info.height != height { + log::warn!("image comparison invalid: size mismatch"); + return None; + } + if info.color_type != png::ColorType::RGBA { + log::warn!("image comparison invalid: color type mismatch"); + return None; + } + if info.bit_depth != png::BitDepth::Eight { + log::warn!("image comparison invalid: bit depth mismatch"); + return None; + } + + let mut buffer = vec![0; info.buffer_size()]; + reader.next_frame(&mut buffer).ok()?; + + Some(buffer) +} + +fn write_png(path: impl AsRef, width: u32, height: u32, data: &[u8]) { + let file = BufWriter::new(File::create(path).unwrap()); + + let mut encoder = png::Encoder::new(file, width, height); + encoder.set_color(png::ColorType::RGBA); + encoder.set_depth(png::BitDepth::Eight); + encoder.set_compression(png::Compression::Best); + let mut writer = encoder.write_header().unwrap(); + + writer.write_image_data(&data).unwrap(); +} + +fn calc_difference(lhs: u8, rhs: u8) -> u8 { + (lhs as i16 - rhs as i16).abs() as u8 +} + +pub fn compare_image_output( + path: impl AsRef + AsRef, + width: u32, + height: u32, + data: &[u8], + tollerance: u8, + max_outliers: usize, +) { + let comparison_data = read_png(&path, width, height); + + if let Some(cmp) = comparison_data { + assert_eq!(cmp.len(), data.len()); + + let difference_data: Vec<_> = cmp + .chunks_exact(4) + .zip(data.chunks_exact(4)) + .flat_map(|(cmp_chunk, data_chunk)| { + [ + calc_difference(cmp_chunk[0], data_chunk[0]), + calc_difference(cmp_chunk[1], data_chunk[1]), + calc_difference(cmp_chunk[2], data_chunk[2]), + 255, + ] + }) + .collect(); + + let outliers: usize = difference_data + .chunks_exact(4) + .map(|colors| { + (colors[0] > tollerance) as usize + + (colors[1] > tollerance) as usize + + (colors[2] > tollerance) as usize + }) + .sum(); + + let max_difference = difference_data + .chunks_exact(4) + .map(|colors| colors[0].max(colors[1]).max(colors[2])) + .max() + .unwrap(); + + if outliers >= max_outliers { + // Because the deta is mismatched, lets output the difference to a file. + let old_path = Path::new(&path); + let difference_path = Path::new(&path).with_file_name( + OsString::from_str( + &(old_path.file_stem().unwrap().to_string_lossy() + "-difference.png"), + ) + .unwrap(), + ); + + write_png(&difference_path, width, height, &difference_data); + + panic!("Image data mismatch! Outlier count {} over limit {}. Max difference {}", outliers, max_outliers, max_difference) + } else { + println!( + "{} outliers over max difference {}", + outliers, max_difference + ); + } + } else { + write_png(&path, width, height, data); + } +} diff --git a/wgpu/tests/common/mod.rs b/wgpu/tests/common/mod.rs index 5fb43a94e..7de9ba953 100644 --- a/wgpu/tests/common/mod.rs +++ b/wgpu/tests/common/mod.rs @@ -1,4 +1,5 @@ //! This module contains common test-only code that needs to be shared between the examples and the tests. +#![allow(dead_code)] // This module is used in a lot of contexts and only parts of it will be used use std::panic::{catch_unwind, AssertUnwindSafe}; @@ -6,6 +7,8 @@ use wgt::{BackendBit, DeviceDescriptor, DownlevelProperties, Features, Limits}; use wgpu::{util, Adapter, Device, Instance, Queue}; +pub mod image; + async fn initialize_device( adapter: &Adapter, features: Features, @@ -37,10 +40,10 @@ pub struct TestingContext { // A rather arbitrary set of limits which should be lower than all devices wgpu reasonably expects to run on and provides enough resources for most tests to run. // Adjust as needed if they are too low/high. -fn lowest_reasonable_limits() -> Limits { +pub fn lowest_reasonable_limits() -> Limits { Limits { - max_texture_dimension_1d: 512, - max_texture_dimension_2d: 512, + max_texture_dimension_1d: 1024, + max_texture_dimension_2d: 1024, max_texture_dimension_3d: 32, max_texture_array_layers: 32, max_bind_groups: 1, @@ -97,7 +100,6 @@ impl Default for TestParameters { } // Builder pattern to make it easier -#[allow(dead_code)] impl TestParameters { /// Set of common features that most tests require. pub fn test_features(self) -> Self { @@ -109,6 +111,11 @@ impl TestParameters { self } + pub fn limits(mut self, limits: Limits) -> Self { + self.required_limits = limits; + self + } + pub fn backend_failures(mut self, backends: BackendBit) -> Self { self.backend_failures |= backends; self