Add image comparison to the mipmap example

This commit is contained in:
Connor Fitzgerald 2021-06-19 22:55:57 -04:00
parent 3d8a4baeb8
commit ebbbf2216b
16 changed files with 286 additions and 28 deletions

3
.gitignore vendored
View File

@ -15,3 +15,6 @@
# Output from capture example
wgpu/red.png
# Output from invalid comparison tests
**/screenshot-difference.png

View File

@ -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 = [

View File

@ -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),

View File

@ -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),

View File

@ -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),

View File

@ -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 {

View File

@ -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<f32> = 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<E: Example>(
}
};
example.render(&frame.output, &device, &queue, &spawner);
example.render(&frame.output.view, &device, &queue, &spawner);
}
_ => {}
}
@ -361,6 +364,108 @@ pub fn run<E: Example>(title: &str) {
});
}
#[cfg(test)]
pub fn test<E: Example>(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)]

View File

@ -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::<Example>("mipmap");
}
#[test]
fn mipmap() {
framework::test::<Example>(
concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/mipmap/screenshot.png"
),
1024,
768,
10, // Mipmap sampling is highly variant between impls.
10,
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 531 KiB

View File

@ -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,
}
};

View File

@ -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 {

View File

@ -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 {

View File

@ -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),

View File

@ -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,

125
wgpu/tests/common/image.rs Normal file
View File

@ -0,0 +1,125 @@
use std::{
ffi::{OsStr, OsString},
fs::File,
io::{BufWriter, Cursor},
path::Path,
str::FromStr,
};
fn read_png(path: impl AsRef<Path>, width: u32, height: u32) -> Option<Vec<u8>> {
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<Path>, 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<Path> + AsRef<OsStr>,
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);
}
}

View File

@ -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