mirror of
https://github.com/gfx-rs/wgpu.git
synced 2024-11-21 22:33:49 +00:00
Add image comparison to the mipmap example
This commit is contained in:
parent
3d8a4baeb8
commit
ebbbf2216b
3
.gitignore
vendored
3
.gitignore
vendored
@ -15,3 +15,6 @@
|
||||
|
||||
# Output from capture example
|
||||
wgpu/red.png
|
||||
|
||||
# Output from invalid comparison tests
|
||||
**/screenshot-difference.png
|
||||
|
@ -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 = [
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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 {
|
||||
|
@ -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)]
|
||||
|
@ -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 |
@ -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,
|
||||
}
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
@ -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
125
wgpu/tests/common/image.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user