diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index d38c28a80d..10420e32d5 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) -> DownlevelProperties { + 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/tests/core-tests.rs b/wgpu/tests/core-tests.rs new file mode 100644 index 0000000000..2ffdb5fd2f --- /dev/null +++ b/wgpu/tests/core-tests.rs @@ -0,0 +1,11 @@ +mod core_tests { + // Contains all the helper routines which facilitate this testing framework. + mod common; + + // All files containing tests + mod instance; + mod device; + mod vertex_indexes; +} + +pub use core_tests::*; diff --git a/wgpu/tests/core_tests/common/init.rs b/wgpu/tests/core_tests/common/init.rs new file mode 100644 index 0000000000..ad87bb313e --- /dev/null +++ b/wgpu/tests/core_tests/common/init.rs @@ -0,0 +1,282 @@ +use std::panic::{catch_unwind, AssertUnwindSafe}; + +// Initialize the instance, obeying the WGPU_BACKEND environment variables. +pub fn get_backend_bits() -> wgpu::BackendBit { + match std::env::var("WGPU_BACKEND") + .as_deref() + .map(str::to_lowercase) + .as_deref() + { + Ok("vulkan") => wgpu::BackendBit::VULKAN, + Ok("dx12") => wgpu::BackendBit::DX12, + Ok("dx11") => wgpu::BackendBit::DX11, + Ok("metal") => wgpu::BackendBit::METAL, + Ok("gl") => wgpu::BackendBit::GL, + Ok("webgpu") => wgpu::BackendBit::BROWSER_WEBGPU, + Ok(_) => panic!("unknown wgpu backend"), + Err(_) => wgpu::BackendBit::all(), + } +} + +// Initialize the adapter, obeying the WGPU_ADAPTER_NAME environment variable. +pub fn initialize_adapter( + instance: &wgpu::Instance, + backend_bits: wgpu::BackendBit, +) -> wgpu::Adapter { + 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 chosen_adapter.is_none() { + if let Some(ref adapter_name) = desired_adapter_name { + if info.name.to_lowercase().contains(adapter_name) { + chosen_adapter = Some(adapter); + continue; // Skip adapter drop + } + } else { + // Just choose first adapter if there's no preference + chosen_adapter = Some(adapter); + continue; // Skip adapter drop + } + } + } + + chosen_adapter.expect("Could not find an adapter") +} + +pub fn initialize_device( + adapter: &wgpu::Adapter, + features: wgpu::Features, + limits: wgpu::Limits, +) -> (wgpu::Device, wgpu::Queue) { + let bundle = pollster::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: None, + features, + limits, + }, + None, + )); + + match bundle { + Ok(b) => b, + Err(e) => panic!("Failed to initialize device: {}", e), + } +} + +pub struct TestingContext<'a> { + pub adapter: &'a wgpu::Adapter, + pub adapter_info: wgt::AdapterInfo, + pub device: &'a wgpu::Device, + pub queue: &'a wgpu::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. +fn lowest_reasonable_limits() -> wgpu::Limits { + wgpu::Limits { + max_texture_dimension_1d: 512, + max_texture_dimension_2d: 512, + max_texture_dimension_3d: 32, + max_texture_array_layers: 32, + max_bind_groups: 1, + max_dynamic_uniform_buffers_per_pipeline_layout: 1, + max_dynamic_storage_buffers_per_pipeline_layout: 1, + max_sampled_textures_per_shader_stage: 1, + max_samplers_per_shader_stage: 1, + max_storage_buffers_per_shader_stage: 1, + max_storage_textures_per_shader_stage: 1, + max_uniform_buffers_per_shader_stage: 1, + max_uniform_buffer_binding_size: 256, + max_storage_buffer_binding_size: 256, + max_vertex_buffers: 1, + max_vertex_attributes: 2, + max_vertex_buffer_array_stride: 32, + max_push_constant_size: 0, + } +} + +fn lowest_downlevel_properties() -> wgpu::DownlevelProperties { + wgpu::DownlevelProperties { + flags: wgt::DownlevelFlags::empty(), + shader_model: wgt::ShaderModel::Sm2, + } +} + +// This information determines if a test should run. +pub struct TestParameters { + pub required_features: wgpu::Features, + pub required_limits: wgpu::Limits, + pub required_downlevel_properties: wgpu::DownlevelProperties, + // Test should always fail + pub always_failure: bool, + // Backends where test should fail. + pub backend_failures: wgpu::BackendBit, + // Vendors where test should fail. + pub vendor_failures: &'static [usize], + // Device names where test should fail. + pub device_failures: &'static [&'static str], +} + +impl Default for TestParameters { + fn default() -> Self { + Self { + required_features: wgpu::Features::empty(), + required_limits: lowest_reasonable_limits(), + required_downlevel_properties: lowest_downlevel_properties(), + always_failure: false, + backend_failures: wgpu::BackendBit::empty(), + vendor_failures: &[], + device_failures: &[], + } + } +} + +// Builder pattern to make it easier +impl TestParameters { + pub fn features(mut self, features: wgpu::Features) -> Self { + self.required_features &= features; + self + } + + pub fn failure(mut self) -> Self { + self.always_failure = true; + self + } +} + +pub fn initialize_test( + parameters: TestParameters, + test_function: impl FnOnce(&mut TestingContext<'_>), +) { + // We don't actually care if it fails + let _ = env_logger::try_init(); + + let backend_bits = get_backend_bits(); + let instance = wgpu::Instance::new(backend_bits); + let adapter = initialize_adapter(&instance, backend_bits); + + 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) = initialize_device( + &adapter, + parameters.required_features, + parameters.required_limits, + ); + + let mut context = TestingContext { + adapter: &adapter, + adapter_info: adapter_info.clone(), + device: &device, + queue: &queue, + }; + + let panicked = catch_unwind(AssertUnwindSafe(|| test_function(&mut context))).is_err(); + + let expect_failure_backend = parameters + .backend_failures + .contains(wgt::BackendBit::from(adapter_info.backend)); + let expect_failure_vendor = parameters + .vendor_failures + .iter() + .find(|&&v| v == adapter_info.vendor) + .is_some(); + let expect_failure_device = parameters + .device_failures + .iter() + .find(|&&v| adapter_lowercase_name.contains(&v.to_lowercase())); + + let expect_failure = parameters.always_failure + || expect_failure_backend + || expect_failure_vendor + || expect_failure_device.is_some(); + + if panicked == expect_failure { + // We got the conditions we expected + if expect_failure { + // Print out reason for the failure + if parameters.always_failure { + println!("GOT EXPECTED TEST FAILURE: ALWAYS"); + } + if expect_failure_backend { + println!( + "GOT EXPECTED TEST FAILURE: BACKEND {:?}", + adapter_info.backend + ); + } + if expect_failure_vendor { + println!("GOT EXPECTED TEST FAILURE: VENDOR {}", adapter_info.vendor); + } + if let Some(device_match) = expect_failure_device { + println!( + "GOT EXPECTED TEST FAILURE: DEVICE {} MATCHED NAME {}", + adapter_info.name, device_match + ); + } + } + } else { + if expect_failure { + // We expected to fail, but things passed + if parameters.always_failure { + panic!("UNEXPECTED TEST PASS: ALWAYS"); + } + if expect_failure_backend { + panic!("UNEXPECTED TEST PASS: BACKEND {:?}", adapter_info.backend); + } + if expect_failure_vendor { + panic!("UNEXPECTED TEST PASS: VENDOR {}", adapter_info.vendor); + } + if let Some(device_match) = expect_failure_device { + panic!( + "UNEXPECTED TEST PASS: DEVICE {} MATCHED NAME {}", + adapter_info.name, device_match + ); + } + unreachable!() + } else { + panic!("UNEXPECTED TEST FAILURE") + } + } +} diff --git a/wgpu/tests/core_tests/common/mod.rs b/wgpu/tests/core_tests/common/mod.rs new file mode 100644 index 0000000000..639a6c5b7b --- /dev/null +++ b/wgpu/tests/core_tests/common/mod.rs @@ -0,0 +1 @@ +pub mod init; \ No newline at end of file diff --git a/wgpu/tests/core_tests/device.rs b/wgpu/tests/core_tests/device.rs new file mode 100644 index 0000000000..600ef55b30 --- /dev/null +++ b/wgpu/tests/core_tests/device.rs @@ -0,0 +1,8 @@ +use crate::core_tests::common::init::{initialize_test, TestParameters}; + +#[test] +fn device_initialization() { + initialize_test(TestParameters::default(), |_ctx| { + // intentionally empty + }) +} diff --git a/wgpu/tests/core_tests/instance.rs b/wgpu/tests/core_tests/instance.rs new file mode 100644 index 0000000000..7690ada26f --- /dev/null +++ b/wgpu/tests/core_tests/instance.rs @@ -0,0 +1,26 @@ +use crate::core_tests::common; + +#[test] +fn initialize() { + let _ = wgpu::Instance::new(common::init::get_backend_bits()); +} + +fn request_adapter_inner(power: wgt::PowerPreference) { + let instance = wgpu::Instance::new(common::init::get_backend_bits()); + + 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/core_tests/vertex_indexes/draw.vert.wgsl b/wgpu/tests/core_tests/vertex_indexes/draw.vert.wgsl new file mode 100644 index 0000000000..fcbe921ec1 --- /dev/null +++ b/wgpu/tests/core_tests/vertex_indexes/draw.vert.wgsl @@ -0,0 +1,13 @@ +[[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(vertex_index)]] instance: u32, [[builtin(vertex_index)]] index: u32) -> [[builtin(position)]] vec4 { + indices.arr[index] = instance; + return vec4(0.0, 0.0, 0.0, 1.0); +} diff --git a/wgpu/tests/core_tests/vertex_indexes/mod.rs b/wgpu/tests/core_tests/vertex_indexes/mod.rs new file mode 100644 index 0000000000..ee8720143f --- /dev/null +++ b/wgpu/tests/core_tests/vertex_indexes/mod.rs @@ -0,0 +1,13 @@ +use crate::core_tests::common::init::{initialize_test, TestParameters}; + +#[test] +fn draw() { + initialize_test( + TestParameters::default().features(wgpu::Features::VERTEX_WRITABLE_STORAGE), + |ctx| { + let shader_module = ctx + .device + .create_shader_module(&wgpu::include_wgsl!("draw.vert.wgsl")); + }, + ) +} diff --git a/wgpu/tests/example-wgsl.rs b/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"))