diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f431943571..7d37b83708 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,7 +133,10 @@ jobs: - if: matrix.clippy_params != '' run: cargo clippy ${{ matrix.clippy_params }} - if: matrix.channel == 'nightly' - run: cargo test -- --nocapture + run: cargo test -p wgpu-core -- --nocapture + # - if: matrix.channel == 'nightly' + # run: cargo run --bin wgpu-info -- cargo test -p wgpu -- --nocapture + docs: runs-on: [ubuntu-latest] diff --git a/.gitignore b/.gitignore index 2f516ba67f..1afba3214e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ # Output from capture example wgpu/red.png + +# Output from invalid comparison tests +**/*-actual.png +**/*-difference.png diff --git a/Cargo.lock b/Cargo.lock index 90f4bd7f0c..6f97c04c6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -664,19 +664,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "generator" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061d3be1afec479d56fa3bd182bf966c7999ec175fcfdb87ac14d417241366c6" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "winapi 0.3.9", -] - [[package]] name = "getrandom" version = "0.1.16" @@ -915,17 +902,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "loom" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0e8460f2f2121162705187214720353c517b97bdfb3494c0b1e33d83ebe4bed" -dependencies = [ - "cfg-if 0.1.10", - "generator", - "scoped-tls", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -1557,12 +1533,6 @@ dependencies = [ "owned_ttf_parser", ] -[[package]] -name = "rustversion" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" - [[package]] name = "same-file" version = "1.0.6" @@ -1921,10 +1891,11 @@ checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" [[package]] name = "wgpu" -version = "0.8.0" +version = "0.9.0" dependencies = [ "arrayvec", "async-executor", + "bitflags", "bytemuck", "cgmath", "console_error_panic_hook", @@ -1954,7 +1925,7 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.8.0" +version = "0.9.0" dependencies = [ "arrayvec", "bitflags", @@ -1962,7 +1933,6 @@ dependencies = [ "copyless", "fxhash", "log", - "loom", "naga", "parking_lot", "profiling", @@ -1977,7 +1947,7 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "0.1.0" +version = "0.9.0" dependencies = [ "arrayvec", "ash", @@ -2004,9 +1974,16 @@ dependencies = [ "winit", ] +[[package]] +name = "wgpu-info" +version = "0.9.0" +dependencies = [ + "wgpu", +] + [[package]] name = "wgpu-types" -version = "0.8.0" +version = "0.9.0" dependencies = [ "bitflags", "serde", diff --git a/Cargo.toml b/Cargo.toml index 3298b4b288..964a706ec9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,10 @@ members = [ "wgpu", "wgpu-core", "wgpu-hal", + "wgpu-info", "wgpu-types", ] -default-members = ["wgpu", "player", "wgpu-hal"] +default-members = ["wgpu", "player", "wgpu-hal", "wgpu-info"] [patch."https://github.com/gfx-rs/naga"] #naga = { path = "../naga" } diff --git a/README.md b/README.md index a18829e564..4fa927d96c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The repository hosts the following parts: - [![Crates.io](https://img.shields.io/crates/v/wgpu.svg?label=wgpu)](https://crates.io/crates/wgpu) [![docs.rs](https://docs.rs/wgpu/badge.svg)](https://docs.rs/wgpu/) - public Rust API for users - [![Crates.io](https://img.shields.io/crates/v/wgpu-core.svg?label=wgpu-core)](https://crates.io/crates/wgpu-core) [![docs.rs](https://docs.rs/wgpu-core/badge.svg)](https://docs.rs/wgpu-core/) - internal Rust API for WebGPU implementations to use - [![Crates.io](https://img.shields.io/crates/v/wgpu-hal.svg?label=wgpu-hal)](https://crates.io/crates/wgpu-hal) [![docs.rs](https://docs.rs/wgpu-hal/badge.svg)](https://docs.rs/wgpu-hal/) - internal unsafe GPU abstraction API + - [![Crates.io](https://img.shields.io/crates/v/wgpu-info.svg?label=wgpu-types)](https://crates.io/crates/wgpu-info) - Program that prints out information about all the adapters on the system or invokes a command for every adapter. - [![Crates.io](https://img.shields.io/crates/v/wgpu-types.svg?label=wgpu-types)](https://crates.io/crates/wgpu-types) [![docs.rs](https://docs.rs/wgpu-types/badge.svg)](https://docs.rs/wgpu-types/) - Rust types shared between `wgpu-core` and `wgpu-rs` - `player` - standalone application for replaying the API traces, uses `winit` @@ -25,10 +26,38 @@ If you are looking for the native implementation or bindings to the API in other API | Windows 7/10 | Linux & Android | macOS & iOS | ----- | ------------------ | ------------------ | ------------------ | - DX11 | | | | - DX12 | | | | + DX11 | :construction: | | | + DX12 | :construction: | | | Vulkan | :white_check_mark: | :white_check_mark: | | Metal | | | :white_check_mark: | - GLes3 | | | | + GLes3 | | :construction: | | :white_check_mark: = Primary support — :ok: = Secondary support — :construction: = Unsupported, but support in progress + +## Testing Infrastructure + +wgpu features a set of unit, integration, and example based tests. All framework based examples are automatically reftested against the screenshot in the example directory. The `wgpu-info` example contains the logic which can automatically run the tests multiple times for all the adapters present on the system. These tests are also run on CI on windows and linux over Vulkan/DX12/DX11/GL on software adapters. + +To run the test suite, run the following command: + +``` +cargo run --bin wgpu-info -- cargo test +``` + +To run any individual test on a specific adapter, populate the following environment variables: +- `WGPU_ADAPTER_NAME` with a substring of the name of the adapter you want to use (ex. "1080" will match "NVIDIA GeForce 1080ti"). +- `WGPU_BACKEND` with the name of the backend you want to use (`vulkan`, `metal`, `dx12`, `dx11`, or `gl`). + +Then to run an example's reftests, run: + +``` +cargo test --example +``` + +Or run a part of the integration test suite: + +``` +cargo test -p wgpu -- +``` + +If you are a user and want a way to help contribute to wgpu, we always need more help writing test cases. diff --git a/wgpu-core/Cargo.toml b/wgpu-core/Cargo.toml index b04af3d261..1239f105b7 100644 --- a/wgpu-core/Cargo.toml +++ b/wgpu-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wgpu-core" -version = "0.8.0" +version = "0.9.0" authors = ["wgpu developers"] edition = "2018" description = "WebGPU core logic on gfx-hal" @@ -42,12 +42,12 @@ features = ["wgsl-in"] [dependencies.wgt] path = "../wgpu-types" package = "wgpu-types" -version = "0.8" +version = "0.9" [dependencies.hal] path = "../wgpu-hal" package = "wgpu-hal" -version = "0.1" +version = "0.9" [target.'cfg(all(not(target_arch = "wasm32"), any(target_os = "ios", target_os = "macos")))'.dependencies] hal = { path = "../wgpu-hal", package = "wgpu-hal", features = ["metal"] } @@ -59,8 +59,5 @@ hal = { path = "../wgpu-hal", package = "wgpu-hal", features = ["vulkan"] } [target.'cfg(all(not(target_arch = "wasm32"), windows))'.dependencies] hal = { path = "../wgpu-hal", package = "wgpu-hal", features = ["vulkan"] } -[dev-dependencies] -loom = "0.3" - [build-dependencies] cfg_aliases = "0.1" diff --git a/wgpu-core/src/lib.rs b/wgpu-core/src/lib.rs index 5a6d6ab2c4..f1aa612db1 100644 --- a/wgpu-core/src/lib.rs +++ b/wgpu-core/src/lib.rs @@ -43,14 +43,9 @@ mod validation; pub use hal::api; -#[cfg(test)] -use loom::sync::atomic; -#[cfg(not(test))] -use std::sync::atomic; - use atomic::{AtomicU64, AtomicUsize, Ordering}; -use std::{borrow::Cow, os::raw::c_char, ptr}; +use std::{borrow::Cow, os::raw::c_char, ptr, sync::atomic}; type SubmissionIndex = hal::FenceValue; type Index = u32; @@ -124,28 +119,6 @@ impl Drop for RefCount { } } -#[cfg(test)] -#[test] -fn loom() { - loom::model(move || { - let bx = Box::new(AtomicUsize::new(1)); - let ref_count_main = ptr::NonNull::new(Box::into_raw(bx)).map(RefCount).unwrap(); - let ref_count_spawned = ref_count_main.clone(); - - let join_handle = loom::thread::spawn(move || { - let _ = ref_count_spawned.clone(); - ref_count_spawned.rich_drop_outer() - }); - - let dropped_in_main = ref_count_main.rich_drop_outer(); - let dropped_in_spawned = join_handle.join().unwrap(); - assert_ne!( - dropped_in_main, dropped_in_spawned, - "must drop exactly once" - ); - }); -} - /// Reference count object that tracks multiple references. /// Unlike `RefCount`, it's manually inc()/dec() called. #[derive(Debug)] diff --git a/wgpu-hal/Cargo.toml b/wgpu-hal/Cargo.toml index 2363f920f8..9fc1c2746d 100644 --- a/wgpu-hal/Cargo.toml +++ b/wgpu-hal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wgpu-hal" -version = "0.1.0" +version = "0.9.0" authors = ["wgpu developers"] edition = "2018" description = "WebGPU hardware abstraction layer" diff --git a/wgpu-info/Cargo.toml b/wgpu-info/Cargo.toml new file mode 100644 index 0000000000..c0aa877c9b --- /dev/null +++ b/wgpu-info/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "wgpu-info" +version = "0.9.0" +authors = ["wgpu developers"] +edition = "2018" +description = "Adapter information and per-adapter test program" +homepage = "https://github.com/gfx-rs/wgpu" +repository = "https://github.com/gfx-rs/wgpu" +keywords = ["graphics"] +license = "MIT OR Apache-2.0" + +[dependencies] +wgpu = { version = "0.9", path = "../wgpu" } diff --git a/wgpu-info/README.md b/wgpu-info/README.md new file mode 100644 index 0000000000..7d3d703a91 --- /dev/null +++ b/wgpu-info/README.md @@ -0,0 +1,17 @@ +# wgpu-info + +This is a command line utility that does two different functions. + +#### Listing Adapters + +When called with no arguments, wgpu-info will list all adapters visible to wgpu and all the information about them we have. + +``` +cargo run --bin wgpu-info +``` + +#### Running Test on many Adapters + +When called with any amount of arguments it will interpret all of the arguments as a command to run. It will run this command N different times, one for every combination of adapter and backend on the system. + +For every command invocation, it will set `WGPU_ADAPTER_NAME` to the name of the adapter name and `WGPU_BACKEND` to the name of the backend. This is used as the primary means of testing across many adapters. diff --git a/wgpu-info/src/main.rs b/wgpu-info/src/main.rs new file mode 100644 index 0000000000..0f79cfd745 --- /dev/null +++ b/wgpu-info/src/main.rs @@ -0,0 +1,153 @@ +use std::{ + mem::size_of, + process::{exit, Command}, + time::Instant, +}; + +// Lets keep these on one line +#[rustfmt::skip] +fn print_info_from_adapter(adapter: &wgpu::Adapter, idx: usize) { + let info = adapter.get_info(); + let downlevel = adapter.get_downlevel_properties(); + let features = adapter.features(); + let limits = adapter.limits(); + + println!("Adapter {}:", idx); + println!("\tBackend: {:?}", info.backend); + println!("\tName: {:?}", info.name); + println!("\tVendorID: {:?}", info.vendor); + println!("\tDeviceID: {:?}", info.device); + println!("\tType: {:?}", info.device_type); + println!("\tCompliant: {:?}", downlevel.is_webgpu_compliant()); + println!("\tFeatures:"); + for i in 0..(size_of::() * 8) { + let bit = wgpu::Features::from_bits(1 << i as u64); + if let Some(bit) = bit { + if wgpu::Features::all().contains(bit) { + println!("\t\t{:<44} {}", format!("{:?}:", bit), features.contains(bit)); + } + } + } + println!("\tLimits:"); + let wgpu::Limits { + max_texture_dimension_1d, + max_texture_dimension_2d, + max_texture_dimension_3d, + max_texture_array_layers, + max_bind_groups, + max_dynamic_uniform_buffers_per_pipeline_layout, + max_dynamic_storage_buffers_per_pipeline_layout, + max_sampled_textures_per_shader_stage, + max_samplers_per_shader_stage, + max_storage_buffers_per_shader_stage, + max_storage_textures_per_shader_stage, + max_uniform_buffers_per_shader_stage, + max_uniform_buffer_binding_size, + max_storage_buffer_binding_size, + max_vertex_buffers, + max_vertex_attributes, + max_vertex_buffer_array_stride, + max_push_constant_size, + } = limits; + println!("\t\tMax Texture Dimension 1d: {}", max_texture_dimension_1d); + println!("\t\tMax Texture Dimension 2d: {}", max_texture_dimension_2d); + println!("\t\tMax Texture Dimension 3d: {}", max_texture_dimension_3d); + println!("\t\tMax Texture Array Layers: {}", max_texture_array_layers); + println!("\t\tMax Bind Groups: {}", max_bind_groups); + println!("\t\tMax Dynamic Uniform Buffers Per Pipeline Layout: {}", max_dynamic_uniform_buffers_per_pipeline_layout); + println!("\t\tMax Dynamic Storage Buffers Per Pipeline Layout: {}", max_dynamic_storage_buffers_per_pipeline_layout); + println!("\t\tMax Sampled Textures Per Shader Stage: {}", max_sampled_textures_per_shader_stage); + println!("\t\tMax Samplers Per Shader Stage: {}", max_samplers_per_shader_stage); + println!("\t\tMax Storage Buffers Per Shader Stage: {}", max_storage_buffers_per_shader_stage); + println!("\t\tMax Storage Textures Per Shader Stage: {}", max_storage_textures_per_shader_stage); + println!("\t\tMax Uniform Buffers Per Shader Stage: {}", max_uniform_buffers_per_shader_stage); + println!("\t\tMax Uniform Buffer Binding Size: {}", max_uniform_buffer_binding_size); + println!("\t\tMax Storage Buffer Binding Size: {}", max_storage_buffer_binding_size); + println!("\t\tMax Vertex Buffers: {}", max_vertex_buffers); + println!("\t\tMax Vertex Attributes: {}", max_vertex_attributes); + println!("\t\tMax Vertex Buffer Array Stride: {}", max_vertex_buffer_array_stride); + println!("\t\tMax Push Constant Size: {}", max_push_constant_size); + println!("\tDownlevel Properties:"); + let wgpu::DownlevelCapabilities { + shader_model, + flags + } = downlevel; + println!("\t\tShader Model: {:?}", shader_model); + for i in 0..(size_of::() * 8) { + let bit = wgpu::DownlevelFlags::from_bits(1 << i as u64); + if let Some(bit) = bit { + if wgpu::DownlevelFlags::all().contains(bit) { + println!("\t\t{:<36} {}", format!("{:?}:", bit), flags.contains(bit)); + } + } + } +} + +fn main() { + let args: Vec<_> = std::env::args().skip(1).collect(); + + let instance = wgpu::Instance::new(wgpu::BackendBit::all()); + let adapters: Vec<_> = instance + .enumerate_adapters(wgpu::BackendBit::all()) + .collect(); + let adapter_count = adapters.len(); + + if args.is_empty() { + for (idx, adapter) in adapters.into_iter().enumerate() { + print_info_from_adapter(&adapter, idx) + } + } else { + let all_start = Instant::now(); + + for (idx, adapter) in adapters.into_iter().enumerate() { + let adapter_start_time = Instant::now(); + let idx = idx + 1; + let info = adapter.get_info(); + println!( + "=========== TESTING {} on {:?} ({} of {}) ===========", + info.name, info.backend, idx, adapter_count + ); + let exit_status = Command::new(&args[0]) + .args(&args[1..]) + .env("WGPU_ADAPTER_NAME", &info.name) + .env( + "WGPU_BACKEND", + match info.backend { + wgpu::Backend::Empty => unreachable!(), + wgpu::Backend::Vulkan => "vulkan", + wgpu::Backend::Metal => "metal", + wgpu::Backend::Dx12 => "dx12", + wgpu::Backend::Dx11 => "dx11", + wgpu::Backend::Gl => "gl", + wgpu::Backend::BrowserWebGpu => "webgpu", + }, + ) + .spawn() + .unwrap() + .wait() + .unwrap(); + + let adapter_time = adapter_start_time.elapsed().as_secs_f32(); + + if exit_status.success() { + println!( + "=========== PASSED! {} on {:?} ({} of {}) in {:.3}s ===========", + info.name, info.backend, idx, adapter_count, adapter_time + ); + } else { + println!( + "=========== FAILED! {} on {:?} ({} of {}) in {:.3}s ===========", + info.name, info.backend, idx, adapter_count, adapter_time + ); + exit(1); + } + } + + let all_time = all_start.elapsed().as_secs_f32(); + + println!( + "=========== {} adapters PASSED in {:.3}s ===========", + adapter_count, all_time + ); + } +} diff --git a/wgpu-types/Cargo.toml b/wgpu-types/Cargo.toml index b36f942eca..e5610b607c 100644 --- a/wgpu-types/Cargo.toml +++ b/wgpu-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wgpu-types" -version = "0.8.0" +version = "0.9.0" authors = ["wgpu developers"] edition = "2018" description = "WebGPU types" diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs index 81e403cbb7..8455888181 100644 --- a/wgpu-types/src/lib.rs +++ b/wgpu-types/src/lib.rs @@ -602,11 +602,6 @@ bitflags::bitflags! { /// /// This is a native only feature. const SPIRV_SHADER_PASSTHROUGH = 0x0000_0400_0000_0000; - - /// Features which are part of the upstream WebGPU standard. - const ALL_WEBGPU = 0x0000_0000_0000_FFFF; - /// Features that are only available when targeting native (not web). - const ALL_NATIVE = 0xFFFF_FFFF_FFFF_0000; } } diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 45a5bd6b30..e1ae2adb6a 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wgpu" -version = "0.8.0" +version = "0.9.0" authors = ["wgpu developers"] edition = "2018" description = "Rusty WebGPU API wrapper" @@ -9,12 +9,17 @@ repository = "https://github.com/gfx-rs/wgpu" keywords = ["graphics"] license = "MIT OR Apache-2.0" exclude = ["etc/**/*", "examples/**/*", "tests/**/*", "Cargo.lock", "target/**/*"] +autotests = false [package.metadata.docs.rs] all-features = true [lib] +[[test]] +name = "wgpu-tests" +path = "tests/root.rs" + [features] default = [] spirv = ["naga/spv-in"] @@ -50,6 +55,7 @@ serde = { version = "1", features = ["derive"], optional = true } smallvec = "1" [dev-dependencies] +bitflags = "1" bytemuck = { version = "1.4", features = ["derive"] } cgmath = "0.18" ddsfile = "0.4" @@ -82,13 +88,50 @@ git = "https://github.com/gfx-rs/naga" tag = "gfx-26" features = ["wgsl-in", "spv-out"] +[[example]] +name="boids" +test = true + +[[example]] +name="bunnymark" +test = true + +[[example]] +name="conservative-raster" +test = true + +[[example]] +name="cube" +test = true + [[example]] name="hello-compute" test = true +[[example]] +name="mipmap" +test = true + +[[example]] +name="msaa-line" +test = true + +[[example]] +name="shadow" +test = true + +[[example]] +name="skybox" +test = true + [[example]] name="texture-arrays" required-features = ["spirv"] +test = true + +[[example]] +name="water" +test = true [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2.73" # remember to change version in wiki as well diff --git a/wgpu/examples/README.md b/wgpu/examples/README.md index 46eeefebd0..ad7b342fa2 100644 --- a/wgpu/examples/README.md +++ b/wgpu/examples/README.md @@ -9,7 +9,7 @@ Notably, `capture` example shows rendering without a surface/window. It reads ba All the examples use [WGSL](https://gpuweb.github.io/gpuweb/wgsl.html) shaders unless specified otherwise. -All framework-based examples render to the window. +All framework-based examples render to the window and are reftested against the screenshot in the directory. ## Feature matrix | Feature | boids | bunnymark | cube | mipmap | msaa-line | shadow | skybox | texture-arrays | water | conservative-raster | diff --git a/wgpu/examples/boids/main.rs b/wgpu/examples/boids/main.rs index 9bafcfe757..e2d39644d1 100644 --- a/wgpu/examples/boids/main.rs +++ b/wgpu/examples/boids/main.rs @@ -1,7 +1,10 @@ // Flocking boids example with gpu compute update pass // adapted from https://github.com/austinEng/webgpu-samples/blob/master/src/examples/computeBoids.ts -use rand::distributions::{Distribution, Uniform}; +use rand::{ + distributions::{Distribution, Uniform}, + SeedableRng, +}; use std::{borrow::Cow, mem}; use wgpu::util::DeviceExt; @@ -168,7 +171,7 @@ impl framework::Example for Example { // buffer for all particles data of type [(posx,posy,velx,vely),...] let mut initial_particle_data = vec![0.0f32; (4 * NUM_PARTICLES) as usize]; - let mut rng = rand::thread_rng(); + let mut rng = rand::rngs::StdRng::seed_from_u64(42); let unif = Uniform::new_inclusive(-1.0, 1.0); for particle_instance_chunk in initial_particle_data.chunks_mut(4) { particle_instance_chunk[0] = unif.sample(&mut rng); // posx @@ -254,14 +257,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, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), @@ -314,3 +317,16 @@ impl framework::Example for Example { fn main() { framework::run::("boids"); } + +#[test] +fn boids() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/boids/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::default(), + base_test_parameters: framework::test_common::TestParameters::default(), + tollerance: 0, + max_outliers: 50, + }); +} diff --git a/wgpu/examples/boids/screenshot.png b/wgpu/examples/boids/screenshot.png index e510c1c164..f3c6796b1f 100644 Binary files a/wgpu/examples/boids/screenshot.png and b/wgpu/examples/boids/screenshot.png differ diff --git a/wgpu/examples/bunnymark/main.rs b/wgpu/examples/bunnymark/main.rs index cec3e5fe63..a4a4899c97 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, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(clear_color), @@ -349,3 +349,16 @@ impl framework::Example for Example { fn main() { framework::run::("bunnymark"); } + +#[test] +fn bunnymark() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/bunnymark/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::default(), + base_test_parameters: framework::test_common::TestParameters::default(), + tollerance: 1, + max_outliers: 50, + }); +} diff --git a/wgpu/examples/bunnymark/screenshot.png b/wgpu/examples/bunnymark/screenshot.png new file mode 100644 index 0000000000..48deda7274 Binary files /dev/null and b/wgpu/examples/bunnymark/screenshot.png differ diff --git a/wgpu/examples/conservative-raster/main.rs b/wgpu/examples/conservative-raster/main.rs index d0a6684541..8cdde1b946 100644 --- a/wgpu/examples/conservative-raster/main.rs +++ b/wgpu/examples/conservative-raster/main.rs @@ -73,7 +73,7 @@ impl framework::Example for Example { } fn init( sc_desc: &wgpu::SwapChainDescriptor, - adapter: &wgpu::Adapter, + _adapter: &wgpu::Adapter, device: &wgpu::Device, _queue: &wgpu::Queue, ) -> Self { @@ -133,7 +133,7 @@ impl framework::Example for Example { multisample: wgpu::MultisampleState::default(), }); - let pipeline_lines = if adapter + let pipeline_lines = if device .features() .contains(wgpu::Features::NON_FILL_POLYGON_MODE) { @@ -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, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), @@ -312,3 +312,16 @@ impl framework::Example for Example { fn main() { framework::run::("conservative-raster"); } + +#[test] +fn conservative_raster() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/conservative-raster/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::default(), + base_test_parameters: framework::test_common::TestParameters::default().failure(), + tollerance: 0, + max_outliers: 0, + }); +} diff --git a/wgpu/examples/conservative-raster/screenshot.png b/wgpu/examples/conservative-raster/screenshot.png index e227e10e65..4a4d4cd7e6 100644 Binary files a/wgpu/examples/conservative-raster/screenshot.png and b/wgpu/examples/conservative-raster/screenshot.png differ diff --git a/wgpu/examples/cube/main.rs b/wgpu/examples/cube/main.rs index c2a5d24304..f0a41e1d4f 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, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { @@ -379,3 +379,29 @@ impl framework::Example for Example { fn main() { framework::run::("cube"); } + +#[test] +fn cube() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/cube/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::default(), + base_test_parameters: framework::test_common::TestParameters::default(), + tollerance: 1, + max_outliers: 3, + }); +} + +#[test] +fn cube_lines() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/cube/screenshot-lines.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::NON_FILL_POLYGON_MODE, + base_test_parameters: framework::test_common::TestParameters::default(), + tollerance: 2, + max_outliers: 400, // Line rasterization is very different between vendors + }); +} diff --git a/wgpu/examples/cube/screenshot-lines.png b/wgpu/examples/cube/screenshot-lines.png new file mode 100644 index 0000000000..289a9733f2 Binary files /dev/null and b/wgpu/examples/cube/screenshot-lines.png differ diff --git a/wgpu/examples/cube/screenshot.png b/wgpu/examples/cube/screenshot.png index 784ffc463b..bd8d0158ca 100644 Binary files a/wgpu/examples/cube/screenshot.png and b/wgpu/examples/cube/screenshot.png differ diff --git a/wgpu/examples/framework.rs b/wgpu/examples/framework.rs index 493e622643..f74aa48a9b 100644 --- a/wgpu/examples/framework.rs +++ b/wgpu/examples/framework.rs @@ -6,6 +6,10 @@ use winit::{ event_loop::{ControlFlow, EventLoop}, }; +#[cfg(test)] +#[path = "../tests/common/mod.rs"] +pub mod test_common; + #[rustfmt::skip] #[allow(unused)] pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( @@ -54,7 +58,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, @@ -106,39 +110,15 @@ async fn setup(title: &str) -> Setup { log::info!("Initializing the surface..."); - let backend = if let Ok(backend) = std::env::var("WGPU_BACKEND") { - match backend.to_lowercase().as_str() { - "vulkan" => wgpu::BackendBit::VULKAN, - "metal" => wgpu::BackendBit::METAL, - "dx12" => wgpu::BackendBit::DX12, - "dx11" => wgpu::BackendBit::DX11, - "gl" => wgpu::BackendBit::GL, - "webgpu" => wgpu::BackendBit::BROWSER_WEBGPU, - other => panic!("Unknown backend: {}", other), - } - } else { - wgpu::BackendBit::PRIMARY - }; - let power_preference = if let Ok(power_preference) = std::env::var("WGPU_POWER_PREF") { - match power_preference.to_lowercase().as_str() { - "low" => wgpu::PowerPreference::LowPower, - "high" => wgpu::PowerPreference::HighPerformance, - other => panic!("Unknown power preference: {}", other), - } - } else { - wgpu::PowerPreference::default() - }; + let backend = wgpu::util::backend_bits_from_env().unwrap_or(wgpu::BackendBit::PRIMARY); + let instance = wgpu::Instance::new(backend); let (size, surface) = unsafe { let size = window.inner_size(); let surface = instance.create_surface(&window); (size, surface) }; - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference, - compatible_surface: Some(&surface), - }) + let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, backend) .await .expect("No suitable GPU adapters found on the system!"); @@ -304,7 +284,7 @@ fn start( } }; - example.render(&frame.output, &device, &queue, &spawner); + example.render(&frame.output.view, &device, &queue, &spawner); } _ => {} } @@ -385,6 +365,120 @@ pub fn run(title: &str) { }); } +#[cfg(test)] +pub struct FrameworkRefTest { + pub image_path: &'static str, + pub width: u32, + pub height: u32, + pub optional_features: wgpu::Features, + pub base_test_parameters: test_common::TestParameters, + pub tollerance: u8, + pub max_outliers: usize, +} + +#[cfg(test)] +#[allow(dead_code)] +pub fn test(mut params: FrameworkRefTest) { + use std::{mem, num::NonZeroU32}; + + assert_eq!(params.width % 64, 0, "width needs to be aligned 64"); + + let features = E::required_features() | params.optional_features; + let mut limits = E::required_limits(); + if limits == wgpu::Limits::default() { + limits = test_common::lowest_reasonable_limits(); + } + + test_common::initialize_test( + mem::take(&mut params.base_test_parameters) + .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: params.width, + height: params.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: params.width as u64 * params.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: params.width, + height: params.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, + aspect: wgpu::TextureAspect::All, + }, + wgpu::ImageCopyBuffer { + buffer: &dst_buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: NonZeroU32::new(params.width * 4), + rows_per_image: None, + }, + }, + wgpu::Extent3d { + width: params.width, + height: params.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( + env!("CARGO_MANIFEST_DIR").to_string() + params.image_path, + params.width, + params.height, + &bytes, + params.tollerance, + params.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/hello-compute/main.rs b/wgpu/examples/hello-compute/main.rs index 2831014f2d..086509a13c 100644 --- a/wgpu/examples/hello-compute/main.rs +++ b/wgpu/examples/hello-compute/main.rs @@ -54,6 +54,20 @@ async fn execute_gpu(numbers: &[u32]) -> Option> { .await .unwrap(); + let info = adapter.get_info(); + // skip this on LavaPipe temporarily + if info.vendor == 0x10005 { + return None; + } + + execute_gpu_inner(&device, &queue, numbers).await +} + +async fn execute_gpu_inner( + device: &wgpu::Device, + queue: &wgpu::Queue, + numbers: &[u32], +) -> Option> { // Loads the shader from WGSL let cs_module = device.create_shader_module(&wgpu::ShaderModuleDescriptor { label: None, @@ -182,52 +196,4 @@ fn main() { } #[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use super::*; - - #[test] - fn test_compute_1() { - let input = &[1, 2, 3, 4]; - pollster::block_on(assert_execute_gpu(input, &[0, 1, 7, 2])); - } - - #[test] - fn test_compute_2() { - let input = &[5, 23, 10, 9]; - pollster::block_on(assert_execute_gpu(input, &[5, 15, 6, 19])); - } - - #[test] - fn test_compute_overflow() { - let input = &[77031, 837799, 8400511, 63728127]; - pollster::block_on(assert_execute_gpu(input, &[350, 524, OVERFLOW, OVERFLOW])); - } - - #[test] - fn test_multithreaded_compute() { - use std::{sync::mpsc, thread, time::Duration}; - - let thread_count = 8; - - let (tx, rx) = mpsc::channel(); - for _ in 0..thread_count { - let tx = tx.clone(); - thread::spawn(move || { - let input = &[100, 100, 100]; - pollster::block_on(assert_execute_gpu(input, &[25, 25, 25])); - tx.send(true).unwrap(); - }); - } - - for _ in 0..thread_count { - rx.recv_timeout(Duration::from_secs(10)) - .expect("A thread never completed."); - } - } - - async fn assert_execute_gpu(input: &[u32], expected: &[u32]) { - if let Some(produced) = execute_gpu(input).await { - assert_eq!(produced, expected); - } - } -} +mod tests; diff --git a/wgpu/examples/hello-compute/tests.rs b/wgpu/examples/hello-compute/tests.rs new file mode 100644 index 0000000000..f0945c5a80 --- /dev/null +++ b/wgpu/examples/hello-compute/tests.rs @@ -0,0 +1,91 @@ +#[path = "../../tests/common/mod.rs"] +mod common; + +use std::sync::Arc; + +use super::*; +use common::{initialize_test, TestParameters}; + +#[test] +fn test_compute_1() { + initialize_test(TestParameters::default(), |ctx| { + let input = &[1, 2, 3, 4]; + + pollster::block_on(assert_execute_gpu( + &ctx.device, + &ctx.queue, + input, + &[0, 1, 7, 2], + )); + }); +} + +#[test] +fn test_compute_2() { + initialize_test(TestParameters::default(), |ctx| { + let input = &[5, 23, 10, 9]; + + pollster::block_on(assert_execute_gpu( + &ctx.device, + &ctx.queue, + input, + &[5, 15, 6, 19], + )); + }); +} + +#[test] +fn test_compute_overflow() { + initialize_test(TestParameters::default(), |ctx| { + let input = &[77031, 837799, 8400511, 63728127]; + pollster::block_on(assert_execute_gpu( + &ctx.device, + &ctx.queue, + input, + &[350, 524, OVERFLOW, OVERFLOW], + )); + }); +} + +#[test] +fn test_multithreaded_compute() { + initialize_test(TestParameters::default(), |ctx| { + use std::{sync::mpsc, thread, time::Duration}; + + let ctx = Arc::new(ctx); + + let thread_count = 8; + + let (tx, rx) = mpsc::channel(); + for _ in 0..thread_count { + let tx = tx.clone(); + let ctx = Arc::clone(&ctx); + thread::spawn(move || { + let input = &[100, 100, 100]; + pollster::block_on(assert_execute_gpu( + &ctx.device, + &ctx.queue, + input, + &[25, 25, 25], + )); + tx.send(true).unwrap(); + }); + } + + for _ in 0..thread_count { + rx.recv_timeout(Duration::from_secs(10)) + .expect("A thread never completed."); + } + }); +} + +async fn assert_execute_gpu( + device: &wgpu::Device, + queue: &wgpu::Queue, + input: &[u32], + expected: &[u32], +) { + if let Some(produced) = execute_gpu_inner(device, queue, input).await { + assert_eq!(produced, expected); + } +} diff --git a/wgpu/examples/mipmap/main.rs b/wgpu/examples/mipmap/main.rs index 0bda5fafb9..1f1501be6f 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, 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::(framework::FrameworkRefTest { + image_path: "/examples/mipmap/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::default(), + base_test_parameters: framework::test_common::TestParameters::default() + .backend_failures(wgpu::BackendBit::VULKAN), + tollerance: 25, + max_outliers: 3000, // Mipmap sampling is highly variant between impls. This is currently bounded by AMD on mac + }); +} diff --git a/wgpu/examples/mipmap/screenshot.png b/wgpu/examples/mipmap/screenshot.png index 977732afe7..eb67ca39cf 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 9f90365e37..2e6fced386 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, resolve_target: None, ops, } } else { wgpu::RenderPassColorAttachment { view: &self.multisampled_framebuffer, - resolve_target: Some(&frame.view), + resolve_target: Some(&view), ops, } }; @@ -282,3 +282,16 @@ impl framework::Example for Example { fn main() { framework::run::("msaa-line"); } + +#[test] +fn msaa_line() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/msaa-line/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::default(), + base_test_parameters: framework::test_common::TestParameters::default(), + tollerance: 64, + max_outliers: 1 << 16, // MSAA is comically different between vendors, 32k is a decent limit + }); +} diff --git a/wgpu/examples/msaa-line/screenshot.png b/wgpu/examples/msaa-line/screenshot.png index 9e42081245..332bda7a9c 100644 Binary files a/wgpu/examples/msaa-line/screenshot.png and b/wgpu/examples/msaa-line/screenshot.png differ diff --git a/wgpu/examples/shadow/main.rs b/wgpu/examples/shadow/main.rs index 2ea56d5443..2db478ebc8 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, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { @@ -821,3 +821,16 @@ impl framework::Example for Example { fn main() { framework::run::("shadow"); } + +#[test] +fn shadow() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/shadow/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::default(), + base_test_parameters: framework::test_common::TestParameters::default(), + tollerance: 2, + max_outliers: 5, + }); +} diff --git a/wgpu/examples/shadow/screenshot.png b/wgpu/examples/shadow/screenshot.png index ae07823782..e0106800b2 100644 Binary files a/wgpu/examples/shadow/screenshot.png and b/wgpu/examples/shadow/screenshot.png differ diff --git a/wgpu/examples/skybox/main.rs b/wgpu/examples/skybox/main.rs index 60a117e66c..a300c5261f 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, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { @@ -459,3 +459,56 @@ impl framework::Example for Skybox { fn main() { framework::run::("skybox"); } + +#[test] +fn skybox() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/skybox/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::default(), + base_test_parameters: framework::test_common::TestParameters::default() + .backend_failures(wgpu::BackendBit::VULKAN), + tollerance: 2, + max_outliers: 3, + }); +} + +#[test] +fn skybox_bc1() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/skybox/screenshot-bc1.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::TEXTURE_COMPRESSION_BC, + base_test_parameters: framework::test_common::TestParameters::default(), + tollerance: 5, + max_outliers: 10, + }); +} + +#[test] +fn skybox_etc2() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/skybox/screenshot-etc2.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::TEXTURE_COMPRESSION_ETC2, + base_test_parameters: framework::test_common::TestParameters::default(), + tollerance: 5, // TODO + max_outliers: 10, // TODO + }); +} + +#[test] +fn skybox_astc() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/skybox/screenshot-astc.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::TEXTURE_COMPRESSION_ASTC_LDR, + base_test_parameters: framework::test_common::TestParameters::default(), + tollerance: 5, // TODO + max_outliers: 10, // TODO + }); +} diff --git a/wgpu/examples/skybox/screenshot-astc.png b/wgpu/examples/skybox/screenshot-astc.png new file mode 100644 index 0000000000..a3dc335ab7 Binary files /dev/null and b/wgpu/examples/skybox/screenshot-astc.png differ diff --git a/wgpu/examples/skybox/screenshot-bc1.png b/wgpu/examples/skybox/screenshot-bc1.png new file mode 100644 index 0000000000..d111bc5122 Binary files /dev/null and b/wgpu/examples/skybox/screenshot-bc1.png differ diff --git a/wgpu/examples/skybox/screenshot-etc2.png b/wgpu/examples/skybox/screenshot-etc2.png new file mode 100644 index 0000000000..d2280935a5 Binary files /dev/null and b/wgpu/examples/skybox/screenshot-etc2.png differ diff --git a/wgpu/examples/skybox/screenshot.png b/wgpu/examples/skybox/screenshot.png index db0cb5ba38..362c921f96 100644 Binary files a/wgpu/examples/skybox/screenshot.png and b/wgpu/examples/skybox/screenshot.png differ diff --git a/wgpu/examples/skybox/shader.wgsl b/wgpu/examples/skybox/shader.wgsl index 9d211eed52..700fde9308 100644 --- a/wgpu/examples/skybox/shader.wgsl +++ b/wgpu/examples/skybox/shader.wgsl @@ -73,6 +73,6 @@ fn fs_entity(in: EntityOutput) -> [[location(0)]] vec4 { let normal = normalize(in.normal); let reflected = incident - 2.0 * dot(normal, incident) * normal; - let reflected_color = textureSample(r_texture, r_sampler, reflected); - return vec4(0.1, 0.1, 0.1, 0.1) + 0.5 * reflected_color; + let reflected_color = textureSample(r_texture, r_sampler, reflected).rgb; + return vec4(vec3(0.1) + 0.5 * reflected_color, 1.0); } diff --git a/wgpu/examples/texture-arrays/main.rs b/wgpu/examples/texture-arrays/main.rs index 5bd76fd3e6..8dbbac9870 100644 --- a/wgpu/examples/texture-arrays/main.rs +++ b/wgpu/examples/texture-arrays/main.rs @@ -100,7 +100,11 @@ impl framework::Example for Example { f if f.contains(wgpu::Features::SAMPLED_TEXTURE_ARRAY_NON_UNIFORM_INDEXING) => { wgpu::include_spirv_raw!("non-uniform.frag.spv") } - f if f.contains(wgpu::Features::SAMPLED_TEXTURE_ARRAY_DYNAMIC_INDEXING) => { + f if f.contains( + wgpu::Features::SAMPLED_TEXTURE_ARRAY_DYNAMIC_INDEXING + | wgpu::Features::PUSH_CONSTANTS, + ) => + { uniform_workaround = true; wgpu::include_spirv_raw!("uniform.frag.spv") } @@ -278,7 +282,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 +294,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, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), @@ -322,3 +326,61 @@ impl framework::Example for Example { fn main() { framework::run::("texture-arrays"); } + +// This fails due to an issue with naga https://github.com/gfx-rs/wgpu/issues/1532 +#[test] +fn texture_arrays_constant() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/texture-arrays/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::default(), + base_test_parameters: framework::test_common::TestParameters::default().failure(), + tollerance: 0, + max_outliers: 0, + }); +} + +// This fails due to an issue with naga https://github.com/gfx-rs/wgpu/issues/1532 +#[test] +fn texture_arrays_uniform() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/texture-arrays/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::SAMPLED_TEXTURE_ARRAY_DYNAMIC_INDEXING + | wgpu::Features::PUSH_CONSTANTS, + base_test_parameters: framework::test_common::TestParameters::default().failure(), + tollerance: 0, + max_outliers: 0, + }); +} + +// This fails due to an issue with naga https://github.com/gfx-rs/wgpu/issues/1532 +#[test] +fn texture_arrays_non_uniform() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/texture-arrays/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::SAMPLED_TEXTURE_ARRAY_NON_UNIFORM_INDEXING, + base_test_parameters: framework::test_common::TestParameters::default().failure(), + tollerance: 0, + max_outliers: 0, + }); +} + +// This fails due to an issue with naga https://github.com/gfx-rs/wgpu/issues/1532 +#[test] +fn texture_arrays_unsized_non_uniform() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/texture-arrays/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::SAMPLED_TEXTURE_ARRAY_NON_UNIFORM_INDEXING + | wgpu::Features::UNSIZED_BINDING_ARRAY, + base_test_parameters: framework::test_common::TestParameters::default().failure(), + tollerance: 0, + max_outliers: 0, + }); +} diff --git a/wgpu/examples/water/main.rs b/wgpu/examples/water/main.rs index f3711a1193..6a375ce6c1 100644 --- a/wgpu/examples/water/main.rs +++ b/wgpu/examples/water/main.rs @@ -5,6 +5,7 @@ mod point_gen; use bytemuck::{Pod, Zeroable}; use cgmath::Point3; +use rand::SeedableRng; use std::{borrow::Cow, iter, mem}; use wgpu::util::DeviceExt; @@ -279,7 +280,7 @@ impl framework::Example for Example { let terrain_noise = noise::OpenSimplex::new(); // Random colouration - let mut terrain_random = rand::thread_rng(); + let mut terrain_random = rand::rngs::StdRng::seed_from_u64(42); // Generate terrain. The closure determines what each hexagon will look like. let terrain = @@ -663,7 +664,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 +735,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, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(back_color), @@ -761,7 +762,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, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, @@ -788,3 +789,16 @@ impl framework::Example for Example { fn main() { framework::run::("water"); } + +#[test] +fn shadow() { + framework::test::(framework::FrameworkRefTest { + image_path: "/examples/water/screenshot.png", + width: 1024, + height: 768, + optional_features: wgpu::Features::default(), + base_test_parameters: framework::test_common::TestParameters::default(), + tollerance: 5, + max_outliers: 10, + }); +} diff --git a/wgpu/examples/water/screenshot.png b/wgpu/examples/water/screenshot.png index 434ebf3f23..7d71ae5733 100644 Binary files a/wgpu/examples/water/screenshot.png and b/wgpu/examples/water/screenshot.png differ diff --git a/wgpu/src/backend/web.rs b/wgpu/src/backend/web.rs index 2dbce8e86e..d570ff6384 100644 --- a/wgpu/src/backend/web.rs +++ b/wgpu/src/backend/web.rs @@ -990,11 +990,6 @@ impl crate::Context for Context { if trace_dir.is_some() { //Error: Tracing isn't supported on the Web target } - assert!( - !desc.features.intersects(crate::Features::ALL_NATIVE), - "The web backend doesn't support any native extensions. Enabled native extensions: {:?}", - desc.features & crate::Features::ALL_NATIVE - ); // TODO: non-guaranteed limits let mut mapped_desc = web_sys::GpuDeviceDescriptor::new(); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index d38c28a80d..5429041320 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -1524,6 +1524,11 @@ impl Adapter { Context::adapter_get_info(&*self.context, &self.id) } + /// Get info about the adapter itself. + pub fn get_downlevel_properties(&self) -> DownlevelCapabilities { + Context::adapter_downlevel_properties(&*self.context, &self.id) + } + /// Returns the features supported for a given texture format by this adapter. /// /// Note that the WebGPU spec further restricts the available usages/features. diff --git a/wgpu/src/util/init.rs b/wgpu/src/util/init.rs new file mode 100644 index 0000000000..d5587ebfd7 --- /dev/null +++ b/wgpu/src/util/init.rs @@ -0,0 +1,91 @@ +use wgt::{BackendBit, PowerPreference, RequestAdapterOptions}; + +use crate::{Adapter, Instance}; + +/// Get a set of backend bits from the environment variable WGPU_BACKEND. +pub fn backend_bits_from_env() -> Option { + Some( + match std::env::var("WGPU_BACKEND") + .as_deref() + .map(str::to_lowercase) + .as_deref() + { + Ok("vulkan") => BackendBit::VULKAN, + Ok("dx12") => BackendBit::DX12, + Ok("dx11") => BackendBit::DX11, + Ok("metal") => BackendBit::METAL, + Ok("gl") => BackendBit::GL, + Ok("webgpu") => BackendBit::BROWSER_WEBGPU, + _ => return None, + }, + ) +} + +/// Get a power preference from the environment variable WGPU_POWER_PREF +pub fn power_preference_from_env() -> Option { + Some( + match std::env::var("WGPU_POWER_PREF") + .as_deref() + .map(str::to_lowercase) + .as_deref() + { + Ok("low") => PowerPreference::LowPower, + Ok("high") => PowerPreference::HighPerformance, + _ => return None, + }, + ) +} + +/// Initialize the adapter obeying the WGPU_ADAPTER_NAME environment variable. +#[cfg(not(target_arch = "wasm32"))] +pub fn initialize_adapter_from_env( + instance: &Instance, + backend_bits: BackendBit, +) -> Option { + let adapters = instance.enumerate_adapters(backend_bits); + + let desired_adapter_name = std::env::var("WGPU_ADAPTER_NAME") + .as_deref() + .map(str::to_lowercase) + .ok()?; + + let mut chosen_adapter = None; + for adapter in adapters { + let info = adapter.get_info(); + + if info.name.to_lowercase().contains(&desired_adapter_name) { + chosen_adapter = Some(adapter); + break; + } + } + + Some(chosen_adapter.expect("WGPU_ADAPTER_NAME set but no matching adapter found!")) +} + +/// Initialize the adapter obeying the WGPU_ADAPTER_NAME environment variable. +#[cfg(target_arch = "wasm32")] +pub fn initialize_adapter_from_env( + _instance: &Instance, + _backend_bits: BackendBit, +) -> Option { + None +} + +/// Initialize the adapter obeying the WGPU_ADAPTER_NAME environment variable and if it doesn't exist fall back on a default adapter. +pub async fn initialize_adapter_from_env_or_default( + instance: &Instance, + backend_bits: wgt::BackendBit, +) -> Option { + match initialize_adapter_from_env(&instance, backend_bits) { + Some(a) => Some(a), + None => { + instance + .request_adapter(&RequestAdapterOptions { + power_preference: power_preference_from_env() + .unwrap_or_else(PowerPreference::default), + compatible_surface: None, + }) + .await + } + } +} diff --git a/wgpu/src/util/mod.rs b/wgpu/src/util/mod.rs index b18548eb08..1e9f85eb4e 100644 --- a/wgpu/src/util/mod.rs +++ b/wgpu/src/util/mod.rs @@ -3,6 +3,7 @@ mod belt; mod device; mod encoder; +mod init; use std::future::Future; #[cfg(feature = "spirv")] @@ -15,6 +16,10 @@ use std::{ pub use belt::StagingBelt; pub use device::{BufferInitDescriptor, DeviceExt}; pub use encoder::RenderEncoder; +pub use init::{ + backend_bits_from_env, initialize_adapter_from_env, initialize_adapter_from_env_or_default, + power_preference_from_env, +}; /// Treat the given byte slice as a SPIR-V module. /// diff --git a/wgpu/tests/common/image.rs b/wgpu/tests/common/image.rs new file mode 100644 index 0000000000..97177f3d3f --- /dev/null +++ b/wgpu/tests/common/image.rs @@ -0,0 +1,147 @@ +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], + compression: png::Compression, +) { + 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(compression); + 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 actual_path = Path::new(&path).with_file_name( + OsString::from_str( + &(old_path.file_stem().unwrap().to_string_lossy() + "-actual.png"), + ) + .unwrap(), + ); + 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(&actual_path, width, height, &data, png::Compression::Fast); + write_png( + &difference_path, + width, + height, + &difference_data, + png::Compression::Fast, + ); + + 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, png::Compression::Best); + } +} diff --git a/wgpu/tests/common/mod.rs b/wgpu/tests/common/mod.rs new file mode 100644 index 0000000000..4b4422a5ed --- /dev/null +++ b/wgpu/tests/common/mod.rs @@ -0,0 +1,280 @@ +//! 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}; + +use wgt::{BackendBit, DeviceDescriptor, DownlevelCapabilities, Features, Limits}; + +use wgpu::{util, Adapter, Device, Instance, Queue}; + +pub mod image; + +async fn initialize_device( + adapter: &Adapter, + features: Features, + limits: Limits, +) -> (Device, Queue) { + let bundle = adapter + .request_device( + &DeviceDescriptor { + label: None, + features, + limits, + }, + None, + ) + .await; + + match bundle { + Ok(b) => b, + Err(e) => panic!("Failed to initialize device: {}", e), + } +} + +pub struct TestingContext { + pub adapter: Adapter, + pub adapter_info: wgt::AdapterInfo, + pub device: Device, + pub queue: Queue, +} + +// 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. +pub fn lowest_reasonable_limits() -> Limits { + Limits { + max_texture_dimension_1d: 1024, + max_texture_dimension_2d: 1024, + max_texture_dimension_3d: 32, + max_texture_array_layers: 32, + max_bind_groups: 2, + max_dynamic_uniform_buffers_per_pipeline_layout: 2, + max_dynamic_storage_buffers_per_pipeline_layout: 2, + max_sampled_textures_per_shader_stage: 2, + max_samplers_per_shader_stage: 2, + max_storage_buffers_per_shader_stage: 2, + max_storage_textures_per_shader_stage: 2, + max_uniform_buffers_per_shader_stage: 2, + max_uniform_buffer_binding_size: 256, + max_storage_buffer_binding_size: 1 << 16, + max_vertex_buffers: 4, + max_vertex_attributes: 4, + max_vertex_buffer_array_stride: 32, + max_push_constant_size: 0, + } +} + +fn lowest_downlevel_properties() -> DownlevelCapabilities { + DownlevelCapabilities { + flags: wgt::DownlevelFlags::empty(), + shader_model: wgt::ShaderModel::Sm2, + } +} + +// This information determines if a test should run. +pub struct TestParameters { + pub required_features: Features, + pub required_limits: Limits, + pub required_downlevel_properties: DownlevelCapabilities, + // Backends where test should fail. + pub failures: Vec<( + Option, + Option, + Option, + bool, + )>, +} + +impl Default for TestParameters { + fn default() -> Self { + Self { + required_features: Features::empty(), + required_limits: lowest_reasonable_limits(), + required_downlevel_properties: lowest_downlevel_properties(), + failures: Vec::new(), + } + } +} + +bitflags::bitflags! { + pub struct FailureReason: u8 { + const BACKEND = 0x1; + const VENDOR = 0x2; + const ADAPTER = 0x4; + const ALWAYS = 0x8; + } +} + +// Builder pattern to make it easier +impl TestParameters { + /// Set of common features that most tests require. + pub fn test_features(self) -> Self { + self.features(Features::MAPPABLE_PRIMARY_BUFFERS | Features::VERTEX_WRITABLE_STORAGE) + } + + /// Set the list of features this test requires. + pub fn features(mut self, features: Features) -> Self { + self.required_features |= features; + self + } + + /// Set the list + pub fn limits(mut self, limits: Limits) -> Self { + self.required_limits = limits; + self + } + + /// Mark the test as always failing, equivilant to specific_failure(None, None, None) + pub fn failure(mut self) -> Self { + self.failures.push((None, None, None, false)); + self + } + + /// Mark the test as always failing on a specific backend, equivilant to specific_failure(backend, None, None) + pub fn backend_failures(mut self, backends: wgpu::BackendBit) -> Self { + self.failures.push((Some(backends), None, None, false)); + self + } + + /// Determines if a test should fail under a particular set of conditions. If any of these are None, that means that it will match anything in that field. + /// + /// ex. + /// `specific_failure(Some(wgpu::BackendBit::DX11 | wgpu::BackendBit::DX12), None, Some("RTX"), false)` + /// means that this test will fail on all cards with RTX in their name on either D3D backend, no matter the vendor ID. + /// + /// If segfault is set to true, the test won't be run at all due to avoid segfaults. + pub fn specific_failure( + mut self, + backends: Option, + vendor: Option, + device: Option<&'static str>, + segfault: bool, + ) -> Self { + self.failures.push(( + backends, + vendor, + device.as_ref().map(AsRef::as_ref).map(str::to_lowercase), + segfault, + )); + self + } +} + +pub fn initialize_test(parameters: TestParameters, test_function: impl FnOnce(TestingContext)) { + // We don't actually care if it fails + let _ = env_logger::try_init(); + + let backend_bits = util::backend_bits_from_env().unwrap_or_else(BackendBit::all); + let instance = Instance::new(backend_bits); + let adapter = pollster::block_on(util::initialize_adapter_from_env_or_default( + &instance, + backend_bits, + )) + .expect("could not find sutable adapter on the system"); + + let adapter_info = adapter.get_info(); + let adapter_lowercase_name = adapter_info.name.to_lowercase(); + let adapter_features = adapter.features(); + let adapter_limits = adapter.limits(); + let adapter_downlevel_properties = adapter.get_downlevel_properties(); + + let missing_features = parameters.required_features - adapter_features; + if !missing_features.is_empty() { + println!("TEST SKIPPED: MISSING FEATURES {:?}", missing_features); + return; + } + + if adapter_limits < parameters.required_limits { + println!("TEST SKIPPED: LIMIT TOO LOW"); + return; + } + + let missing_downlevel_flags = + parameters.required_downlevel_properties.flags - adapter_downlevel_properties.flags; + if !missing_downlevel_flags.is_empty() { + println!( + "TEST SKIPPED: MISSING DOWNLEVEL FLAGS {:?}", + missing_downlevel_flags + ); + return; + } + + if adapter_downlevel_properties.shader_model + < parameters.required_downlevel_properties.shader_model + { + println!( + "TEST SKIPPED: LOW SHADER MODEL {:?}", + adapter_downlevel_properties.shader_model + ); + return; + } + + let (device, queue) = pollster::block_on(initialize_device( + &adapter, + parameters.required_features, + parameters.required_limits, + )); + + let context = TestingContext { + adapter, + adapter_info: adapter_info.clone(), + device, + queue, + }; + + let failure_reason = parameters.failures.iter().find_map( + |(backend_failure, vendor_failure, adapter_failure, segfault)| { + let always = + backend_failure.is_none() && vendor_failure.is_none() && adapter_failure.is_none(); + + let expect_failure_backend = backend_failure + .map(|f| f.contains(wgpu::BackendBit::from(adapter_info.backend))) + .unwrap_or(true); + let expect_failure_vendor = vendor_failure + .map(|v| v == adapter_info.vendor) + .unwrap_or(true); + let expect_failure_adapter = adapter_failure + .as_deref() + .map(|f| adapter_lowercase_name.contains(f)) + .unwrap_or(true); + + if expect_failure_backend && expect_failure_vendor && expect_failure_adapter { + if always { + Some((FailureReason::ALWAYS, *segfault)) + } else { + let mut reason = FailureReason::empty(); + reason.set(FailureReason::BACKEND, expect_failure_backend); + reason.set(FailureReason::VENDOR, expect_failure_vendor); + reason.set(FailureReason::ADAPTER, expect_failure_adapter); + Some((reason, *segfault)) + } + } else { + None + } + }, + ); + + if let Some((reason, true)) = failure_reason { + println!( + "EXPECTED TEST FAILURE SKIPPED DUE TO SEGFAULT: {:?}", + reason + ); + return; + } + + let panicked = catch_unwind(AssertUnwindSafe(|| test_function(context))).is_err(); + + let expect_failure = failure_reason.is_some(); + + if panicked == expect_failure { + // We got the conditions we expected + if let Some((reason, _)) = failure_reason { + // Print out reason for the failure + println!("GOT EXPECTED TEST FAILURE: {:?}", reason); + } + } else if let Some(reason) = failure_reason { + // We expected to fail, but things passed + panic!("UNEXPECTED TEST PASS: {:?}", reason); + } else { + panic!("UNEXPECTED TEST FAILURE") + } +} diff --git a/wgpu/tests/device.rs b/wgpu/tests/device.rs new file mode 100644 index 0000000000..2763a6a21a --- /dev/null +++ b/wgpu/tests/device.rs @@ -0,0 +1,8 @@ +use crate::common::{initialize_test, TestParameters}; + +#[test] +fn device_initialization() { + initialize_test(TestParameters::default(), |_ctx| { + // intentionally empty + }) +} diff --git a/wgpu/tests/example-wgsl.rs b/wgpu/tests/example_wgsl.rs similarity index 95% rename from wgpu/tests/example-wgsl.rs rename to wgpu/tests/example_wgsl.rs index 481eeb919d..db51d8fc59 100644 --- a/wgpu/tests/example-wgsl.rs +++ b/wgpu/tests/example_wgsl.rs @@ -1,6 +1,7 @@ use naga::{front::wgsl, valid::Validator}; use std::{fs, path::PathBuf}; +/// Runs through all example shaders and ensures they are valid wgsl. #[test] fn parse_example_wgsl() { let read_dir = match PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/wgpu/tests/instance.rs b/wgpu/tests/instance.rs new file mode 100644 index 0000000000..850a7170af --- /dev/null +++ b/wgpu/tests/instance.rs @@ -0,0 +1,28 @@ +#[test] +fn initialize() { + let _ = wgpu::Instance::new( + wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::BackendBit::all), + ); +} + +fn request_adapter_inner(power: wgt::PowerPreference) { + let instance = wgpu::Instance::new( + wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::BackendBit::all), + ); + + let _adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: power, + compatible_surface: None, + })) + .unwrap(); +} + +#[test] +fn request_adapter_low_power() { + request_adapter_inner(wgt::PowerPreference::LowPower); +} + +#[test] +fn request_adapter_high_power() { + request_adapter_inner(wgt::PowerPreference::HighPerformance); +} diff --git a/wgpu/tests/root.rs b/wgpu/tests/root.rs new file mode 100644 index 0000000000..f4442e6bc7 --- /dev/null +++ b/wgpu/tests/root.rs @@ -0,0 +1,7 @@ +// All files containing tests +mod common; + +mod device; +mod example_wgsl; +mod instance; +mod vertex_indices; diff --git a/wgpu/tests/vertex_indices/draw.vert.wgsl b/wgpu/tests/vertex_indices/draw.vert.wgsl new file mode 100644 index 0000000000..69babbcd49 --- /dev/null +++ b/wgpu/tests/vertex_indices/draw.vert.wgsl @@ -0,0 +1,19 @@ +[[block]] +struct Indices { + arr: array; +}; // this is used as both input and output for convenience + +[[group(0), binding(0)]] +var indices: [[access(read_write)]] Indices; + +[[stage(vertex)]] +fn vs_main([[builtin(instance_index)]] instance: u32, [[builtin(vertex_index)]] index: u32) -> [[builtin(position)]] vec4 { + let idx = instance * 3u + index; + indices.arr[idx] = idx; + return vec4(0.0, 0.0, 0.0, 1.0); +} + +[[stage(fragment)]] +fn fs_main() -> [[location(0)]] vec4 { + return vec4(0.0); +} diff --git a/wgpu/tests/vertex_indices/mod.rs b/wgpu/tests/vertex_indices/mod.rs new file mode 100644 index 0000000000..e5e1074504 --- /dev/null +++ b/wgpu/tests/vertex_indices/mod.rs @@ -0,0 +1,178 @@ +use std::num::NonZeroU64; + +use wgpu::util::DeviceExt; + +use crate::common::{initialize_test, TestParameters, TestingContext}; + +fn pulling_common( + ctx: TestingContext, + expected: &[u32], + function: impl FnOnce(&mut wgpu::RenderPass<'_>), +) { + let shader = ctx + .device + .create_shader_module(&wgpu::include_wgsl!("draw.vert.wgsl")); + + let bgl = ctx + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: false }, + has_dynamic_offset: false, + min_binding_size: NonZeroU64::new(4), + }, + visibility: wgpu::ShaderStage::VERTEX, + count: None, + }], + }); + + let buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: 4 * expected.len() as u64, + usage: wgpu::BufferUsage::COPY_SRC + | wgpu::BufferUsage::STORAGE + | wgpu::BufferUsage::MAP_READ, + mapped_at_creation: false, + }); + + let bg = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + let ppl = ctx + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + + let pipeline = ctx + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&ppl), + vertex: wgpu::VertexState { + buffers: &[], + entry_point: "vs_main", + module: &shader, + }, + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + fragment: Some(wgpu::FragmentState { + entry_point: "fs_main", + module: &shader, + targets: &[wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8Unorm, + blend: None, + write_mask: wgpu::ColorWrite::ALL, + }], + }), + }); + + let dummy = ctx + .device + .create_texture_with_data( + &ctx.queue, + &wgpu::TextureDescriptor { + label: Some("dummy"), + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsage::RENDER_ATTACHMENT | wgpu::TextureUsage::COPY_DST, + }, + &[0, 0, 0, 1], + ) + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachment { + ops: wgpu::Operations::default(), + resolve_target: None, + view: &dummy, + }], + depth_stencil_attachment: None, + label: None, + }); + + rpass.set_pipeline(&pipeline); + rpass.set_bind_group(0, &bg, &[]); + function(&mut rpass); + + drop(rpass); + + ctx.queue.submit(Some(encoder.finish())); + let slice = buffer.slice(..); + let _ = slice.map_async(wgpu::MapMode::Read); + ctx.device.poll(wgpu::Maintain::Wait); + let data: Vec = bytemuck::cast_slice(&*slice.get_mapped_range()).to_vec(); + + assert_eq!(data, expected); +} + +#[test] +fn draw() { + initialize_test(TestParameters::default().test_features(), |ctx| { + pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { + cmb.draw(0..6, 0..1); + }) + }) +} + +#[test] +fn draw_vertex_offset() { + initialize_test( + TestParameters::default() + .test_features() + .backend_failures(wgpu::BackendBit::DX12 | wgpu::BackendBit::DX11), + |ctx| { + pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { + cmb.draw(0..3, 0..1); + cmb.draw(3..6, 0..1); + }) + }, + ) +} + +#[test] +fn draw_instanced() { + initialize_test(TestParameters::default().test_features(), |ctx| { + pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { + cmb.draw(0..3, 0..2); + }) + }) +} + +#[test] +fn draw_instanced_offset() { + initialize_test( + TestParameters::default() + .test_features() + .backend_failures(wgpu::BackendBit::DX12 | wgpu::BackendBit::DX11), + |ctx| { + pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { + cmb.draw(0..3, 0..1); + cmb.draw(0..3, 1..2); + }) + }, + ) +}