diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 95d2a35175..0000000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,7 +0,0 @@ -[alias] -xtask = "run --manifest-path xtask/Cargo.toml --" - -[build] -rustflags = [ -"--cfg=web_sys_unstable_apis" -] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4ed15c4a7..981fcd3498 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,7 @@ jobs: steps: - name: checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install MSRV toolchain run: | @@ -181,7 +181,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install wasm-pack uses: taiki-e/install-action@v2 @@ -219,7 +219,7 @@ jobs: steps: - name: checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install cargo-nextest and cargo-llvm-cov uses: taiki-e/install-action@v2 @@ -284,6 +284,7 @@ jobs: done - uses: actions/upload-artifact@v3 + if: always() # We want artifacts even if the tests fail. with: name: comparison-images path: | @@ -308,7 +309,7 @@ jobs: steps: - name: checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: disable debug shell: bash @@ -335,7 +336,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: run rustfmt run: | @@ -346,7 +347,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install MSRV toolchain run: | @@ -375,7 +376,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run `cargo deny check` uses: EmbarkStudios/cargo-deny-action@v1 @@ -389,7 +390,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run `cargo deny check` uses: EmbarkStudios/cargo-deny-action@v1 diff --git a/.github/workflows/cts.yml b/.github/workflows/cts.yml index 70479533cf..e4bb20e7b1 100644 --- a/.github/workflows/cts.yml +++ b/.github/workflows/cts.yml @@ -39,7 +39,7 @@ jobs: steps: - name: checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: wgpu diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 11d8d9e962..396a93ef04 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 81a2a7b407..f0aa086961 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 801010778d..400b7dee72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,22 +70,31 @@ By @Valaphee in [#3402](https://github.com/gfx-rs/wgpu/pull/3402) ### Changes +#### General + - Omit texture store bound checks since they are no-ops if out of bounds on all APIs. By @teoxoy in [#3975](https://github.com/gfx-rs/wgpu/pull/3975) - Validate `DownlevelFlags::READ_ONLY_DEPTH_STENCIL`. By @teoxoy in [#4031](https://github.com/gfx-rs/wgpu/pull/4031) - Add validation in accordance with WebGPU `setViewport` valid usage for `x`, `y` and `this.[[attachment_size]]`. By @James2022-rgb in [#4058](https://github.com/gfx-rs/wgpu/pull/4058) +- `wgpu::CreateSurfaceError` now gives details of the failure, but no longer implements `PartialEq`. By @kpreid in [#4066](https://github.com/gfx-rs/wgpu/pull/4066) - Make `WGPU_POWER_PREF=none` a valid value. By @fornwall in [4076](https://github.com/gfx-rs/wgpu/pull/4076) #### Vulkan +- Rename `wgpu_hal::vulkan::Instance::required_extensions` to `desired_extensions`. By @jimblandy in [#4115](https://github.com/gfx-rs/wgpu/pull/4115) + - Don't bother calling `vkFreeCommandBuffers` when `vkDestroyCommandPool` will take care of that for us. By @jimblandy in [#4059](https://github.com/gfx-rs/wgpu/pull/4059) +### Documentation +- Use WGSL for VertexFormat example types. By @ScanMountGoat in [#4305](https://github.com/gfx-rs/wgpu/pull/4035) + ### Bug Fixes #### General - Derive storage bindings via `naga::StorageAccess` instead of `naga::GlobalUse`. By @teoxoy in [#3985](https://github.com/gfx-rs/wgpu/pull/3985). - `Queue::on_submitted_work_done` callbacks will now always be called after all previous `BufferSlice::map_async` callbacks, even when there are no active submissions. By @cwfitzgerald in [#4036](https://github.com/gfx-rs/wgpu/pull/4036). +- Fix `clear` texture views being leaked when `wgpu::SurfaceTexture` is dropped before it is presented. By @rajveermalviya in [#4057](https://github.com/gfx-rs/wgpu/pull/4057). #### Vulkan - Fix enabling `wgpu::Features::PARTIALLY_BOUND_BINDING_ARRAY` not being actually enabled in vulkan backend. By @39ali in[#3772](https://github.com/gfx-rs/wgpu/pull/3772). @@ -94,11 +103,25 @@ By @Valaphee in [#3402](https://github.com/gfx-rs/wgpu/pull/3402) - Enhancement of [#4038], using ash's definition instead of hard-coded c_str. By @hybcloud in[#4044](https://github.com/gfx-rs/wgpu/pull/4044). +- Enable vulkan presentation on (Linux) Intel Mesa >= v21.2. By @flukejones in[#4110](https://github.com/gfx-rs/wgpu/pull/4110) + #### DX12 - DX12 doesn't support `Features::POLYGON_MODE_POINT``. By @teoxoy in [#4032](https://github.com/gfx-rs/wgpu/pull/4032). - Set `Features::VERTEX_WRITABLE_STORAGE` based on the right feature level. By @teoxoy in [#4033](https://github.com/gfx-rs/wgpu/pull/4033). +#### Metal + +- Ensure that MTLCommandEncoder calls endEncoding before it is deallocated. By @bradwerth in [#4023](https://github.com/gfx-rs/wgpu/pull/4023) + +#### WebGPU + +- Ensure that limit requests and reporting is done correctly. By @OptimisticPeach in [#4107](https://github.com/gfx-rs/wgpu/pull/4107) + +#### Testing + +- Skip `test_multithreaded_compute` on MoltenVK. By @jimblandy in [#4096](https://github.com/gfx-rs/wgpu/pull/4096). + ### Documentation - Add an overview of `RenderPass` and how render state works. By @kpreid in [#4055](https://github.com/gfx-rs/wgpu/pull/4055) @@ -113,7 +136,7 @@ This release was fairly minor as breaking changes go. #### `wgpu` types now `!Send` `!Sync` on wasm -Up until this point, wgpu has made the assumption that threads do not exist on wasm. With the rise of libraries like [`wasm_thread`](https://crates.io/crates/wasm_thread) making it easier and easier to do wasm multithreading this assumption is no longer sound. As all wgpu objects contain references into the JS heap, they cannot leave the thread they started on. +Up until this point, wgpu has made the assumption that threads do not exist on wasm. With the rise of libraries like [`wasm_thread`](https://crates.io/crates/wasm_thread) making it easier and easier to do wasm multithreading this assumption is no longer sound. As all wgpu objects contain references into the JS heap, they cannot leave the thread they started on. As we understand that this change might be very inconvenient for users who don't care about wasm threading, there is a crate feature which re-enables the old behavior: `fragile-send-sync-non-atomic-wasm`. So long as you don't compile your code with `-Ctarget-feature=+atomics`, `Send` and `Sync` will be implemented again on wgpu types on wasm. As the name implies, especially for libraries, this is very fragile, as you don't know if a user will want to compile with atomics (and therefore threads) or not. diff --git a/Cargo.lock b/Cargo.lock index a492279cf0..c8385fc6e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,9 +262,9 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" dependencies = [ "bytemuck_derive", ] @@ -1473,9 +1473,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libloading" @@ -1610,7 +1610,7 @@ dependencies = [ [[package]] name = "naga" version = "0.13.0" -source = "git+https://github.com/gfx-rs/naga?rev=7a19f3af909202c7eafd36633b5584bfbb353ecb#7a19f3af909202c7eafd36633b5584bfbb353ecb" +source = "git+https://github.com/gfx-rs/naga?rev=cc87b8f9eb30bb55d0735b89d3df3e099e1a6e7c#cc87b8f9eb30bb55d0735b89d3df3e099e1a6e7c" dependencies = [ "bit-set", "bitflags 2.4.0", @@ -2134,9 +2134,9 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46b2164ebdb1dfeec5e337be164292351e11daf63a05174c6776b2f47460f0c9" +checksum = "45f10e75d83c7aec79a6aa46f897075890e156b105eebe51cfa0abce51af025f" [[package]] name = "quote" @@ -2436,9 +2436,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "indexmap 2.0.0", "itoa", @@ -2649,18 +2649,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9455290b3d..55c6048b86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ version = "0.17" [workspace.dependencies.naga] git = "https://github.com/gfx-rs/naga" -rev = "7a19f3af909202c7eafd36633b5584bfbb353ecb" +rev = "cc87b8f9eb30bb55d0735b89d3df3e099e1a6e7c" version = "0.13.0" [workspace.dependencies] @@ -60,7 +60,7 @@ arrayvec = "0.7" async-executor = "1" bitflags = "2" bit-vec = "0.6" -bytemuck = { version = "1.13", features = ["derive"] } +bytemuck = { version = "1.14", features = ["derive"] } cfg_aliases = "0.1" cfg-if = "1" codespan-reporting = "0.11" @@ -89,7 +89,7 @@ raw-window-handle = "0.5" renderdoc-sys = "1.0.0" ron = "0.8" serde = "1" -serde_json = "1.0.105" +serde_json = "1.0.107" smallvec = "1" static_assertions = "1.1.0" thiserror = "1" diff --git a/examples/boids/src/main.rs b/examples/boids/src/main.rs index 357792de4f..e8aa2f71fd 100644 --- a/examples/boids/src/main.rs +++ b/examples/boids/src/main.rs @@ -345,7 +345,7 @@ fn boids() { .downlevel_flags(wgpu::DownlevelFlags::COMPUTE_SHADERS) .limits(wgpu::Limits::downlevel_defaults()) // Lots of validation errors, maybe related to https://github.com/gfx-rs/wgpu/issues/3160 - .molten_vk_failure(), + .expect_fail(wgpu_test::FailureCase::molten_vk()), comparisons: &[wgpu_test::ComparisonType::Mean(0.005)], }); } diff --git a/examples/common/src/framework.rs b/examples/common/src/framework.rs index 06db6092f7..875d8544e7 100644 --- a/examples/common/src/framework.rs +++ b/examples/common/src/framework.rs @@ -625,7 +625,7 @@ pub fn test(mut params: FrameworkRefTest) { wgpu_test::image::compare_image_output( env!("CARGO_MANIFEST_DIR").to_string() + "/../../" + params.image_path, - ctx.adapter_info.backend, + &ctx.adapter_info, params.width, params.height, &bytes, diff --git a/examples/hello-compute/src/tests.rs b/examples/hello-compute/src/tests.rs index 54cddbe379..7f8649f72f 100644 --- a/examples/hello-compute/src/tests.rs +++ b/examples/hello-compute/src/tests.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use super::*; -use wgpu_test::{initialize_test, TestParameters}; +use wgpu_test::{initialize_test, FailureCase, TestParameters}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -13,7 +13,7 @@ fn test_compute_1() { .downlevel_flags(wgpu::DownlevelFlags::COMPUTE_SHADERS) .limits(wgpu::Limits::downlevel_defaults()) .features(wgpu::Features::TIMESTAMP_QUERY) - .specific_failure(None, None, Some("V3D"), true), + .skip(FailureCase::adapter("V3D")), |ctx| { let input = &[1, 2, 3, 4]; @@ -35,7 +35,7 @@ fn test_compute_2() { .downlevel_flags(wgpu::DownlevelFlags::COMPUTE_SHADERS) .limits(wgpu::Limits::downlevel_defaults()) .features(wgpu::Features::TIMESTAMP_QUERY) - .specific_failure(None, None, Some("V3D"), true), + .skip(FailureCase::adapter("V3D")), |ctx| { let input = &[5, 23, 10, 9]; @@ -57,7 +57,7 @@ fn test_compute_overflow() { .downlevel_flags(wgpu::DownlevelFlags::COMPUTE_SHADERS) .limits(wgpu::Limits::downlevel_defaults()) .features(wgpu::Features::TIMESTAMP_QUERY) - .specific_failure(None, None, Some("V3D"), true), + .skip(FailureCase::adapter("V3D")), |ctx| { let input = &[77031, 837799, 8400511, 63728127]; pollster::block_on(assert_execute_gpu( @@ -78,16 +78,15 @@ fn test_multithreaded_compute() { .downlevel_flags(wgpu::DownlevelFlags::COMPUTE_SHADERS) .limits(wgpu::Limits::downlevel_defaults()) .features(wgpu::Features::TIMESTAMP_QUERY) - .specific_failure(None, None, Some("V3D"), true) + .skip(FailureCase::adapter("V3D")) // https://github.com/gfx-rs/wgpu/issues/3944 - .specific_failure( - Some(wgpu::Backends::VULKAN), - None, - Some("swiftshader"), - true, - ) + .skip(FailureCase::backend_adapter( + wgpu::Backends::VULKAN, + "swiftshader", + )) // https://github.com/gfx-rs/wgpu/issues/3250 - .specific_failure(Some(wgpu::Backends::GL), None, Some("llvmpipe"), true), + .skip(FailureCase::backend_adapter(wgpu::Backends::GL, "llvmpipe")) + .skip(FailureCase::molten_vk()), |ctx| { use std::{sync::mpsc, thread, time::Duration}; diff --git a/examples/mipmap/src/main.rs b/examples/mipmap/src/main.rs index d21f6c1e08..a85110ff14 100644 --- a/examples/mipmap/src/main.rs +++ b/examples/mipmap/src/main.rs @@ -521,7 +521,7 @@ fn mipmap() { height: 768, optional_features: wgpu::Features::default(), base_test_parameters: wgpu_test::TestParameters::default() - .backend_failure(wgpu::Backends::GL), + .expect_fail(wgpu_test::FailureCase::backend(wgpu::Backends::GL)), comparisons: &[wgpu_test::ComparisonType::Mean(0.02)], }); } @@ -535,7 +535,7 @@ fn mipmap_query() { height: 768, optional_features: QUERY_FEATURES, base_test_parameters: wgpu_test::TestParameters::default() - .backend_failure(wgpu::Backends::GL), + .expect_fail(wgpu_test::FailureCase::backend(wgpu::Backends::GL)), comparisons: &[wgpu_test::ComparisonType::Mean(0.02)], }); } diff --git a/examples/msaa-line/src/main.rs b/examples/msaa-line/src/main.rs index 2f42817765..aa7a277418 100644 --- a/examples/msaa-line/src/main.rs +++ b/examples/msaa-line/src/main.rs @@ -12,6 +12,9 @@ use std::{borrow::Cow, iter}; use bytemuck::{Pod, Zeroable}; use wgpu::util::DeviceExt; +#[cfg(test)] +use wgpu_test::FailureCase; + #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct Vertex { @@ -326,7 +329,11 @@ fn msaa_line() { optional_features: wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, base_test_parameters: wgpu_test::TestParameters::default() // AMD seems to render nothing on DX12 https://github.com/gfx-rs/wgpu/issues/3838 - .specific_failure(Some(wgpu::Backends::DX12), Some(0x1002), None, false), + .expect_fail(FailureCase { + backends: Some(wgpu::Backends::DX12), + vendor: Some(0x1002), + ..FailureCase::default() + }), // There's a lot of natural variance so we check the weighted median too to differentiate // real failures from variance. comparisons: &[ diff --git a/examples/shadow/src/main.rs b/examples/shadow/src/main.rs index 09b0982ea9..3f963d0c53 100644 --- a/examples/shadow/src/main.rs +++ b/examples/shadow/src/main.rs @@ -857,9 +857,15 @@ fn shadow() { base_test_parameters: wgpu_test::TestParameters::default() .downlevel_flags(wgpu::DownlevelFlags::COMPARISON_SAMPLERS) // rpi4 on VK doesn't work: https://gitlab.freedesktop.org/mesa/mesa/-/issues/3916 - .specific_failure(Some(wgpu::Backends::VULKAN), None, Some("V3D"), false) + .expect_fail(wgpu_test::FailureCase::backend_adapter( + wgpu::Backends::VULKAN, + "V3D", + )) // llvmpipe versions in CI are flaky: https://github.com/gfx-rs/wgpu/issues/2594 - .specific_failure(Some(wgpu::Backends::VULKAN), None, Some("llvmpipe"), true), + .skip(wgpu_test::FailureCase::backend_adapter( + wgpu::Backends::VULKAN, + "llvmpipe", + )), comparisons: &[wgpu_test::ComparisonType::Mean(0.02)], }); } diff --git a/examples/skybox/src/main.rs b/examples/skybox/src/main.rs index 9873ac9c0b..d09622f53c 100644 --- a/examples/skybox/src/main.rs +++ b/examples/skybox/src/main.rs @@ -475,11 +475,8 @@ fn skybox() { width: 1024, height: 768, optional_features: wgpu::Features::default(), - base_test_parameters: wgpu_test::TestParameters::default().specific_failure( - Some(wgpu::Backends::GL), - None, - Some("ANGLE"), - false, + base_test_parameters: wgpu_test::TestParameters::default().expect_fail( + wgpu_test::FailureCase::backend_adapter(wgpu::Backends::GL, "ANGLE"), ), comparisons: &[wgpu_test::ComparisonType::Mean(0.015)], }); diff --git a/player/src/lib.rs b/player/src/lib.rs index a4be0b1c81..fbfb2697d1 100644 --- a/player/src/lib.rs +++ b/player/src/lib.rs @@ -158,7 +158,7 @@ impl GlobalPlay for wgc::global::Global { let (cmd_buf, error) = self .command_encoder_finish::(encoder, &wgt::CommandBufferDescriptor { label: None }); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } cmd_buf } @@ -186,7 +186,7 @@ impl GlobalPlay for wgc::global::Global { self.device_maintain_ids::(device).unwrap(); let (_, error) = self.device_create_buffer::(device, &desc, id); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::FreeBuffer(id) => { @@ -199,7 +199,7 @@ impl GlobalPlay for wgc::global::Global { self.device_maintain_ids::(device).unwrap(); let (_, error) = self.device_create_texture::(device, &desc, id); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::FreeTexture(id) => { @@ -216,7 +216,7 @@ impl GlobalPlay for wgc::global::Global { self.device_maintain_ids::(device).unwrap(); let (_, error) = self.texture_create_view::(parent_id, &desc, id); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::DestroyTextureView(id) => { @@ -226,7 +226,7 @@ impl GlobalPlay for wgc::global::Global { self.device_maintain_ids::(device).unwrap(); let (_, error) = self.device_create_sampler::(device, &desc, id); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::DestroySampler(id) => { @@ -242,7 +242,7 @@ impl GlobalPlay for wgc::global::Global { Action::CreateBindGroupLayout(id, desc) => { let (_, error) = self.device_create_bind_group_layout::(device, &desc, id); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::DestroyBindGroupLayout(id) => { @@ -252,7 +252,7 @@ impl GlobalPlay for wgc::global::Global { self.device_maintain_ids::(device).unwrap(); let (_, error) = self.device_create_pipeline_layout::(device, &desc, id); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::DestroyPipelineLayout(id) => { @@ -262,7 +262,7 @@ impl GlobalPlay for wgc::global::Global { self.device_maintain_ids::(device).unwrap(); let (_, error) = self.device_create_bind_group::(device, &desc, id); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::DestroyBindGroup(id) => { @@ -272,7 +272,7 @@ impl GlobalPlay for wgc::global::Global { log::info!("Creating shader from {}", data); let code = fs::read_to_string(dir.join(&data)).unwrap(); let source = if data.ends_with(".wgsl") { - wgc::pipeline::ShaderModuleSource::Wgsl(Cow::Owned(code)) + wgc::pipeline::ShaderModuleSource::Wgsl(Cow::Owned(code.clone())) } else if data.ends_with(".ron") { let module = ron::de::from_str(&code).unwrap(); wgc::pipeline::ShaderModuleSource::Naga(module) @@ -281,7 +281,7 @@ impl GlobalPlay for wgc::global::Global { }; let (_, error) = self.device_create_shader_module::(device, &desc, source, id); if let Some(e) = error { - panic!("{:?}", e); + println!("shader compilation error:\n---{code}\n---\n{e}"); } } Action::DestroyShaderModule(id) => { @@ -303,7 +303,7 @@ impl GlobalPlay for wgc::global::Global { let (_, error) = self.device_create_compute_pipeline::(device, &desc, id, implicit_ids); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::DestroyComputePipeline(id) => { @@ -325,7 +325,7 @@ impl GlobalPlay for wgc::global::Global { let (_, error) = self.device_create_render_pipeline::(device, &desc, id, implicit_ids); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::DestroyRenderPipeline(id) => { @@ -340,7 +340,7 @@ impl GlobalPlay for wgc::global::Global { id, ); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::DestroyRenderBundle(id) => { @@ -350,7 +350,7 @@ impl GlobalPlay for wgc::global::Global { self.device_maintain_ids::(device).unwrap(); let (_, error) = self.device_create_query_set::(device, &desc, id); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } } Action::DestroyQuerySet(id) => { @@ -393,7 +393,7 @@ impl GlobalPlay for wgc::global::Global { comb_manager.alloc(device.backend()), ); if let Some(e) = error { - panic!("{:?}", e); + panic!("{e}"); } let cmdbuf = self.encode_commands::(encoder, commands); self.queue_submit::(device, &[cmdbuf]).unwrap(); diff --git a/tests/src/image.rs b/tests/src/image.rs index 00aa78f660..e50fd43e7f 100644 --- a/tests/src/image.rs +++ b/tests/src/image.rs @@ -150,7 +150,7 @@ impl ComparisonType { pub fn compare_image_output( path: impl AsRef + AsRef, - backend: Backend, + adapter_info: &wgt::AdapterInfo, width: u32, height: u32, test_with_alpha: &[u8], @@ -205,17 +205,18 @@ pub fn compare_image_output( } let file_stem = reference_path.file_stem().unwrap().to_string_lossy(); + let renderer = format!( + "{}-{}-{}", + adapter_info.backend.to_str(), + sanitize_for_path(&adapter_info.name), + sanitize_for_path(&adapter_info.driver) + ); // Determine the paths to write out the various intermediate files let actual_path = Path::new(&path).with_file_name( - OsString::from_str(&format!("{}-{}-actual.png", file_stem, backend.to_str(),)).unwrap(), + OsString::from_str(&format!("{}-{}-actual.png", file_stem, renderer)).unwrap(), ); let difference_path = Path::new(&path).with_file_name( - OsString::from_str(&format!( - "{}-{}-difference.png", - file_stem, - backend.to_str(), - )) - .unwrap(), + OsString::from_str(&format!("{}-{}-difference.png", file_stem, renderer,)).unwrap(), ); // Convert the error values to a false color reprensentation @@ -246,10 +247,16 @@ pub fn compare_image_output( #[cfg(target_arch = "wasm32")] { - let _ = (path, backend, width, height, test_with_alpha, checks); + let _ = (path, adapter_info, width, height, test_with_alpha, checks); } } +fn sanitize_for_path(s: &str) -> String { + s.chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + fn copy_via_compute( device: &Device, encoder: &mut CommandEncoder, diff --git a/tests/src/lib.rs b/tests/src/lib.rs index fb57d2a5a8..236b353386 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -53,11 +53,195 @@ fn lowest_downlevel_properties() -> DownlevelCapabilities { } } +/// Conditions under which a test should fail or be skipped. +/// +/// By passing a `FailureCase` to [`TestParameters::expect_fail`], you can +/// mark a test as expected to fail under the indicated conditions. By +/// passing it to [`TestParameters::skip`], you can request that the +/// test be skipped altogether. +/// +/// If a field is `None`, then that field does not restrict matches. For +/// example: +/// +/// ``` +/// # use wgpu_test::FailureCase; +/// FailureCase { +/// backends: Some(wgpu::Backends::DX11 | wgpu::Backends::DX12), +/// vendor: None, +/// adapter: Some("RTX"), +/// driver: None, +/// } +/// # ; +/// ``` +/// +/// This applies to all cards with `"RTX'` in their name on either +/// Direct3D backend, no matter the vendor ID or driver name. +/// +/// The strings given here need only appear as a substring in the +/// corresponding [`AdapterInfo`] fields. The comparison is +/// case-insensitive. +/// +/// The default value of `FailureCase` applies to any test case. That +/// is, there are no criteria to constrain the match. +/// +/// [`AdapterInfo`]: wgt::AdapterInfo +#[derive(Default)] pub struct FailureCase { - backends: Option, - vendor: Option, - adapter: Option, - skip: bool, + /// Backends expected to fail, or `None` for any backend. + /// + /// If this is `None`, or if the test is using one of the backends + /// in `backends`, then this `FailureCase` applies. + pub backends: Option, + + /// Vendor expected to fail, or `None` for any vendor. + /// + /// If `Some`, this must match [`AdapterInfo::device`], which is + /// usually the PCI device id. Otherwise, this `FailureCase` + /// applies regardless of vendor. + /// + /// [`AdapterInfo::device`]: wgt::AdapterInfo::device + pub vendor: Option, + + /// Name of adaper expected to fail, or `None` for any adapter name. + /// + /// If this is `Some(s)` and `s` is a substring of + /// [`AdapterInfo::name`], then this `FailureCase` applies. If + /// this is `None`, the adapter name isn't considered. + /// + /// [`AdapterInfo::name`]: wgt::AdapterInfo::name + pub adapter: Option<&'static str>, + + /// Name of driver expected to fail, or `None` for any driver name. + /// + /// If this is `Some(s)` and `s` is a substring of + /// [`AdapterInfo::driver`], then this `FailureCase` applies. If + /// this is `None`, the driver name isn't considered. + /// + /// [`AdapterInfo::driver`]: wgt::AdapterInfo::driver + pub driver: Option<&'static str>, +} + +impl FailureCase { + /// This case applies to all tests. + pub fn always() -> Self { + FailureCase::default() + } + + /// This case applies to no tests. + pub fn never() -> Self { + FailureCase { + backends: Some(wgpu::Backends::empty()), + ..FailureCase::default() + } + } + + /// Tests running on any of the given backends. + pub fn backend(backends: wgpu::Backends) -> Self { + FailureCase { + backends: Some(backends), + ..FailureCase::default() + } + } + + /// Tests running on `adapter`. + /// + /// For this case to apply, the `adapter` string must appear as a substring + /// of the adapter's [`AdapterInfo::name`]. The comparison is + /// case-insensitive. + /// + /// [`AdapterInfo::name`]: wgt::AdapterInfo::name + pub fn adapter(adapter: &'static str) -> Self { + FailureCase { + adapter: Some(adapter), + ..FailureCase::default() + } + } + + /// Tests running on `backend` and `adapter`. + /// + /// For this case to apply, the test must be using an adapter for one of the + /// given `backend` bits, and `adapter` string must appear as a substring of + /// the adapter's [`AdapterInfo::name`]. The string comparison is + /// case-insensitive. + /// + /// [`AdapterInfo::name`]: wgt::AdapterInfo::name + pub fn backend_adapter(backends: wgpu::Backends, adapter: &'static str) -> Self { + FailureCase { + backends: Some(backends), + adapter: Some(adapter), + ..FailureCase::default() + } + } + + /// Tests running under WebGL. + /// + /// Because of wasm's limited ability to recover from errors, we + /// usually need to skip the test altogether if it's not + /// supported, so this should be usually used with + /// [`TestParameters::skip`]. + pub fn webgl2() -> Self { + #[cfg(target_arch = "wasm32")] + let case = FailureCase::backend(wgpu::Backends::GL); + #[cfg(not(target_arch = "wasm32"))] + let case = FailureCase::never(); + case + } + + /// Tests running on the MoltenVK Vulkan driver on macOS. + pub fn molten_vk() -> Self { + FailureCase { + backends: Some(wgpu::Backends::VULKAN), + driver: Some("MoltenVK"), + ..FailureCase::default() + } + } + + /// Test whether `self` applies to `info`. + /// + /// If it does, return a `FailureReasons` whose set bits indicate + /// why. If it doesn't, return `None`. + /// + /// The caller is responsible for converting the string-valued + /// fields of `info` to lower case, to ensure case-insensitive + /// matching. + fn applies_to(&self, info: &wgt::AdapterInfo) -> Option { + let mut reasons = FailureReasons::empty(); + + if let Some(backends) = self.backends { + if !backends.contains(wgpu::Backends::from(info.backend)) { + return None; + } + reasons.set(FailureReasons::BACKEND, true); + } + if let Some(vendor) = self.vendor { + if vendor != info.vendor { + return None; + } + reasons.set(FailureReasons::VENDOR, true); + } + if let Some(adapter) = self.adapter { + let adapter = adapter.to_lowercase(); + if !info.name.contains(&adapter) { + return None; + } + reasons.set(FailureReasons::ADAPTER, true); + } + if let Some(driver) = self.driver { + let driver = driver.to_lowercase(); + if !info.driver.contains(&driver) { + return None; + } + reasons.set(FailureReasons::DRIVER, true); + } + + // If we got this far but no specific reasons were triggered, then this + // must be a wildcard. + if reasons.is_empty() { + Some(FailureReasons::ALWAYS) + } else { + Some(reasons) + } + } } // This information determines if a test should run. @@ -65,7 +249,11 @@ pub struct TestParameters { pub required_features: Features, pub required_downlevel_properties: DownlevelCapabilities, pub required_limits: Limits, - // Backends where test should fail. + + /// Conditions under which this test should be skipped. + pub skips: Vec, + + /// Conditions under which this test should be run, but is expected to fail. pub failures: Vec, } @@ -75,6 +263,7 @@ impl Default for TestParameters { required_features: Features::empty(), required_downlevel_properties: lowest_downlevel_properties(), required_limits: Limits::downlevel_webgl2_defaults(), + skips: Vec::new(), failures: Vec::new(), } } @@ -86,7 +275,8 @@ bitflags::bitflags! { const BACKEND = 1 << 0; const VENDOR = 1 << 1; const ADAPTER = 1 << 2; - const ALWAYS = 1 << 3; + const DRIVER = 1 << 3; + const ALWAYS = 1 << 4; } } @@ -115,87 +305,17 @@ impl TestParameters { self } - /// Mark the test as always failing, equivalent to specific_failure(None, None, None) - pub fn failure(mut self) -> Self { - self.failures.push(FailureCase { - backends: None, - vendor: None, - adapter: None, - skip: false, - }); + /// Mark the test as always failing, but not to be skipped. + pub fn expect_fail(mut self, when: FailureCase) -> Self { + self.failures.push(when); self } - /// Mark the test as always failing and needing to be skipped, equivalent to specific_failure(None, None, None) - pub fn skip(mut self) -> Self { - self.failures.push(FailureCase { - backends: None, - vendor: None, - adapter: None, - skip: true, - }); + /// Mark the test as always failing, and needing to be skipped. + pub fn skip(mut self, when: FailureCase) -> Self { + self.skips.push(when); self } - - /// Mark the test as always failing on a specific backend, equivalent to specific_failure(backend, None, None) - pub fn backend_failure(mut self, backends: wgpu::Backends) -> Self { - self.failures.push(FailureCase { - backends: Some(backends), - vendor: None, - adapter: None, - skip: false, - }); - self - } - - /// Mark the test as always failing on WebGL. Because limited ability of wasm to recover from errors, we need to wholesale - /// skip the test if it's not supported. - pub fn webgl2_failure(mut self) -> Self { - let _ = &mut self; - #[cfg(target_arch = "wasm32")] - self.failures.push(FailureCase { - backends: Some(wgpu::Backends::GL), - vendor: None, - adapter: None, - skip: true, - }); - 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::Backends::DX11 | wgpu::Backends::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>, - skip: bool, - ) -> Self { - self.failures.push(FailureCase { - backends, - vendor, - adapter: device.as_ref().map(AsRef::as_ref).map(str::to_lowercase), - skip, - }); - self - } - - /// Mark the test as failing on vulkan on mac only - pub fn molten_vk_failure(self) -> Self { - #[cfg(any(target_os = "macos", target_os = "ios"))] - { - self.specific_failure(Some(wgpu::Backends::VULKAN), None, None, false) - } - #[cfg(not(any(target_os = "macos", target_os = "ios")))] - { - self - } - } } pub fn initialize_test(parameters: TestParameters, test_function: impl FnOnce(TestingContext)) { @@ -210,7 +330,15 @@ pub fn initialize_test(parameters: TestParameters, test_function: impl FnOnce(Te let (adapter, _surface_guard) = initialize_adapter(); let adapter_info = adapter.get_info(); - let adapter_lowercase_name = adapter_info.name.to_lowercase(); + + // Produce a lower-case version of the adapter info, for comparison against + // `parameters.skips` and `parameters.failures`. + let adapter_lowercase_info = wgt::AdapterInfo { + name: adapter_info.name.to_lowercase(), + driver: adapter_info.driver.to_lowercase(), + ..adapter_info.clone() + }; + let adapter_features = adapter.features(); let adapter_limits = adapter.limits(); let adapter_downlevel_capabilities = adapter.get_downlevel_capabilities(); @@ -254,7 +382,7 @@ pub fn initialize_test(parameters: TestParameters, test_function: impl FnOnce(Te let context = TestingContext { adapter, - adapter_info: adapter_info.clone(), + adapter_info, adapter_downlevel_capabilities, device, device_features: parameters.required_features, @@ -262,107 +390,77 @@ pub fn initialize_test(parameters: TestParameters, test_function: impl FnOnce(Te queue, }; - let expected_failure_reason = parameters.failures.iter().find_map(|failure| { - let always = - failure.backends.is_none() && failure.vendor.is_none() && failure.adapter.is_none(); - - let expect_failure_backend = failure - .backends - .map(|f| f.contains(wgpu::Backends::from(adapter_info.backend))); - let expect_failure_vendor = failure.vendor.map(|v| v == adapter_info.vendor); - let expect_failure_adapter = failure - .adapter - .as_deref() - .map(|f| adapter_lowercase_name.contains(f)); - - if expect_failure_backend.unwrap_or(true) - && expect_failure_vendor.unwrap_or(true) - && expect_failure_adapter.unwrap_or(true) - { - if always { - Some((FailureReasons::ALWAYS, failure.skip)) - } else { - let mut reason = FailureReasons::empty(); - reason.set( - FailureReasons::BACKEND, - expect_failure_backend.unwrap_or(false), - ); - reason.set( - FailureReasons::VENDOR, - expect_failure_vendor.unwrap_or(false), - ); - reason.set( - FailureReasons::ADAPTER, - expect_failure_adapter.unwrap_or(false), - ); - Some((reason, failure.skip)) - } - } else { - None - } - }); - - if let Some((reason, true)) = expected_failure_reason { - log::info!("EXPECTED TEST FAILURE SKIPPED: {:?}", reason); + // Check if we should skip the test altogether. + if let Some(skip_reason) = parameters + .skips + .iter() + .find_map(|case| case.applies_to(&adapter_lowercase_info)) + { + log::info!("EXPECTED TEST FAILURE SKIPPED: {:?}", skip_reason); return; } + // Determine if we expect this test to fail, and if so, why. + let expected_failure_reason = parameters + .failures + .iter() + .find_map(|case| case.applies_to(&adapter_lowercase_info)); + + // Run the test, and catch panics (possibly due to failed assertions). let panicked = catch_unwind(AssertUnwindSafe(|| test_function(context))).is_err(); + + // Check whether any validation errors were reported during the test run. cfg_if::cfg_if!( if #[cfg(any(not(target_arch = "wasm32"), target_os = "emscripten"))] { let canary_set = wgpu::hal::VALIDATION_CANARY.get_and_reset(); } else { - let canary_set = _surface_guard.check_for_unreported_errors(); + let canary_set = _surface_guard.unwrap().check_for_unreported_errors(); } ); - let failed = panicked || canary_set; - + // Summarize reasons for actual failure, if any. let failure_cause = match (panicked, canary_set) { - (true, true) => "PANIC AND VALIDATION ERROR", - (true, false) => "PANIC", - (false, true) => "VALIDATION ERROR", - (false, false) => "", + (true, true) => Some("PANIC AND VALIDATION ERROR"), + (true, false) => Some("PANIC"), + (false, true) => Some("VALIDATION ERROR"), + (false, false) => None, }; - let expect_failure = expected_failure_reason.is_some(); - - if failed == expect_failure { - // We got the conditions we expected - if let Some((expected_reason, _)) = expected_failure_reason { - // Print out reason for the failure + // Compare actual results against expectations. + match (failure_cause, expected_failure_reason) { + // The test passed, as expected. + (None, None) => {} + // The test failed unexpectedly. + (Some(cause), None) => { + panic!("UNEXPECTED TEST FAILURE DUE TO {cause}") + } + // The test passed unexpectedly. + (None, Some(reason)) => { + panic!("UNEXPECTED TEST PASS: {reason:?}"); + } + // The test failed, as expected. + (Some(cause), Some(reason_expected)) => { log::info!( - "GOT EXPECTED TEST FAILURE DUE TO {}: {:?}", - failure_cause, - expected_reason + "EXPECTED FAILURE DUE TO {} (expected because of {:?})", + cause, + reason_expected ); } - } else if let Some((reason, _)) = expected_failure_reason { - // We expected to fail, but things passed - panic!("UNEXPECTED TEST PASS: {reason:?}"); - } else { - panic!("UNEXPECTED TEST FAILURE DUE TO {failure_cause}") } } -fn initialize_adapter() -> (Adapter, SurfaceGuard) { - let backends = wgpu::util::backend_bits_from_env().unwrap_or_else(Backends::all); - let dx12_shader_compiler = wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(); - let gles_minor_version = wgpu::util::gles_minor_version_from_env().unwrap_or_default(); - let instance = Instance::new(wgpu::InstanceDescriptor { - backends, - dx12_shader_compiler, - gles_minor_version, - }); - let surface_guard; +fn initialize_adapter() -> (Adapter, Option) { + let instance = initialize_instance(); + let surface_guard: Option; let compatible_surface; + // Create a canvas iff we need a WebGL2RenderingContext to have a working device. #[cfg(not(all( target_arch = "wasm32", any(target_os = "emscripten", feature = "webgl") )))] { - surface_guard = SurfaceGuard {}; + surface_guard = None; compatible_surface = None; } #[cfg(all( @@ -398,7 +496,7 @@ fn initialize_adapter() -> (Adapter, SurfaceGuard) { .expect("could not create surface from canvas") }; - surface_guard = SurfaceGuard { canvas }; + surface_guard = Some(SurfaceGuard { canvas }); compatible_surface = Some(surface); } @@ -413,12 +511,21 @@ fn initialize_adapter() -> (Adapter, SurfaceGuard) { (adapter, surface_guard) } -struct SurfaceGuard { - #[cfg(all( - target_arch = "wasm32", - any(target_os = "emscripten", feature = "webgl") - ))] - canvas: web_sys::HtmlCanvasElement, +pub fn initialize_instance() -> Instance { + let backends = wgpu::util::backend_bits_from_env().unwrap_or_else(Backends::all); + let dx12_shader_compiler = wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(); + let gles_minor_version = wgpu::util::gles_minor_version_from_env().unwrap_or_default(); + Instance::new(wgpu::InstanceDescriptor { + backends, + dx12_shader_compiler, + gles_minor_version, + }) +} + +// Public because it is used by tests of interacting with canvas +pub struct SurfaceGuard { + #[cfg(target_arch = "wasm32")] + pub canvas: web_sys::HtmlCanvasElement, } impl SurfaceGuard { @@ -452,11 +559,8 @@ impl Drop for SurfaceGuard { } } -#[cfg(all( - target_arch = "wasm32", - any(target_os = "emscripten", feature = "webgl") -))] -fn create_html_canvas() -> web_sys::HtmlCanvasElement { +#[cfg(target_arch = "wasm32")] +pub fn create_html_canvas() -> web_sys::HtmlCanvasElement { use wasm_bindgen::JsCast; web_sys::window() diff --git a/tests/tests/clear_texture.rs b/tests/tests/clear_texture.rs index 7b2024c64c..36f48af359 100644 --- a/tests/tests/clear_texture.rs +++ b/tests/tests/clear_texture.rs @@ -1,5 +1,7 @@ use wasm_bindgen_test::*; -use wgpu_test::{image::ReadbackBuffers, initialize_test, TestParameters, TestingContext}; +use wgpu_test::{ + image::ReadbackBuffers, initialize_test, FailureCase, TestParameters, TestingContext, +}; static TEXTURE_FORMATS_UNCOMPRESSED_GLES_COMPAT: &[wgpu::TextureFormat] = &[ wgpu::TextureFormat::R8Unorm, @@ -328,7 +330,7 @@ fn clear_texture_tests(ctx: &TestingContext, formats: &[wgpu::TextureFormat]) { fn clear_texture_uncompressed_gles_compat() { initialize_test( TestParameters::default() - .webgl2_failure() + .skip(FailureCase::webgl2()) .features(wgpu::Features::CLEAR_TEXTURE), |ctx| { clear_texture_tests(&ctx, TEXTURE_FORMATS_UNCOMPRESSED_GLES_COMPAT); @@ -341,8 +343,8 @@ fn clear_texture_uncompressed_gles_compat() { fn clear_texture_uncompressed() { initialize_test( TestParameters::default() - .webgl2_failure() - .backend_failure(wgpu::Backends::GL) + .skip(FailureCase::webgl2()) + .expect_fail(FailureCase::backend(wgpu::Backends::GL)) .features(wgpu::Features::CLEAR_TEXTURE), |ctx| { clear_texture_tests(&ctx, TEXTURE_FORMATS_UNCOMPRESSED); @@ -355,7 +357,7 @@ fn clear_texture_uncompressed() { fn clear_texture_depth() { initialize_test( TestParameters::default() - .webgl2_failure() + .skip(FailureCase::webgl2()) .downlevel_flags( wgpu::DownlevelFlags::DEPTH_TEXTURE_AND_BUFFER_COPIES | wgpu::DownlevelFlags::COMPUTE_SHADERS, @@ -385,8 +387,10 @@ fn clear_texture_bc() { initialize_test( TestParameters::default() .features(wgpu::Features::CLEAR_TEXTURE | wgpu::Features::TEXTURE_COMPRESSION_BC) - .specific_failure(Some(wgpu::Backends::GL), None, Some("ANGLE"), false) // https://bugs.chromium.org/p/angleproject/issues/detail?id=7056 - .backend_failure(wgpu::Backends::GL), // compressed texture copy to buffer not yet implemented + // https://bugs.chromium.org/p/angleproject/issues/detail?id=7056 + .expect_fail(FailureCase::backend_adapter(wgpu::Backends::GL, "ANGLE")) + // compressed texture copy to buffer not yet implemented + .expect_fail(FailureCase::backend(wgpu::Backends::GL)), |ctx| { clear_texture_tests(&ctx, TEXTURE_FORMATS_BC); }, @@ -402,8 +406,10 @@ fn clear_texture_astc() { max_texture_dimension_2d: wgpu::COPY_BYTES_PER_ROW_ALIGNMENT * 12, ..wgpu::Limits::downlevel_defaults() }) - .specific_failure(Some(wgpu::Backends::GL), None, Some("ANGLE"), false) // https://bugs.chromium.org/p/angleproject/issues/detail?id=7056 - .backend_failure(wgpu::Backends::GL), // compressed texture copy to buffer not yet implemented + // https://bugs.chromium.org/p/angleproject/issues/detail?id=7056 + .expect_fail(FailureCase::backend_adapter(wgpu::Backends::GL, "ANGLE")) + // compressed texture copy to buffer not yet implemented + .expect_fail(FailureCase::backend(wgpu::Backends::GL)), |ctx| { clear_texture_tests(&ctx, TEXTURE_FORMATS_ASTC); }, @@ -415,8 +421,10 @@ fn clear_texture_etc2() { initialize_test( TestParameters::default() .features(wgpu::Features::CLEAR_TEXTURE | wgpu::Features::TEXTURE_COMPRESSION_ETC2) - .specific_failure(Some(wgpu::Backends::GL), None, Some("ANGLE"), false) // https://bugs.chromium.org/p/angleproject/issues/detail?id=7056 - .backend_failure(wgpu::Backends::GL), // compressed texture copy to buffer not yet implemented + // https://bugs.chromium.org/p/angleproject/issues/detail?id=7056 + .expect_fail(FailureCase::backend_adapter(wgpu::Backends::GL, "ANGLE")) + // compressed texture copy to buffer not yet implemented + .expect_fail(FailureCase::backend(wgpu::Backends::GL)), |ctx| { clear_texture_tests(&ctx, TEXTURE_FORMATS_ETC2); }, diff --git a/tests/tests/create_surface_error.rs b/tests/tests/create_surface_error.rs new file mode 100644 index 0000000000..f8962697ce --- /dev/null +++ b/tests/tests/create_surface_error.rs @@ -0,0 +1,28 @@ +//! Test that `create_surface_*()` accurately reports those errors we can provoke. + +/// This test applies to those cfgs that have a `create_surface_from_canvas` method, which +/// include WebGL and WebGPU, but *not* Emscripten GLES. +#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))] +#[wasm_bindgen_test::wasm_bindgen_test] +fn canvas_get_context_returned_null() { + // Not using initialize_test() because that goes straight to creating the canvas for us. + let instance = wgpu_test::initialize_instance(); + // Create canvas and cleanup on drop + let canvas_g = wgpu_test::SurfaceGuard { + canvas: wgpu_test::create_html_canvas(), + }; + // Using a context id that is not "webgl2" or "webgpu" will render the canvas unusable by wgpu. + canvas_g.canvas.get_context("2d").unwrap(); + + #[allow(clippy::redundant_clone)] // false positive — can't and shouldn't move out. + let error = instance + .create_surface_from_canvas(canvas_g.canvas.clone()) + .unwrap_err(); + + assert!( + error + .to_string() + .contains("canvas.getContext() returned null"), + "{error}" + ); +} diff --git a/tests/tests/device.rs b/tests/tests/device.rs index 945d5476d7..f43791f86e 100644 --- a/tests/tests/device.rs +++ b/tests/tests/device.rs @@ -1,6 +1,6 @@ use wasm_bindgen_test::*; -use wgpu_test::{initialize_test, TestParameters}; +use wgpu_test::{initialize_test, FailureCase, TestParameters}; #[test] #[wasm_bindgen_test] @@ -13,26 +13,30 @@ fn device_initialization() { #[test] #[ignore] fn device_mismatch() { - initialize_test(TestParameters::default().failure(), |ctx| { - // Create a bind group uisng a lyaout from another device. This should be a validation - // error but currently crashes. - let (device2, _) = - pollster::block_on(ctx.adapter.request_device(&Default::default(), None)).unwrap(); + initialize_test( + // https://github.com/gfx-rs/wgpu/issues/3927 + TestParameters::default().expect_fail(FailureCase::always()), + |ctx| { + // Create a bind group uisng a lyaout from another device. This should be a validation + // error but currently crashes. + let (device2, _) = + pollster::block_on(ctx.adapter.request_device(&Default::default(), None)).unwrap(); - { - let bind_group_layout = - device2.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + { + let bind_group_layout = + device2.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[], + }); + + let _bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, + layout: &bind_group_layout, entries: &[], }); + } - let _bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: &bind_group_layout, - entries: &[], - }); - } - - ctx.device.poll(wgpu::Maintain::Poll); - }); + ctx.device.poll(wgpu::Maintain::Poll); + }, + ); } diff --git a/tests/tests/encoder.rs b/tests/tests/encoder.rs index 9e541c16a8..5914cd22da 100644 --- a/tests/tests/encoder.rs +++ b/tests/tests/encoder.rs @@ -1,5 +1,6 @@ use wasm_bindgen_test::*; -use wgpu_test::{initialize_test, TestParameters}; +use wgpu::RenderPassDescriptor; +use wgpu_test::{fail, initialize_test, FailureCase, TestParameters}; #[test] #[wasm_bindgen_test] @@ -11,3 +12,60 @@ fn drop_encoder() { drop(encoder); }) } + +#[test] +fn drop_encoder_after_error() { + // This test crashes on DX12 with the exception: + // + // ID3D12CommandAllocator::Reset: The command allocator cannot be reset because a + // command list is currently being recorded with the allocator. [ EXECUTION ERROR + // #543: COMMAND_ALLOCATOR_CANNOT_RESET] + // + // For now, we mark the test as failing on DX12. + let parameters = + TestParameters::default().expect_fail(FailureCase::backend(wgpu::Backends::DX12)); + initialize_test(parameters, |ctx| { + let mut encoder = ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + + let target_tex = ctx.device.create_texture(&wgpu::TextureDescriptor { + label: None, + size: wgpu::Extent3d { + width: 100, + height: 100, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::R8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let target_view = target_tex.create_view(&wgpu::TextureViewDescriptor::default()); + + let mut renderpass = encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("renderpass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + ops: wgpu::Operations::default(), + resolve_target: None, + view: &target_view, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Set a bad viewport on renderpass, triggering an error. + fail(&ctx.device, || { + renderpass.set_viewport(0.0, 0.0, -1.0, -1.0, 0.0, 1.0); + drop(renderpass); + }); + + // This is the actual interesting error condition. We've created + // a CommandEncoder which errored out when processing a command. + // The encoder is still open! + drop(encoder); + }) +} diff --git a/tests/tests/poll.rs b/tests/tests/poll.rs index 7409dad093..e27a47a42c 100644 --- a/tests/tests/poll.rs +++ b/tests/tests/poll.rs @@ -7,7 +7,7 @@ use wgpu::{ }; use wasm_bindgen_test::*; -use wgpu_test::{initialize_test, TestParameters, TestingContext}; +use wgpu_test::{initialize_test, FailureCase, TestParameters, TestingContext}; fn generate_dummy_work(ctx: &TestingContext) -> CommandBuffer { let buffer = ctx.device.create_buffer(&BufferDescriptor { @@ -56,60 +56,75 @@ fn generate_dummy_work(ctx: &TestingContext) -> CommandBuffer { #[test] #[wasm_bindgen_test] fn wait() { - initialize_test(TestParameters::default().skip(), |ctx| { - let cmd_buf = generate_dummy_work(&ctx); - - ctx.queue.submit(Some(cmd_buf)); - ctx.device.poll(Maintain::Wait); - }) + initialize_test( + TestParameters::default().skip(FailureCase::always()), + |ctx| { + let cmd_buf = generate_dummy_work(&ctx); + + ctx.queue.submit(Some(cmd_buf)); + ctx.device.poll(Maintain::Wait); + }, + ) } #[test] #[wasm_bindgen_test] fn double_wait() { - initialize_test(TestParameters::default().skip(), |ctx| { - let cmd_buf = generate_dummy_work(&ctx); - - ctx.queue.submit(Some(cmd_buf)); - ctx.device.poll(Maintain::Wait); - ctx.device.poll(Maintain::Wait); - }) + initialize_test( + TestParameters::default().skip(FailureCase::always()), + |ctx| { + let cmd_buf = generate_dummy_work(&ctx); + + ctx.queue.submit(Some(cmd_buf)); + ctx.device.poll(Maintain::Wait); + ctx.device.poll(Maintain::Wait); + }, + ) } #[test] #[wasm_bindgen_test] fn wait_on_submission() { - initialize_test(TestParameters::default().skip(), |ctx| { - let cmd_buf = generate_dummy_work(&ctx); - - let index = ctx.queue.submit(Some(cmd_buf)); - ctx.device.poll(Maintain::WaitForSubmissionIndex(index)); - }) + initialize_test( + TestParameters::default().skip(FailureCase::always()), + |ctx| { + let cmd_buf = generate_dummy_work(&ctx); + + let index = ctx.queue.submit(Some(cmd_buf)); + ctx.device.poll(Maintain::WaitForSubmissionIndex(index)); + }, + ) } #[test] #[wasm_bindgen_test] fn double_wait_on_submission() { - initialize_test(TestParameters::default().skip(), |ctx| { - let cmd_buf = generate_dummy_work(&ctx); - - let index = ctx.queue.submit(Some(cmd_buf)); - ctx.device - .poll(Maintain::WaitForSubmissionIndex(index.clone())); - ctx.device.poll(Maintain::WaitForSubmissionIndex(index)); - }) + initialize_test( + TestParameters::default().skip(FailureCase::always()), + |ctx| { + let cmd_buf = generate_dummy_work(&ctx); + + let index = ctx.queue.submit(Some(cmd_buf)); + ctx.device + .poll(Maintain::WaitForSubmissionIndex(index.clone())); + ctx.device.poll(Maintain::WaitForSubmissionIndex(index)); + }, + ) } #[test] #[wasm_bindgen_test] fn wait_out_of_order() { - initialize_test(TestParameters::default().skip(), |ctx| { - let cmd_buf1 = generate_dummy_work(&ctx); - let cmd_buf2 = generate_dummy_work(&ctx); - - let index1 = ctx.queue.submit(Some(cmd_buf1)); - let index2 = ctx.queue.submit(Some(cmd_buf2)); - ctx.device.poll(Maintain::WaitForSubmissionIndex(index2)); - ctx.device.poll(Maintain::WaitForSubmissionIndex(index1)); - }) + initialize_test( + TestParameters::default().skip(FailureCase::always()), + |ctx| { + let cmd_buf1 = generate_dummy_work(&ctx); + let cmd_buf2 = generate_dummy_work(&ctx); + + let index1 = ctx.queue.submit(Some(cmd_buf1)); + let index2 = ctx.queue.submit(Some(cmd_buf2)); + ctx.device.poll(Maintain::WaitForSubmissionIndex(index2)); + ctx.device.poll(Maintain::WaitForSubmissionIndex(index1)); + }, + ) } diff --git a/tests/tests/regression/issue_4122.rs b/tests/tests/regression/issue_4122.rs new file mode 100644 index 0000000000..41b9cd4231 --- /dev/null +++ b/tests/tests/regression/issue_4122.rs @@ -0,0 +1,110 @@ +use std::{num::NonZeroU64, ops::Range}; + +use wasm_bindgen_test::wasm_bindgen_test; +use wgpu_test::{initialize_test, TestParameters, TestingContext}; + +fn fill_test(ctx: &TestingContext, range: Range, size: u64) -> bool { + let gpu_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("gpu_buffer"), + size, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC, + mapped_at_creation: false, + }); + + let cpu_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("cpu_buffer"), + size, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + // Initialize the whole buffer with values. + let buffer_contents = vec![0xFF_u8; size as usize]; + ctx.queue.write_buffer(&gpu_buffer, 0, &buffer_contents); + + let mut encoder = ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("encoder"), + }); + + encoder.clear_buffer( + &gpu_buffer, + range.start, + NonZeroU64::new(range.end - range.start), + ); + encoder.copy_buffer_to_buffer(&gpu_buffer, 0, &cpu_buffer, 0, size); + + ctx.queue.submit(Some(encoder.finish())); + cpu_buffer.slice(..).map_async(wgpu::MapMode::Read, |_| ()); + ctx.device.poll(wgpu::Maintain::Wait); + + let buffer_slice = cpu_buffer.slice(..); + let buffer_data = buffer_slice.get_mapped_range(); + + let first_clear_byte = buffer_data + .iter() + .enumerate() + .find_map(|(index, byte)| (*byte == 0x00).then_some(index)) + .expect("No clear happened at all"); + + let first_dirty_byte = buffer_data + .iter() + .enumerate() + .skip(first_clear_byte) + .find_map(|(index, byte)| (*byte != 0x00).then_some(index)) + .unwrap_or(size as usize); + + let second_clear_byte = buffer_data + .iter() + .enumerate() + .skip(first_dirty_byte) + .find_map(|(index, byte)| (*byte == 0x00).then_some(index)); + + if second_clear_byte.is_some() { + eprintln!("Found multiple cleared ranges instead of a single clear range of {}..{} on a buffer of size {}.", range.start, range.end, size); + return false; + } + + let cleared_range = first_clear_byte as u64..first_dirty_byte as u64; + + if cleared_range != range { + eprintln!( + "Cleared range is {}..{}, but the clear range is {}..{} on a buffer of size {}.", + cleared_range.start, cleared_range.end, range.start, range.end, size + ); + return false; + } + + eprintln!( + "Cleared range is {}..{} on a buffer of size {}.", + cleared_range.start, cleared_range.end, size + ); + + true +} + +/// Nvidia has a bug in vkCmdFillBuffer where the clear range is not properly respected under +/// certain conditions. See https://github.com/gfx-rs/wgpu/issues/4122 for more information. +/// +/// This test will fail on nvidia if the bug is not properly worked around. +#[wasm_bindgen_test] +#[test] +fn clear_buffer_bug() { + initialize_test(TestParameters::default(), |ctx| { + // This hits most of the cases in nvidia's clear buffer bug + let mut succeeded = true; + for power in 4..14 { + let size = 1 << power; + for start_offset in (0..=36).step_by(4) { + for size_offset in (0..=36).step_by(4) { + let range = start_offset..size + size_offset + start_offset; + let result = fill_test(&ctx, range, 1 << 16); + + succeeded &= result; + } + } + } + assert!(succeeded); + }); +} diff --git a/tests/tests/root.rs b/tests/tests/root.rs index b376ab4981..85901ae491 100644 --- a/tests/tests/root.rs +++ b/tests/tests/root.rs @@ -3,6 +3,7 @@ use wasm_bindgen_test::wasm_bindgen_test_configure; mod regression { mod issue_3457; mod issue_4024; + mod issue_4122; } mod bind_group_layout_dedup; @@ -10,6 +11,7 @@ mod buffer; mod buffer_copy; mod buffer_usages; mod clear_texture; +mod create_surface_error; mod device; mod encoder; mod example_wgsl; diff --git a/tests/tests/shader/struct_layout.rs b/tests/tests/shader/struct_layout.rs index bc433b5820..7da8cfeef8 100644 --- a/tests/tests/shader/struct_layout.rs +++ b/tests/tests/shader/struct_layout.rs @@ -4,7 +4,7 @@ use wasm_bindgen_test::*; use wgpu::{Backends, DownlevelFlags, Features, Limits}; use crate::shader::{shader_input_output_test, InputStorageType, ShaderTest, MAX_BUFFER_SIZE}; -use wgpu_test::{initialize_test, TestParameters}; +use wgpu_test::{initialize_test, FailureCase, TestParameters}; fn create_struct_layout_tests(storage_type: InputStorageType) -> Vec { let input_values: Vec<_> = (0..(MAX_BUFFER_SIZE as u32 / 4)).collect(); @@ -182,7 +182,7 @@ fn uniform_input() { TestParameters::default() .downlevel_flags(DownlevelFlags::COMPUTE_SHADERS) // Validation errors thrown by the SPIR-V validator https://github.com/gfx-rs/naga/issues/2034 - .specific_failure(Some(wgpu::Backends::VULKAN), None, None, false) + .expect_fail(FailureCase::backend(wgpu::Backends::VULKAN)) .limits(Limits::downlevel_defaults()), |ctx| { shader_input_output_test( @@ -222,7 +222,7 @@ fn push_constant_input() { max_push_constant_size: MAX_BUFFER_SIZE as u32, ..Limits::downlevel_defaults() }) - .backend_failure(Backends::GL), + .expect_fail(FailureCase::backend(Backends::GL)), |ctx| { shader_input_output_test( ctx, diff --git a/tests/tests/shader/zero_init_workgroup_mem.rs b/tests/tests/shader/zero_init_workgroup_mem.rs index a666d2aa28..cbd1b3e561 100644 --- a/tests/tests/shader/zero_init_workgroup_mem.rs +++ b/tests/tests/shader/zero_init_workgroup_mem.rs @@ -8,7 +8,7 @@ use wgpu::{ ShaderStages, }; -use wgpu_test::{initialize_test, TestParameters, TestingContext}; +use wgpu_test::{initialize_test, FailureCase, TestParameters, TestingContext}; #[test] fn zero_init_workgroup_mem() { @@ -18,13 +18,16 @@ fn zero_init_workgroup_mem() { .limits(Limits::downlevel_defaults()) // remove both of these once we get to https://github.com/gfx-rs/wgpu/issues/3193 or // https://github.com/gfx-rs/wgpu/issues/3160 - .specific_failure( - Some(Backends::DX12), - Some(5140), - Some("Microsoft Basic Render Driver"), - true, - ) - .specific_failure(Some(Backends::VULKAN), None, Some("swiftshader"), true), + .skip(FailureCase { + backends: Some(Backends::DX12), + vendor: Some(5140), + adapter: Some("Microsoft Basic Render Driver"), + ..FailureCase::default() + }) + .skip(FailureCase::backend_adapter( + Backends::VULKAN, + "swiftshader", + )), zero_init_workgroup_mem_impl, ); } diff --git a/tests/tests/shader_view_format/mod.rs b/tests/tests/shader_view_format/mod.rs index 1d7dd2630d..46741b4ea8 100644 --- a/tests/tests/shader_view_format/mod.rs +++ b/tests/tests/shader_view_format/mod.rs @@ -1,12 +1,17 @@ use wgpu::{util::DeviceExt, DownlevelFlags, Limits, TextureFormat}; -use wgpu_test::{image::calc_difference, initialize_test, TestParameters, TestingContext}; +use wgpu_test::{ + image::calc_difference, initialize_test, FailureCase, TestParameters, TestingContext, +}; #[test] fn reinterpret_srgb_ness() { let parameters = TestParameters::default() .downlevel_flags(DownlevelFlags::VIEW_FORMATS) .limits(Limits::downlevel_defaults()) - .specific_failure(Some(wgpu::Backends::GL), None, None, true); + .skip(FailureCase { + backends: Some(wgpu::Backends::GL), + ..FailureCase::default() + }); initialize_test(parameters, |ctx| { let unorm_data: [[u8; 4]; 4] = [ [180, 0, 0, 255], diff --git a/tests/tests/vertex_indices/mod.rs b/tests/tests/vertex_indices/mod.rs index 136876017f..edd4f7b057 100644 --- a/tests/tests/vertex_indices/mod.rs +++ b/tests/tests/vertex_indices/mod.rs @@ -3,7 +3,7 @@ use std::num::NonZeroU64; use wasm_bindgen_test::*; use wgpu::util::DeviceExt; -use wgpu_test::{initialize_test, TestParameters, TestingContext}; +use wgpu_test::{initialize_test, FailureCase, TestParameters, TestingContext}; fn pulling_common( ctx: TestingContext, @@ -150,7 +150,7 @@ fn draw_vertex_offset() { initialize_test( TestParameters::default() .test_features_limits() - .backend_failure(wgpu::Backends::DX11), + .expect_fail(FailureCase::backend(wgpu::Backends::DX11)), |ctx| { pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { cmb.draw(0..3, 0..1); @@ -176,7 +176,7 @@ fn draw_instanced_offset() { initialize_test( TestParameters::default() .test_features_limits() - .backend_failure(wgpu::Backends::DX11), + .expect_fail(FailureCase::backend(wgpu::Backends::DX11)), |ctx| { pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { cmb.draw(0..3, 0..1); diff --git a/tests/tests/write_texture.rs b/tests/tests/write_texture.rs index 0578c60352..8b33cae7f5 100644 --- a/tests/tests/write_texture.rs +++ b/tests/tests/write_texture.rs @@ -1,6 +1,6 @@ //! Tests for texture copy -use wgpu_test::{initialize_test, TestParameters}; +use wgpu_test::{initialize_test, FailureCase, TestParameters}; use wasm_bindgen_test::*; @@ -8,7 +8,8 @@ use wasm_bindgen_test::*; #[wasm_bindgen_test] fn write_texture_subset_2d() { let size = 256; - let parameters = TestParameters::default().backend_failure(wgpu::Backends::DX12); + let parameters = + TestParameters::default().expect_fail(FailureCase::backend(wgpu::Backends::DX12)); initialize_test(parameters, |ctx| { let tex = ctx.device.create_texture(&wgpu::TextureDescriptor { label: None, diff --git a/tests/tests/zero_init_texture_after_discard.rs b/tests/tests/zero_init_texture_after_discard.rs index 4d508f8280..2b757e069a 100644 --- a/tests/tests/zero_init_texture_after_discard.rs +++ b/tests/tests/zero_init_texture_after_discard.rs @@ -1,38 +1,46 @@ use wasm_bindgen_test::*; use wgpu::*; -use wgpu_test::{image::ReadbackBuffers, initialize_test, TestParameters, TestingContext}; +use wgpu_test::{ + image::ReadbackBuffers, initialize_test, FailureCase, TestParameters, TestingContext, +}; // Checks if discarding a color target resets its init state, causing a zero read of this texture when copied in after submit of the encoder. #[test] #[wasm_bindgen_test] fn discarding_color_target_resets_texture_init_state_check_visible_on_copy_after_submit() { - initialize_test(TestParameters::default().webgl2_failure(), |mut ctx| { - let mut case = TestCase::new(&mut ctx, TextureFormat::Rgba8UnormSrgb); - case.create_command_encoder(); - case.discard(); - case.submit_command_encoder(); + initialize_test( + TestParameters::default().skip(FailureCase::webgl2()), + |mut ctx| { + let mut case = TestCase::new(&mut ctx, TextureFormat::Rgba8UnormSrgb); + case.create_command_encoder(); + case.discard(); + case.submit_command_encoder(); - case.create_command_encoder(); - case.copy_texture_to_buffer(); - case.submit_command_encoder(); + case.create_command_encoder(); + case.copy_texture_to_buffer(); + case.submit_command_encoder(); - case.assert_buffers_are_zero(); - }); + case.assert_buffers_are_zero(); + }, + ); } // Checks if discarding a color target resets its init state, causing a zero read of this texture when copied in the same encoder to a buffer. #[test] #[wasm_bindgen_test] fn discarding_color_target_resets_texture_init_state_check_visible_on_copy_in_same_encoder() { - initialize_test(TestParameters::default().webgl2_failure(), |mut ctx| { - let mut case = TestCase::new(&mut ctx, TextureFormat::Rgba8UnormSrgb); - case.create_command_encoder(); - case.discard(); - case.copy_texture_to_buffer(); - case.submit_command_encoder(); + initialize_test( + TestParameters::default().skip(FailureCase::webgl2()), + |mut ctx| { + let mut case = TestCase::new(&mut ctx, TextureFormat::Rgba8UnormSrgb); + case.create_command_encoder(); + case.discard(); + case.copy_texture_to_buffer(); + case.submit_command_encoder(); - case.assert_buffers_are_zero(); - }); + case.assert_buffers_are_zero(); + }, + ); } #[test] diff --git a/wgpu-core/Cargo.toml b/wgpu-core/Cargo.toml index 5cebd9fdca..5487a8bdc0 100644 --- a/wgpu-core/Cargo.toml +++ b/wgpu-core/Cargo.toml @@ -72,7 +72,7 @@ thiserror = "1" [dependencies.naga] git = "https://github.com/gfx-rs/naga" -rev = "7a19f3af909202c7eafd36633b5584bfbb353ecb" +rev = "cc87b8f9eb30bb55d0735b89d3df3e099e1a6e7c" version = "0.13.0" features = ["clone", "span", "validate"] diff --git a/wgpu-core/src/device/global.rs b/wgpu-core/src/device/global.rs index 8fe5a6fcc9..632c83e37f 100644 --- a/wgpu-core/src/device/global.rs +++ b/wgpu-core/src/device/global.rs @@ -2134,7 +2134,7 @@ impl Global { let (mut surface_guard, mut token) = self.surfaces.write(&mut token); let (adapter_guard, mut token) = hub.adapters.read(&mut token); - let (device_guard, _token) = hub.devices.read(&mut token); + let (device_guard, mut token) = hub.devices.read(&mut token); let error = 'outer: loop { let device = match device_guard.get(device_id) { @@ -2207,6 +2207,24 @@ impl Global { break error; } + // Wait for all work to finish before configuring the surface. + if let Err(e) = device.maintain(hub, wgt::Maintain::Wait, &mut token) { + break e.into(); + } + + // All textures must be destroyed before the surface can be re-configured. + if let Some(present) = surface.presentation.take() { + if present.acquired_texture.is_some() { + break E::PreviousOutputExists; + } + } + + // TODO: Texture views may still be alive that point to the texture. + // this will allow the user to render to the surface texture, long after + // it has been removed. + // + // https://github.com/gfx-rs/wgpu/issues/4105 + match unsafe { A::get_surface_mut(surface) .unwrap() @@ -2226,12 +2244,6 @@ impl Global { } } - if let Some(present) = surface.presentation.take() { - if present.acquired_texture.is_some() { - break E::PreviousOutputExists; - } - } - surface.presentation = Some(present::Presentation { device_id: Stored { value: id::Valid(device_id), diff --git a/wgpu-core/src/device/mod.rs b/wgpu-core/src/device/mod.rs index 0ae6d7a2dd..9a77bf9536 100644 --- a/wgpu-core/src/device/mod.rs +++ b/wgpu-core/src/device/mod.rs @@ -1,6 +1,5 @@ use crate::{ binding_model, - device::life::WaitIdleError, hal_api::HalApi, hub::Hub, id, @@ -24,7 +23,7 @@ pub mod queue; pub mod resource; #[cfg(any(feature = "trace", feature = "replay"))] pub mod trace; -pub use resource::Device; +pub use {life::WaitIdleError, resource::Device}; pub const SHADER_STAGE_COUNT: usize = 3; // Should be large enough for the largest possible texture row. This diff --git a/wgpu-core/src/instance.rs b/wgpu-core/src/instance.rs index ae1a395d85..0aee56ac6e 100644 --- a/wgpu-core/src/instance.rs +++ b/wgpu-core/src/instance.rs @@ -84,8 +84,22 @@ impl Instance { dx12_shader_compiler: instance_desc.dx12_shader_compiler.clone(), gles_minor_version: instance_desc.gles_minor_version, }; - unsafe { hal::Instance::init(&hal_desc).ok() } + match unsafe { hal::Instance::init(&hal_desc) } { + Ok(instance) => { + log::debug!("Instance::new: created {:?} backend", A::VARIANT); + Some(instance) + } + Err(err) => { + log::debug!( + "Instance::new: failed to create {:?} backend: {:?}", + A::VARIANT, + err + ); + None + } + } } else { + log::trace!("Instance::new: backend {:?} not requested", A::VARIANT); None } } diff --git a/wgpu-core/src/present.rs b/wgpu-core/src/present.rs index c9df46ad93..7366934d27 100644 --- a/wgpu-core/src/present.rs +++ b/wgpu-core/src/present.rs @@ -15,7 +15,7 @@ use std::borrow::Borrow; use crate::device::trace::Action; use crate::{ conv, - device::{DeviceError, MissingDownlevelFlags}, + device::{DeviceError, MissingDownlevelFlags, WaitIdleError}, global::Global, hal_api::HalApi, hub::Token, @@ -96,6 +96,18 @@ pub enum ConfigureSurfaceError { }, #[error("Requested usage is not supported")] UnsupportedUsage, + #[error("Gpu got stuck :(")] + StuckGpu, +} + +impl From for ConfigureSurfaceError { + fn from(e: WaitIdleError) -> Self { + match e { + WaitIdleError::Device(d) => ConfigureSurfaceError::Device(d), + WaitIdleError::WrongSubmissionIndex(..) => unreachable!(), + WaitIdleError::StuckGpu => ConfigureSurfaceError::StuckGpu, + } + } } #[repr(C)] @@ -300,15 +312,7 @@ impl Global { let (texture, _) = hub.textures.unregister(texture_id.value.0, &mut token); if let Some(texture) = texture { - if let resource::TextureClearMode::RenderPass { clear_views, .. } = - texture.clear_mode - { - for clear_view in clear_views { - unsafe { - hal::Device::destroy_texture_view(&device.raw, clear_view); - } - } - } + texture.clear_mode.destroy_clear_views(&device.raw); let suf = A::get_surface_mut(surface); match texture.inner { @@ -386,10 +390,16 @@ impl Global { // The texture ID got added to the device tracker by `submit()`, // and now we are moving it away. + log::debug!( + "Removing swapchain texture {:?} from the device tracker", + texture_id.value + ); device.trackers.lock().textures.remove(texture_id.value); let (texture, _) = hub.textures.unregister(texture_id.value.0, &mut token); if let Some(texture) = texture { + texture.clear_mode.destroy_clear_views(&device.raw); + let suf = A::get_surface_mut(surface); match texture.inner { resource::TextureInner::Surface { diff --git a/wgpu-core/src/resource.rs b/wgpu-core/src/resource.rs index fe881c2d06..c0977b80ef 100644 --- a/wgpu-core/src/resource.rs +++ b/wgpu-core/src/resource.rs @@ -384,6 +384,18 @@ pub enum TextureClearMode { None, } +impl TextureClearMode { + pub(crate) fn destroy_clear_views(self, device: &A::Device) { + if let TextureClearMode::RenderPass { clear_views, .. } = self { + for clear_view in clear_views { + unsafe { + hal::Device::destroy_texture_view(device, clear_view); + } + } + } + } +} + #[derive(Debug)] pub struct Texture { pub(crate) inner: TextureInner, diff --git a/wgpu-core/src/validation.rs b/wgpu-core/src/validation.rs index 84e1e71691..e3ecb916d3 100644 --- a/wgpu-core/src/validation.rs +++ b/wgpu-core/src/validation.rs @@ -812,6 +812,7 @@ impl Interface { location, interpolation, sampling, + .. // second_blend_source }) => Varying::Local { location, iv: InterfaceVar { diff --git a/wgpu-hal/Cargo.toml b/wgpu-hal/Cargo.toml index 51b5e2a9ac..d382ca09e8 100644 --- a/wgpu-hal/Cargo.toml +++ b/wgpu-hal/Cargo.toml @@ -120,14 +120,14 @@ android_system_properties = "0.1.1" [dependencies.naga] git = "https://github.com/gfx-rs/naga" -rev = "7a19f3af909202c7eafd36633b5584bfbb353ecb" +rev = "cc87b8f9eb30bb55d0735b89d3df3e099e1a6e7c" version = "0.13.0" features = ["clone"] # DEV dependencies [dev-dependencies.naga] git = "https://github.com/gfx-rs/naga" -rev = "7a19f3af909202c7eafd36633b5584bfbb353ecb" +rev = "cc87b8f9eb30bb55d0735b89d3df3e099e1a6e7c" version = "0.13.0" features = ["wgsl-in"] diff --git a/wgpu-hal/examples/halmark/main.rs b/wgpu-hal/examples/halmark/main.rs index 67aefc5ade..2b0081d20f 100644 --- a/wgpu-hal/examples/halmark/main.rs +++ b/wgpu-hal/examples/halmark/main.rs @@ -86,7 +86,7 @@ struct Example { } impl Example { - fn init(window: &winit::window::Window) -> Result { + fn init(window: &winit::window::Window) -> Result> { let instance_desc = hal::InstanceDescriptor { name: "example", flags: if cfg!(debug_assertions) { @@ -108,13 +108,13 @@ impl Example { let (adapter, capabilities) = unsafe { let mut adapters = instance.enumerate_adapters(); if adapters.is_empty() { - return Err(hal::InstanceError); + return Err("no adapters found".into()); } let exposed = adapters.swap_remove(0); (exposed.adapter, exposed.capabilities) }; - let surface_caps = - unsafe { adapter.surface_capabilities(&surface) }.ok_or(hal::InstanceError)?; + let surface_caps = unsafe { adapter.surface_capabilities(&surface) } + .ok_or("failed to get surface capabilities")?; log::info!("Surface caps: {:#?}", surface_caps); let hal::OpenDevice { device, mut queue } = unsafe { diff --git a/wgpu-hal/src/auxil/dxgi/factory.rs b/wgpu-hal/src/auxil/dxgi/factory.rs index 123ca4933e..7ae6e745f0 100644 --- a/wgpu-hal/src/auxil/dxgi/factory.rs +++ b/wgpu-hal/src/auxil/dxgi/factory.rs @@ -96,7 +96,9 @@ pub fn create_factory( required_factory_type: DxgiFactoryType, instance_flags: crate::InstanceFlags, ) -> Result<(d3d12::DxgiLib, d3d12::DxgiFactory), crate::InstanceError> { - let lib_dxgi = d3d12::DxgiLib::new().map_err(|_| crate::InstanceError)?; + let lib_dxgi = d3d12::DxgiLib::new().map_err(|e| { + crate::InstanceError::with_source(String::from("failed to load dxgi.dll"), e) + })?; let mut factory_flags = d3d12::FactoryCreationFlags::empty(); @@ -128,18 +130,22 @@ pub fn create_factory( Ok(factory) => Some(factory), // We hard error here as we _should have_ been able to make a factory4 but couldn't. Err(err) => { - log::error!("Failed to create IDXGIFactory4: {}", err); - return Err(crate::InstanceError); + // err is a Cow, not an Error implementor + return Err(crate::InstanceError::new(format!( + "failed to create IDXGIFactory4: {err:?}" + ))); } }, // If we require factory4, hard error. Err(err) if required_factory_type == DxgiFactoryType::Factory4 => { - log::error!("IDXGIFactory1 creation function not found: {:?}", err); - return Err(crate::InstanceError); + return Err(crate::InstanceError::with_source( + String::from("IDXGIFactory1 creation function not found"), + err, + )); } // If we don't print it to info as all win7 will hit this case. Err(err) => { - log::info!("IDXGIFactory1 creation function not found: {:?}", err); + log::info!("IDXGIFactory1 creation function not found: {err:?}"); None } }; @@ -153,8 +159,10 @@ pub fn create_factory( } // If we require factory6, hard error. Err(err) if required_factory_type == DxgiFactoryType::Factory6 => { - log::warn!("Failed to cast IDXGIFactory4 to IDXGIFactory6: {:?}", err); - return Err(crate::InstanceError); + // err is a Cow, not an Error implementor + return Err(crate::InstanceError::new(format!( + "failed to cast IDXGIFactory4 to IDXGIFactory6: {err:?}" + ))); } // If we don't print it to info. Err(err) => { @@ -169,14 +177,18 @@ pub fn create_factory( Ok(pair) => match pair.into_result() { Ok(factory) => factory, Err(err) => { - log::error!("Failed to create IDXGIFactory1: {}", err); - return Err(crate::InstanceError); + // err is a Cow, not an Error implementor + return Err(crate::InstanceError::new(format!( + "failed to create IDXGIFactory1: {err:?}" + ))); } }, // We always require at least factory1, so hard error Err(err) => { - log::error!("IDXGIFactory1 creation function not found: {:?}", err); - return Err(crate::InstanceError); + return Err(crate::InstanceError::with_source( + String::from("IDXGIFactory1 creation function not found"), + err, + )); } }; @@ -188,8 +200,10 @@ pub fn create_factory( } // If we require factory2, hard error. Err(err) if required_factory_type == DxgiFactoryType::Factory2 => { - log::warn!("Failed to cast IDXGIFactory1 to IDXGIFactory2: {:?}", err); - return Err(crate::InstanceError); + // err is a Cow, not an Error implementor + return Err(crate::InstanceError::new(format!( + "failed to cast IDXGIFactory1 to IDXGIFactory2: {err:?}" + ))); } // If we don't print it to info. Err(err) => { diff --git a/wgpu-hal/src/dx11/instance.rs b/wgpu-hal/src/dx11/instance.rs index 1d8c2b51a2..e7a4e2e705 100644 --- a/wgpu-hal/src/dx11/instance.rs +++ b/wgpu-hal/src/dx11/instance.rs @@ -8,10 +8,13 @@ impl crate::Instance for super::Instance { }; if !enable_dx11 { - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(String::from( + "DX11 support is unstable; set WGPU_UNSTABLE_DX11_BACKEND=1 to enable anyway", + ))); } - let lib_d3d11 = super::library::D3D11Lib::new().ok_or(crate::InstanceError)?; + let lib_d3d11 = super::library::D3D11Lib::new() + .ok_or_else(|| crate::InstanceError::new(String::from("failed to load d3d11.dll")))?; let (lib_dxgi, factory) = auxil::dxgi::factory::create_factory( auxil::dxgi::factory::DxgiFactoryType::Factory1, diff --git a/wgpu-hal/src/dx12/device.rs b/wgpu-hal/src/dx12/device.rs index 467fb5586e..e776c35ab2 100644 --- a/wgpu-hal/src/dx12/device.rs +++ b/wgpu-hal/src/dx12/device.rs @@ -181,7 +181,10 @@ impl super::Device { }) } - pub(super) unsafe fn wait_idle(&self) -> Result<(), crate::DeviceError> { + // Blocks until the dedicated present queue is finished with all of its work. + // + // Once this method completes, the surface is able to be resized or deleted. + pub(super) unsafe fn wait_for_present_queue_idle(&self) -> Result<(), crate::DeviceError> { let cur_value = self.idler.fence.get_value(); if cur_value == !0 { return Err(crate::DeviceError::Lost); diff --git a/wgpu-hal/src/dx12/instance.rs b/wgpu-hal/src/dx12/instance.rs index 208d2179f7..32d6f1690c 100644 --- a/wgpu-hal/src/dx12/instance.rs +++ b/wgpu-hal/src/dx12/instance.rs @@ -12,7 +12,9 @@ impl Drop for super::Instance { impl crate::Instance for super::Instance { unsafe fn init(desc: &crate::InstanceDescriptor) -> Result { - let lib_main = d3d12::D3D12Lib::new().map_err(|_| crate::InstanceError)?; + let lib_main = d3d12::D3D12Lib::new().map_err(|e| { + crate::InstanceError::with_source(String::from("failed to load d3d12.dll"), e) + })?; if desc.flags.contains(crate::InstanceFlags::VALIDATION) { // Enable debug layer @@ -95,7 +97,9 @@ impl crate::Instance for super::Instance { supports_allow_tearing: self.supports_allow_tearing, swap_chain: None, }), - _ => Err(crate::InstanceError), + _ => Err(crate::InstanceError::new(format!( + "window handle {window_handle:?} is not a Win32 handle" + ))), } } unsafe fn destroy_surface(&self, _surface: super::Surface) { diff --git a/wgpu-hal/src/dx12/mod.rs b/wgpu-hal/src/dx12/mod.rs index 2178998d47..3e9d3bff15 100644 --- a/wgpu-hal/src/dx12/mod.rs +++ b/wgpu-hal/src/dx12/mod.rs @@ -618,19 +618,23 @@ impl crate::Surface for Surface { let mut flags = dxgi::DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; // We always set ALLOW_TEARING on the swapchain no matter // what kind of swapchain we want because ResizeBuffers - // cannot change if ALLOW_TEARING is applied to the swapchain. + // cannot change the swapchain's ALLOW_TEARING flag. + // + // This does not change the behavior of the swapchain, just + // allow present calls to use tearing. if self.supports_allow_tearing { flags |= dxgi::DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; } + // While `configure`s contract ensures that no work on the GPU's main queues + // are in flight, we still need to wait for the present queue to be idle. + unsafe { device.wait_for_present_queue_idle() }?; + let non_srgb_format = auxil::dxgi::conv::map_texture_format_nosrgb(config.format); let swap_chain = match self.swap_chain.take() { //Note: this path doesn't properly re-initialize all of the things Some(sc) => { - // can't have image resources in flight used by GPU - let _ = unsafe { device.wait_idle() }; - let raw = unsafe { sc.release_resources() }; let result = unsafe { raw.ResizeBuffers( @@ -778,12 +782,16 @@ impl crate::Surface for Surface { } unsafe fn unconfigure(&mut self, device: &Device) { - if let Some(mut sc) = self.swap_chain.take() { + if let Some(sc) = self.swap_chain.take() { unsafe { - let _ = sc.wait(None); - //TODO: this shouldn't be needed, - // but it complains that the queue is still used otherwise - let _ = device.wait_idle(); + // While `unconfigure`s contract ensures that no work on the GPU's main queues + // are in flight, we still need to wait for the present queue to be idle. + + // The major failure mode of this function is device loss, + // which if we have lost the device, we should just continue + // cleaning up, without error. + let _ = device.wait_for_present_queue_idle(); + let _raw = sc.release_resources(); } } @@ -842,6 +850,13 @@ impl crate::Queue for Queue { .signal(&fence.raw, value) .into_device_result("Signal fence")?; } + + // Note the lack of synchronization here between the main Direct queue + // and the dedicated presentation queue. This is automatically handled + // by the D3D runtime by detecting uses of resources derived from the + // swapchain. This automatic detection is why you cannot use a swapchain + // as an UAV in D3D12. + Ok(()) } unsafe fn present( diff --git a/wgpu-hal/src/gles/adapter.rs b/wgpu-hal/src/gles/adapter.rs index 2c68961e39..348f62bc03 100644 --- a/wgpu-hal/src/gles/adapter.rs +++ b/wgpu-hal/src/gles/adapter.rs @@ -43,8 +43,9 @@ impl super::Adapter { src = &src[pos + es_sig.len()..]; } None => { - log::warn!("ES not found in '{}'", src); - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(format!( + "OpenGL version {src:?} does not contain 'ES'" + ))); } } }; @@ -86,10 +87,9 @@ impl super::Adapter { }, minor, )), - _ => { - log::warn!("Unable to extract the version from '{}'", version); - Err(crate::InstanceError) - } + _ => Err(crate::InstanceError::new(format!( + "unable to extract OpenGL version from {version:?}" + ))), } } @@ -975,27 +975,30 @@ mod tests { #[test] fn test_version_parse() { - let error = Err(crate::InstanceError); - assert_eq!(Adapter::parse_version("1"), error); - assert_eq!(Adapter::parse_version("1."), error); - assert_eq!(Adapter::parse_version("1 h3l1o. W0rld"), error); - assert_eq!(Adapter::parse_version("1. h3l1o. W0rld"), error); - assert_eq!(Adapter::parse_version("1.2.3"), error); - assert_eq!(Adapter::parse_version("OpenGL ES 3.1"), Ok((3, 1))); + Adapter::parse_version("1").unwrap_err(); + Adapter::parse_version("1.").unwrap_err(); + Adapter::parse_version("1 h3l1o. W0rld").unwrap_err(); + Adapter::parse_version("1. h3l1o. W0rld").unwrap_err(); + Adapter::parse_version("1.2.3").unwrap_err(); + + assert_eq!(Adapter::parse_version("OpenGL ES 3.1").unwrap(), (3, 1)); + assert_eq!( + Adapter::parse_version("OpenGL ES 2.0 Google Nexus").unwrap(), + (2, 0) + ); + assert_eq!(Adapter::parse_version("GLSL ES 1.1").unwrap(), (1, 1)); assert_eq!( - Adapter::parse_version("OpenGL ES 2.0 Google Nexus"), - Ok((2, 0)) + Adapter::parse_version("OpenGL ES GLSL ES 3.20").unwrap(), + (3, 2) ); - assert_eq!(Adapter::parse_version("GLSL ES 1.1"), Ok((1, 1))); - assert_eq!(Adapter::parse_version("OpenGL ES GLSL ES 3.20"), Ok((3, 2))); assert_eq!( // WebGL 2.0 should parse as OpenGL ES 3.0 - Adapter::parse_version("WebGL 2.0 (OpenGL ES 3.0 Chromium)"), - Ok((3, 0)) + Adapter::parse_version("WebGL 2.0 (OpenGL ES 3.0 Chromium)").unwrap(), + (3, 0) ); assert_eq!( - Adapter::parse_version("WebGL GLSL ES 3.00 (OpenGL ES GLSL ES 3.0 Chromium)"), - Ok((3, 0)) + Adapter::parse_version("WebGL GLSL ES 3.00 (OpenGL ES GLSL ES 3.0 Chromium)").unwrap(), + (3, 0) ); } } diff --git a/wgpu-hal/src/gles/egl.rs b/wgpu-hal/src/gles/egl.rs index b904dffee9..d6d3d621f9 100644 --- a/wgpu-hal/src/gles/egl.rs +++ b/wgpu-hal/src/gles/egl.rs @@ -283,7 +283,10 @@ fn choose_config( } } - Err(crate::InstanceError) + // TODO: include diagnostic details that are currently logged + Err(crate::InstanceError::new(String::from( + "unable to find an acceptable EGL framebuffer configuration", + ))) } fn gl_debug_message_callback(source: u32, gltype: u32, id: u32, severity: u32, message: &str) { @@ -495,7 +498,12 @@ impl Inner { display: khronos_egl::Display, force_gles_minor_version: wgt::Gles3MinorVersion, ) -> Result { - let version = egl.initialize(display).map_err(|_| crate::InstanceError)?; + let version = egl.initialize(display).map_err(|e| { + crate::InstanceError::with_source( + String::from("failed to initialize EGL display connection"), + e, + ) + })?; let vendor = egl .query_string(Some(display), khronos_egl::VENDOR) .unwrap(); @@ -599,8 +607,10 @@ impl Inner { let context = match egl.create_context(display, config, None, &context_attributes) { Ok(context) => context, Err(e) => { - log::warn!("unable to create GLES 3.x context: {:?}", e); - return Err(crate::InstanceError); + return Err(crate::InstanceError::with_source( + String::from("unable to create GLES 3.x context"), + e, + )); } }; @@ -623,8 +633,10 @@ impl Inner { egl.create_pbuffer_surface(display, config, &attributes) .map(Some) .map_err(|e| { - log::warn!("Error in create_pbuffer_surface: {:?}", e); - crate::InstanceError + crate::InstanceError::with_source( + String::from("error in create_pbuffer_surface"), + e, + ) })? }; @@ -734,8 +746,10 @@ impl crate::Instance for Instance { let egl = match egl_result { Ok(egl) => Arc::new(egl), Err(e) => { - log::info!("Unable to open libEGL: {:?}", e); - return Err(crate::InstanceError); + return Err(crate::InstanceError::with_source( + String::from("unable to open libEGL"), + e, + )); } }; @@ -899,8 +913,9 @@ impl crate::Instance for Instance { }; if ret != 0 { - log::error!("Error returned from ANativeWindow_setBuffersGeometry"); - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(format!( + "error {ret} returned from ANativeWindow_setBuffersGeometry", + ))); } } #[cfg(not(target_os = "emscripten"))] @@ -938,8 +953,7 @@ impl crate::Instance for Instance { Arc::clone(&inner.egl.instance), display, inner.force_gles_minor_version, - ) - .map_err(|_| crate::InstanceError)?; + )?; let old_inner = std::mem::replace(inner.deref_mut(), new_inner); inner.wl_display = Some(display_handle.display); @@ -950,8 +964,9 @@ impl crate::Instance for Instance { #[cfg(target_os = "emscripten")] (Rwh::Web(_), _) => {} other => { - log::error!("Unsupported window: {:?}", other); - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(format!( + "unsupported window: {other:?}" + ))); } }; diff --git a/wgpu-hal/src/gles/web.rs b/wgpu-hal/src/gles/web.rs index 02cd6a3ecb..13bce85f84 100644 --- a/wgpu-hal/src/gles/web.rs +++ b/wgpu-hal/src/gles/web.rs @@ -66,14 +66,16 @@ impl Instance { // “not supported” could include “insufficient GPU resources” or “the GPU process // previously crashed”. So, we must return it as an `Err` since it could occur // for circumstances outside the application author's control. - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(String::from( + "canvas.getContext() returned null; webgl2 not available or canvas already in use" + ))); } Err(js_error) => { // - // A thrown exception indicates misuse of the canvas state. Ideally we wouldn't - // panic in this case, but for now, `InstanceError` conveys no detail, so it - // is more informative to panic with a specific message. - panic!("canvas.getContext() threw {js_error:?}") + // A thrown exception indicates misuse of the canvas state. + return Err(crate::InstanceError::new(format!( + "canvas.getContext() threw exception {js_error:?}", + ))); } }; @@ -156,7 +158,9 @@ impl crate::Instance for Instance { self.create_surface_from_canvas(canvas) } else { - Err(crate::InstanceError) + Err(crate::InstanceError::new(format!( + "window handle {window_handle:?} is not a web handle" + ))) } } diff --git a/wgpu-hal/src/lib.rs b/wgpu-hal/src/lib.rs index fdf91c56bd..4bc7f5c6fb 100644 --- a/wgpu-hal/src/lib.rs +++ b/wgpu-hal/src/lib.rs @@ -90,7 +90,7 @@ use std::{ num::NonZeroU32, ops::{Range, RangeInclusive}, ptr::NonNull, - sync::atomic::AtomicBool, + sync::{atomic::AtomicBool, Arc}, }; use bitflags::bitflags; @@ -152,9 +152,42 @@ pub enum SurfaceError { Other(&'static str), } -#[derive(Clone, Debug, Eq, PartialEq, Error)] -#[error("Not supported")] -pub struct InstanceError; +/// Error occurring while trying to create an instance, or create a surface from an instance; +/// typically relating to the state of the underlying graphics API or hardware. +#[derive(Clone, Debug, Error)] +#[error("{message}")] +pub struct InstanceError { + /// These errors are very platform specific, so do not attempt to encode them as an enum. + /// + /// This message should describe the problem in sufficient detail to be useful for a + /// user-to-developer “why won't this work on my machine” bug report, and otherwise follow + /// . + message: String, + + /// Underlying error value, if any is available. + #[source] + source: Option>, +} + +impl InstanceError { + #[allow(dead_code)] // may be unused on some platforms + pub(crate) fn new(message: String) -> Self { + Self { + message, + source: None, + } + } + #[allow(dead_code)] // may be unused on some platforms + pub(crate) fn with_source( + message: String, + source: impl std::error::Error + Send + Sync + 'static, + ) -> Self { + Self { + message, + source: Some(Arc::new(source)), + } + } +} pub trait Api: Clone + Sized { type Instance: Instance; @@ -196,12 +229,28 @@ pub trait Instance: Sized + WasmNotSend + WasmNotSync { } pub trait Surface: WasmNotSend + WasmNotSync { + /// Configures the surface to use the given device. + /// + /// # Safety + /// + /// - All gpu work that uses the surface must have been completed. + /// - All [`AcquiredSurfaceTexture`]s must have been destroyed. + /// - All [`Api::TextureView`]s derived from the [`AcquiredSurfaceTexture`]s must have been destroyed. + /// - All surfaces created using other devices must have been unconfigured before this call. unsafe fn configure( &mut self, device: &A::Device, config: &SurfaceConfiguration, ) -> Result<(), SurfaceError>; + /// Unconfigures the surface on the given device. + /// + /// # Safety + /// + /// - All gpu work that uses the surface must have been completed. + /// - All [`AcquiredSurfaceTexture`]s must have been destroyed. + /// - All [`Api::TextureView`]s derived from the [`AcquiredSurfaceTexture`]s must have been destroyed. + /// - The surface must have been configured on the given device. unsafe fn unconfigure(&mut self, device: &A::Device); /// Returns the next texture to be presented by the swapchain for drawing diff --git a/wgpu-hal/src/metal/command.rs b/wgpu-hal/src/metal/command.rs index 153373b238..35edf746c5 100644 --- a/wgpu-hal/src/metal/command.rs +++ b/wgpu-hal/src/metal/command.rs @@ -1,4 +1,5 @@ use super::{conv, AsNative}; +use crate::CommandEncoder as _; use std::{borrow::Cow, mem, ops::Range}; // has to match `Temp::binding_sizes` @@ -1072,3 +1073,20 @@ impl crate::CommandEncoder for super::CommandEncoder { unimplemented!() } } + +impl Drop for super::CommandEncoder { + fn drop(&mut self) { + // Metal raises an assert when a MTLCommandEncoder is deallocated without a call + // to endEncoding. This isn't documented in the general case at + // https://developer.apple.com/documentation/metal/mtlcommandencoder, but for the + // more-specific MTLComputeCommandEncoder it is stated as a requirement at + // https://developer.apple.com/documentation/metal/mtlcomputecommandencoder. It + // appears to be a requirement for all MTLCommandEncoder objects. Failing to call + // endEncoding causes a crash with the message 'Command encoder released without + // endEncoding'. To prevent this, we explicitiy call discard_encoding, which + // calls end_encoding on any still-held metal::CommandEncoders. + unsafe { + self.discard_encoding(); + } + } +} diff --git a/wgpu-hal/src/metal/mod.rs b/wgpu-hal/src/metal/mod.rs index 040163935e..078c776153 100644 --- a/wgpu-hal/src/metal/mod.rs +++ b/wgpu-hal/src/metal/mod.rs @@ -102,7 +102,9 @@ impl crate::Instance for Instance { raw_window_handle::RawWindowHandle::AppKit(handle) => Ok(unsafe { Surface::from_view(handle.ns_view, Some(&self.managed_metal_layer_delegate)) }), - _ => Err(crate::InstanceError), + _ => Err(crate::InstanceError::new(format!( + "window handle {window_handle:?} is not a Metal-compatible handle" + ))), } } diff --git a/wgpu-hal/src/vulkan/adapter.rs b/wgpu-hal/src/vulkan/adapter.rs index a8cc02cfb2..a5862e9662 100644 --- a/wgpu-hal/src/vulkan/adapter.rs +++ b/wgpu-hal/src/vulkan/adapter.rs @@ -1068,6 +1068,10 @@ impl super::Instance { super::Workarounds::EMPTY_RESOLVE_ATTACHMENT_LISTS, phd_capabilities.properties.vendor_id == db::qualcomm::VENDOR, ); + workarounds.set( + super::Workarounds::FORCE_FILL_BUFFER_WITH_SIZE_GREATER_4096_ALIGNED_OFFSET_16, + phd_capabilities.properties.vendor_id == db::nvidia::VENDOR, + ); }; if phd_capabilities.effective_api_version == vk::API_VERSION_1_0 diff --git a/wgpu-hal/src/vulkan/command.rs b/wgpu-hal/src/vulkan/command.rs index 09059175f0..58fd6f7d8e 100644 --- a/wgpu-hal/src/vulkan/command.rs +++ b/wgpu-hal/src/vulkan/command.rs @@ -212,15 +212,44 @@ impl crate::CommandEncoder for super::CommandEncoder { } unsafe fn clear_buffer(&mut self, buffer: &super::Buffer, range: crate::MemoryRange) { - unsafe { - self.device.raw.cmd_fill_buffer( - self.active, - buffer.raw, - range.start, - range.end - range.start, - 0, - ) - }; + let range_size = range.end - range.start; + if self.device.workarounds.contains( + super::Workarounds::FORCE_FILL_BUFFER_WITH_SIZE_GREATER_4096_ALIGNED_OFFSET_16, + ) && range_size >= 4096 + && range.start % 16 != 0 + { + let rounded_start = wgt::math::align_to(range.start, 16); + let prefix_size = rounded_start - range.start; + + unsafe { + self.device.raw.cmd_fill_buffer( + self.active, + buffer.raw, + range.start, + prefix_size, + 0, + ) + }; + + // This will never be zero, as rounding can only add up to 12 bytes, and the total size is 4096. + let suffix_size = range.end - rounded_start; + + unsafe { + self.device.raw.cmd_fill_buffer( + self.active, + buffer.raw, + rounded_start, + suffix_size, + 0, + ) + }; + } else { + unsafe { + self.device + .raw + .cmd_fill_buffer(self.active, buffer.raw, range.start, range_size, 0) + }; + } } unsafe fn copy_buffer_to_buffer( diff --git a/wgpu-hal/src/vulkan/device.rs b/wgpu-hal/src/vulkan/device.rs index aa79dd236e..6e463ee2d2 100644 --- a/wgpu-hal/src/vulkan/device.rs +++ b/wgpu-hal/src/vulkan/device.rs @@ -1152,7 +1152,7 @@ impl crate::Device for super::Device { } if desc.anisotropy_clamp != 1 { - // We only enable anisotropy if it is supported, and wgpu-hal interface guarentees + // We only enable anisotropy if it is supported, and wgpu-hal interface guarantees // the clamp is in the range [1, 16] which is always supported if anisotropy is. vk_info = vk_info .anisotropy_enable(true) diff --git a/wgpu-hal/src/vulkan/instance.rs b/wgpu-hal/src/vulkan/instance.rs index 4fa4a3e27d..18269fff77 100644 --- a/wgpu-hal/src/vulkan/instance.rs +++ b/wgpu-hal/src/vulkan/instance.rs @@ -152,12 +152,11 @@ unsafe extern "system" fn debug_utils_messenger_callback( } impl super::Swapchain { + /// # Safety + /// + /// - The device must have been made idle before calling this function. unsafe fn release_resources(self, device: &ash::Device) -> Self { profiling::scope!("Swapchain::release_resources"); - { - profiling::scope!("vkDeviceWaitIdle"); - let _ = unsafe { device.device_wait_idle() }; - }; unsafe { device.destroy_fence(self.fence, None) }; self } @@ -186,7 +185,20 @@ impl super::Instance { &self.shared } - pub fn required_extensions( + /// Return the instance extension names wgpu would like to enable. + /// + /// Return a vector of the names of instance extensions actually available + /// on `entry` that wgpu would like to enable. + /// + /// The `driver_api_version` argument should be the instance's Vulkan API + /// version, as obtained from `vkEnumerateInstanceVersion`. This is the same + /// space of values as the `VK_API_VERSION` constants. + /// + /// Note that wgpu can function without many of these extensions (for + /// example, `VK_KHR_wayland_surface` is certainly not going to be available + /// everywhere), but if one of these extensions is available at all, wgpu + /// assumes that it has been enabled. + pub fn desired_extensions( entry: &ash::Entry, _driver_api_version: u32, flags: crate::InstanceFlags, @@ -194,8 +206,10 @@ impl super::Instance { let instance_extensions = entry .enumerate_instance_extension_properties(None) .map_err(|e| { - log::info!("enumerate_instance_extension_properties: {:?}", e); - crate::InstanceError + crate::InstanceError::with_source( + String::from("enumerate_instance_extension_properties() failed"), + e, + ) })?; // Check our extensions against the available extensions @@ -263,7 +277,7 @@ impl super::Instance { /// /// - `raw_instance` must be created from `entry` /// - `raw_instance` must be created respecting `driver_api_version`, `extensions` and `flags` - /// - `extensions` must be a superset of `required_extensions()` and must be created from the + /// - `extensions` must be a superset of `desired_extensions()` and must be created from the /// same entry, driver_api_version and flags. /// - `android_sdk_version` is ignored and can be `0` for all platforms besides Android /// @@ -366,8 +380,9 @@ impl super::Instance { window: vk::Window, ) -> Result { if !self.shared.extensions.contains(&khr::XlibSurface::name()) { - log::warn!("Vulkan driver does not support VK_KHR_xlib_surface"); - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(String::from( + "Vulkan driver does not support VK_KHR_xlib_surface", + ))); } let surface = { @@ -391,8 +406,9 @@ impl super::Instance { window: vk::xcb_window_t, ) -> Result { if !self.shared.extensions.contains(&khr::XcbSurface::name()) { - log::warn!("Vulkan driver does not support VK_KHR_xcb_surface"); - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(String::from( + "Vulkan driver does not support VK_KHR_xcb_surface", + ))); } let surface = { @@ -420,8 +436,9 @@ impl super::Instance { .extensions .contains(&khr::WaylandSurface::name()) { - log::debug!("Vulkan driver does not support VK_KHR_wayland_surface"); - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(String::from( + "Vulkan driver does not support VK_KHR_wayland_surface", + ))); } let surface = { @@ -447,8 +464,9 @@ impl super::Instance { .extensions .contains(&khr::AndroidSurface::name()) { - log::warn!("Vulkan driver does not support VK_KHR_android_surface"); - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(String::from( + "Vulkan driver does not support VK_KHR_android_surface", + ))); } let surface = { @@ -470,8 +488,9 @@ impl super::Instance { hwnd: *mut c_void, ) -> Result { if !self.shared.extensions.contains(&khr::Win32Surface::name()) { - log::debug!("Vulkan driver does not support VK_KHR_win32_surface"); - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(String::from( + "Vulkan driver does not support VK_KHR_win32_surface", + ))); } let surface = { @@ -496,8 +515,9 @@ impl super::Instance { view: *mut c_void, ) -> Result { if !self.shared.extensions.contains(&ext::MetalSurface::name()) { - log::warn!("Vulkan driver does not support VK_EXT_metal_surface"); - return Err(crate::InstanceError); + return Err(crate::InstanceError::new(String::from( + "Vulkan driver does not support VK_EXT_metal_surface", + ))); } let layer = unsafe { @@ -546,20 +566,18 @@ impl crate::Instance for super::Instance { unsafe fn init(desc: &crate::InstanceDescriptor) -> Result { use crate::auxil::cstr_from_bytes_until_nul; - let entry = match unsafe { ash::Entry::load() } { - Ok(entry) => entry, - Err(err) => { - log::info!("Missing Vulkan entry points: {:?}", err); - return Err(crate::InstanceError); - } - }; + let entry = unsafe { ash::Entry::load() }.map_err(|err| { + crate::InstanceError::with_source(String::from("missing Vulkan entry points"), err) + })?; let driver_api_version = match entry.try_enumerate_instance_version() { // Vulkan 1.1+ Ok(Some(version)) => version, Ok(None) => vk::API_VERSION_1_0, Err(err) => { - log::warn!("try_enumerate_instance_version: {:?}", err); - return Err(crate::InstanceError); + return Err(crate::InstanceError::with_source( + String::from("try_enumerate_instance_version() failed"), + err, + )); } }; @@ -586,11 +604,14 @@ impl crate::Instance for super::Instance { }, ); - let extensions = Self::required_extensions(&entry, driver_api_version, desc.flags)?; + let extensions = Self::desired_extensions(&entry, driver_api_version, desc.flags)?; let instance_layers = entry.enumerate_instance_layer_properties().map_err(|e| { log::info!("enumerate_instance_layer_properties: {:?}", e); - crate::InstanceError + crate::InstanceError::with_source( + String::from("enumerate_instance_layer_properties() failed"), + e, + ) })?; fn find_layer<'layers>( @@ -682,8 +703,10 @@ impl crate::Instance for super::Instance { .enabled_extension_names(&str_pointers[layers.len()..]); unsafe { entry.create_instance(&create_info, None) }.map_err(|e| { - log::warn!("create_instance: {:?}", e); - crate::InstanceError + crate::InstanceError::with_source( + String::from("Entry::create_instance() failed"), + e, + ) })? }; @@ -739,7 +762,9 @@ impl crate::Instance for super::Instance { { self.create_surface_from_view(handle.ui_view) } - (_, _) => Err(crate::InstanceError), + (_, _) => Err(crate::InstanceError::new(format!( + "window handle {window_handle:?} is not a Vulkan-compatible handle" + ))), } } @@ -773,13 +798,22 @@ impl crate::Instance for super::Instance { if exposed.info.device_type == wgt::DeviceType::IntegratedGpu && exposed.info.vendor == db::intel::VENDOR { - // See https://gitlab.freedesktop.org/mesa/mesa/-/issues/4688 - log::warn!( - "Disabling presentation on '{}' (id {:?}) because of NV Optimus (on Linux)", - exposed.info.name, - exposed.adapter.raw - ); - exposed.adapter.private_caps.can_present = false; + // Check if mesa driver and version less than 21.2 + if let Some(version) = exposed.info.driver_info.split_once("Mesa ").map(|s| { + s.1.rsplit_once('.') + .map(|v| v.0.parse::().unwrap_or_default()) + .unwrap_or_default() + }) { + if version < 21.2 { + // See https://gitlab.freedesktop.org/mesa/mesa/-/issues/4688 + log::warn!( + "Disabling presentation on '{}' (id {:?}) due to NV Optimus and Intel Mesa < v21.2", + exposed.info.name, + exposed.adapter.raw + ); + exposed.adapter.private_caps.can_present = false; + } + } } } } @@ -794,6 +828,7 @@ impl crate::Surface for super::Surface { device: &super::Device, config: &crate::SurfaceConfiguration, ) -> Result<(), crate::SurfaceError> { + // Safety: `configure`'s contract guarantees there are no resources derived from the swapchain in use. let old = self .swapchain .take() @@ -807,6 +842,7 @@ impl crate::Surface for super::Surface { unsafe fn unconfigure(&mut self, device: &super::Device) { if let Some(sc) = self.swapchain.take() { + // Safety: `unconfigure`'s contract guarantees there are no resources derived from the swapchain in use. let swapchain = unsafe { sc.release_resources(&device.shared.raw) }; unsafe { swapchain.functor.destroy_swapchain(swapchain.raw, None) }; } diff --git a/wgpu-hal/src/vulkan/mod.rs b/wgpu-hal/src/vulkan/mod.rs index 837bd15cec..dcc9b0cbd0 100644 --- a/wgpu-hal/src/vulkan/mod.rs +++ b/wgpu-hal/src/vulkan/mod.rs @@ -214,6 +214,28 @@ bitflags::bitflags!( /// Qualcomm OOMs when there are zero color attachments but a non-null pointer /// to a subpass resolve attachment array. This nulls out that pointer in that case. const EMPTY_RESOLVE_ATTACHMENT_LISTS = 0x2; + /// If the following code returns false, then nvidia will end up filling the wrong range. + /// + /// ```skip + /// fn nvidia_succeeds() -> bool { + /// # let (copy_length, start_offset) = (0, 0); + /// if copy_length >= 4096 { + /// if start_offset % 16 != 0 { + /// if copy_length == 4096 { + /// return true; + /// } + /// if copy_length % 16 == 0 { + /// return false; + /// } + /// } + /// } + /// true + /// } + /// ``` + /// + /// As such, we need to make sure all calls to vkCmdFillBuffer are aligned to 16 bytes + /// if they cover a range of 4096 bytes or more. + const FORCE_FILL_BUFFER_WITH_SIZE_GREATER_4096_ALIGNED_OFFSET_16 = 0x4; } ); diff --git a/wgpu-types/Cargo.toml b/wgpu-types/Cargo.toml index 4ef59398d0..fd0abb0dc9 100644 --- a/wgpu-types/Cargo.toml +++ b/wgpu-types/Cargo.toml @@ -42,4 +42,4 @@ web-sys = { version = "0.3.64", features = [ [dev-dependencies] serde = { version = "1", features = ["serde_derive"] } -serde_json = "1.0.105" +serde_json = "1.0.107" diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs index f389a17267..532e34b037 100644 --- a/wgpu-types/src/lib.rs +++ b/wgpu-types/src/lib.rs @@ -4326,73 +4326,73 @@ pub struct VertexAttribute { #[cfg_attr(feature = "replay", derive(Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum VertexFormat { - /// Two unsigned bytes (u8). `uvec2` in shaders. + /// Two unsigned bytes (u8). `vec2` in shaders. Uint8x2 = 0, - /// Four unsigned bytes (u8). `uvec4` in shaders. + /// Four unsigned bytes (u8). `vec4` in shaders. Uint8x4 = 1, - /// Two signed bytes (i8). `ivec2` in shaders. + /// Two signed bytes (i8). `vec2` in shaders. Sint8x2 = 2, - /// Four signed bytes (i8). `ivec4` in shaders. + /// Four signed bytes (i8). `vec4` in shaders. Sint8x4 = 3, - /// Two unsigned bytes (u8). [0, 255] converted to float [0, 1] `vec2` in shaders. + /// Two unsigned bytes (u8). [0, 255] converted to float [0, 1] `vec2` in shaders. Unorm8x2 = 4, - /// Four unsigned bytes (u8). [0, 255] converted to float [0, 1] `vec4` in shaders. + /// Four unsigned bytes (u8). [0, 255] converted to float [0, 1] `vec4` in shaders. Unorm8x4 = 5, - /// Two signed bytes (i8). [-127, 127] converted to float [-1, 1] `vec2` in shaders. + /// Two signed bytes (i8). [-127, 127] converted to float [-1, 1] `vec2` in shaders. Snorm8x2 = 6, - /// Four signed bytes (i8). [-127, 127] converted to float [-1, 1] `vec4` in shaders. + /// Four signed bytes (i8). [-127, 127] converted to float [-1, 1] `vec4` in shaders. Snorm8x4 = 7, - /// Two unsigned shorts (u16). `uvec2` in shaders. + /// Two unsigned shorts (u16). `vec2` in shaders. Uint16x2 = 8, - /// Four unsigned shorts (u16). `uvec4` in shaders. + /// Four unsigned shorts (u16). `vec4` in shaders. Uint16x4 = 9, - /// Two signed shorts (i16). `ivec2` in shaders. + /// Two signed shorts (i16). `vec2` in shaders. Sint16x2 = 10, - /// Four signed shorts (i16). `ivec4` in shaders. + /// Four signed shorts (i16). `vec4` in shaders. Sint16x4 = 11, - /// Two unsigned shorts (u16). [0, 65535] converted to float [0, 1] `vec2` in shaders. + /// Two unsigned shorts (u16). [0, 65535] converted to float [0, 1] `vec2` in shaders. Unorm16x2 = 12, - /// Four unsigned shorts (u16). [0, 65535] converted to float [0, 1] `vec4` in shaders. + /// Four unsigned shorts (u16). [0, 65535] converted to float [0, 1] `vec4` in shaders. Unorm16x4 = 13, - /// Two signed shorts (i16). [-32767, 32767] converted to float [-1, 1] `vec2` in shaders. + /// Two signed shorts (i16). [-32767, 32767] converted to float [-1, 1] `vec2` in shaders. Snorm16x2 = 14, - /// Four signed shorts (i16). [-32767, 32767] converted to float [-1, 1] `vec4` in shaders. + /// Four signed shorts (i16). [-32767, 32767] converted to float [-1, 1] `vec4` in shaders. Snorm16x4 = 15, - /// Two half-precision floats (no Rust equiv). `vec2` in shaders. + /// Two half-precision floats (no Rust equiv). `vec2` in shaders. Float16x2 = 16, - /// Four half-precision floats (no Rust equiv). `vec4` in shaders. + /// Four half-precision floats (no Rust equiv). `vec4` in shaders. Float16x4 = 17, - /// One single-precision float (f32). `float` in shaders. + /// One single-precision float (f32). `f32` in shaders. Float32 = 18, - /// Two single-precision floats (f32). `vec2` in shaders. + /// Two single-precision floats (f32). `vec2` in shaders. Float32x2 = 19, - /// Three single-precision floats (f32). `vec3` in shaders. + /// Three single-precision floats (f32). `vec3` in shaders. Float32x3 = 20, - /// Four single-precision floats (f32). `vec4` in shaders. + /// Four single-precision floats (f32). `vec4` in shaders. Float32x4 = 21, - /// One unsigned int (u32). `uint` in shaders. + /// One unsigned int (u32). `u32` in shaders. Uint32 = 22, - /// Two unsigned ints (u32). `uvec2` in shaders. + /// Two unsigned ints (u32). `vec2` in shaders. Uint32x2 = 23, - /// Three unsigned ints (u32). `uvec3` in shaders. + /// Three unsigned ints (u32). `vec3` in shaders. Uint32x3 = 24, - /// Four unsigned ints (u32). `uvec4` in shaders. + /// Four unsigned ints (u32). `vec4` in shaders. Uint32x4 = 25, - /// One signed int (i32). `int` in shaders. + /// One signed int (i32). `i32` in shaders. Sint32 = 26, - /// Two signed ints (i32). `ivec2` in shaders. + /// Two signed ints (i32). `vec2` in shaders. Sint32x2 = 27, - /// Three signed ints (i32). `ivec3` in shaders. + /// Three signed ints (i32). `vec3` in shaders. Sint32x3 = 28, - /// Four signed ints (i32). `ivec4` in shaders. + /// Four signed ints (i32). `vec4` in shaders. Sint32x4 = 29, - /// One double-precision float (f64). `double` in shaders. Requires [`Features::VERTEX_ATTRIBUTE_64BIT`]. + /// One double-precision float (f64). `f32` in shaders. Requires [`Features::VERTEX_ATTRIBUTE_64BIT`]. Float64 = 30, - /// Two double-precision floats (f64). `dvec2` in shaders. Requires [`Features::VERTEX_ATTRIBUTE_64BIT`]. + /// Two double-precision floats (f64). `vec2` in shaders. Requires [`Features::VERTEX_ATTRIBUTE_64BIT`]. Float64x2 = 31, - /// Three double-precision floats (f64). `dvec3` in shaders. Requires [`Features::VERTEX_ATTRIBUTE_64BIT`]. + /// Three double-precision floats (f64). `vec3` in shaders. Requires [`Features::VERTEX_ATTRIBUTE_64BIT`]. Float64x3 = 32, - /// Four double-precision floats (f64). `dvec4` in shaders. Requires [`Features::VERTEX_ATTRIBUTE_64BIT`]. + /// Four double-precision floats (f64). `vec4` in shaders. Requires [`Features::VERTEX_ATTRIBUTE_64BIT`]. Float64x4 = 33, } diff --git a/wgpu/src/backend/direct.rs b/wgpu/src/backend/direct.rs index fca1d80c3c..8eec9adad5 100644 --- a/wgpu/src/backend/direct.rs +++ b/wgpu/src/backend/direct.rs @@ -244,10 +244,7 @@ impl Context { &self, canvas: web_sys::HtmlCanvasElement, ) -> Result { - let id = self - .0 - .create_surface_webgl_canvas(canvas, ()) - .map_err(|hal::InstanceError| crate::CreateSurfaceError {})?; + let id = self.0.create_surface_webgl_canvas(canvas, ())?; Ok(Surface { id, configured_device: Mutex::default(), @@ -259,10 +256,7 @@ impl Context { &self, canvas: web_sys::OffscreenCanvas, ) -> Result { - let id = self - .0 - .create_surface_webgl_offscreen_canvas(canvas, ()) - .map_err(|hal::InstanceError| crate::CreateSurfaceError {})?; + let id = self.0.create_surface_webgl_offscreen_canvas(canvas, ())?; Ok(Surface { id, configured_device: Mutex::default(), diff --git a/wgpu/src/backend/web.rs b/wgpu/src/backend/web.rs index 10cc068a0b..0c42b00a15 100644 --- a/wgpu/src/backend/web.rs +++ b/wgpu/src/backend/web.rs @@ -687,6 +687,99 @@ fn map_wgt_features(supported_features: web_sys::GpuSupportedFeatures) -> wgt::F features } +fn map_wgt_limits(limits: web_sys::GpuSupportedLimits) -> wgt::Limits { + wgt::Limits { + max_texture_dimension_1d: limits.max_texture_dimension_1d(), + max_texture_dimension_2d: limits.max_texture_dimension_2d(), + max_texture_dimension_3d: limits.max_texture_dimension_3d(), + max_texture_array_layers: limits.max_texture_array_layers(), + max_bind_groups: limits.max_bind_groups(), + max_bindings_per_bind_group: limits.max_bindings_per_bind_group(), + max_dynamic_uniform_buffers_per_pipeline_layout: limits + .max_dynamic_uniform_buffers_per_pipeline_layout(), + max_dynamic_storage_buffers_per_pipeline_layout: limits + .max_dynamic_storage_buffers_per_pipeline_layout(), + max_sampled_textures_per_shader_stage: limits.max_sampled_textures_per_shader_stage(), + max_samplers_per_shader_stage: limits.max_samplers_per_shader_stage(), + max_storage_buffers_per_shader_stage: limits.max_storage_buffers_per_shader_stage(), + max_storage_textures_per_shader_stage: limits.max_storage_textures_per_shader_stage(), + max_uniform_buffers_per_shader_stage: limits.max_uniform_buffers_per_shader_stage(), + max_uniform_buffer_binding_size: limits.max_uniform_buffer_binding_size() as u32, + max_storage_buffer_binding_size: limits.max_storage_buffer_binding_size() as u32, + max_vertex_buffers: limits.max_vertex_buffers(), + max_buffer_size: limits.max_buffer_size() as u64, + max_vertex_attributes: limits.max_vertex_attributes(), + max_vertex_buffer_array_stride: limits.max_vertex_buffer_array_stride(), + min_uniform_buffer_offset_alignment: limits.min_uniform_buffer_offset_alignment(), + min_storage_buffer_offset_alignment: limits.min_storage_buffer_offset_alignment(), + max_inter_stage_shader_components: limits.max_inter_stage_shader_components(), + max_compute_workgroup_storage_size: limits.max_compute_workgroup_storage_size(), + max_compute_invocations_per_workgroup: limits.max_compute_invocations_per_workgroup(), + max_compute_workgroup_size_x: limits.max_compute_workgroup_size_x(), + max_compute_workgroup_size_y: limits.max_compute_workgroup_size_y(), + max_compute_workgroup_size_z: limits.max_compute_workgroup_size_z(), + max_compute_workgroups_per_dimension: limits.max_compute_workgroups_per_dimension(), + // The following are not part of WebGPU + max_push_constant_size: wgt::Limits::default().max_push_constant_size, + max_non_sampler_bindings: wgt::Limits::default().max_non_sampler_bindings, + } +} + +fn map_js_sys_limits(limits: &wgt::Limits) -> js_sys::Object { + let object = js_sys::Object::new(); + + macro_rules! set_properties { + (($from:expr) => ($on:expr) : $(($js_ident:ident, $rs_ident:ident)),* $(,)?) => { + $( + ::js_sys::Reflect::set( + &$on, + &::wasm_bindgen::JsValue::from(stringify!($js_ident)), + // Numbers may be u64, however using `from` on a u64 yields + // errors on the wasm side, since it uses an unsupported api. + // Wasm sends us things that need to fit into u64s by sending + // us f64s instead. So we just send them f64s back. + &::wasm_bindgen::JsValue::from($from.$rs_ident as f64) + ) + .expect("Setting Object properties should never fail."); + )* + } + } + + set_properties![ + (limits) => (object): + (maxTextureDimension1D, max_texture_dimension_1d), + (maxTextureDimension2D, max_texture_dimension_2d), + (maxTextureDimension3D, max_texture_dimension_3d), + (maxTextureArrayLayers, max_texture_array_layers), + (maxBindGroups, max_bind_groups), + (maxBindingsPerBindGroup, max_bindings_per_bind_group), + (maxDynamicUniformBuffersPerPipelineLayout, max_dynamic_uniform_buffers_per_pipeline_layout), + (maxDynamicStorageBuffersPerPipelineLayout, max_dynamic_storage_buffers_per_pipeline_layout), + (maxSampledTexturesPerShaderStage, max_sampled_textures_per_shader_stage), + (maxSamplersPerShaderStage, max_samplers_per_shader_stage), + (maxStorageBuffersPerShaderStage, max_storage_buffers_per_shader_stage), + (maxStorageTexturesPerShaderStage, max_storage_textures_per_shader_stage), + (maxUniformBuffersPerShaderStage, max_uniform_buffers_per_shader_stage), + (maxUniformBufferBindingSize, max_uniform_buffer_binding_size), + (maxStorageBufferBindingSize, max_storage_buffer_binding_size), + (minUniformBufferOffsetAlignment, min_uniform_buffer_offset_alignment), + (minStorageBufferOffsetAlignment, min_storage_buffer_offset_alignment), + (maxVertexBuffers, max_vertex_buffers), + (maxBufferSize, max_buffer_size), + (maxVertexAttributes, max_vertex_attributes), + (maxVertexBufferArrayStride, max_vertex_buffer_array_stride), + (maxInterStageShaderComponents, max_inter_stage_shader_components), + (maxComputeWorkgroupStorageSize, max_compute_workgroup_storage_size), + (maxComputeInvocationsPerWorkgroup, max_compute_invocations_per_workgroup), + (maxComputeWorkgroupSizeX, max_compute_workgroup_size_x), + (maxComputeWorkgroupSizeY, max_compute_workgroup_size_y), + (maxComputeWorkgroupSizeZ, max_compute_workgroup_size_z), + (maxComputeWorkgroupsPerDimension, max_compute_workgroups_per_dimension), + ]; + + object +} + type JsFutureResult = Result; fn future_request_adapter( @@ -827,13 +920,22 @@ impl Context { // “not supported” could include “insufficient GPU resources” or “the GPU process // previously crashed”. So, we must return it as an `Err` since it could occur // for circumstances outside the application author's control. - return Err(crate::CreateSurfaceError {}); + return Err(crate::CreateSurfaceError { + inner: crate::CreateSurfaceErrorKind::Web( + String::from( + "canvas.getContext() returned null; webgpu not available or canvas already in use" + ) + ) + }); } Err(js_error) => { // - // A thrown exception indicates misuse of the canvas state. Ideally we wouldn't - // panic in this case ... TODO - panic!("canvas.getContext() threw {js_error:?}") + // A thrown exception indicates misuse of the canvas state. + return Err(crate::CreateSurfaceError { + inner: crate::CreateSurfaceErrorKind::Web(format!( + "canvas.getContext() threw exception {js_error:?}", + )), + }); } }; @@ -1014,9 +1116,19 @@ impl crate::context::Context for Context { //Error: Tracing isn't supported on the Web target } - // TODO: non-guaranteed limits let mut mapped_desc = web_sys::GpuDeviceDescriptor::new(); + // TODO: Migrate to a web_sys api. + // See https://github.com/rustwasm/wasm-bindgen/issues/3587 + let limits_object = map_js_sys_limits(&desc.limits); + + js_sys::Reflect::set( + &mapped_desc, + &JsValue::from("requiredLimits"), + &limits_object, + ) + .expect("Setting Object properties should never fail."); + let required_features = FEATURES_MAPPING .iter() .copied() @@ -1070,30 +1182,7 @@ impl crate::context::Context for Context { _adapter: &Self::AdapterId, adapter_data: &Self::AdapterData, ) -> wgt::Limits { - let limits = adapter_data.0.limits(); - wgt::Limits { - max_texture_dimension_1d: limits.max_texture_dimension_1d(), - max_texture_dimension_2d: limits.max_texture_dimension_2d(), - max_texture_dimension_3d: limits.max_texture_dimension_3d(), - max_texture_array_layers: limits.max_texture_array_layers(), - max_bind_groups: limits.max_bind_groups(), - max_bindings_per_bind_group: limits.max_bindings_per_bind_group(), - max_dynamic_uniform_buffers_per_pipeline_layout: limits - .max_dynamic_uniform_buffers_per_pipeline_layout(), - max_dynamic_storage_buffers_per_pipeline_layout: limits - .max_dynamic_storage_buffers_per_pipeline_layout(), - max_sampled_textures_per_shader_stage: limits.max_sampled_textures_per_shader_stage(), - max_samplers_per_shader_stage: limits.max_samplers_per_shader_stage(), - max_storage_buffers_per_shader_stage: limits.max_storage_buffers_per_shader_stage(), - max_storage_textures_per_shader_stage: limits.max_storage_textures_per_shader_stage(), - max_uniform_buffers_per_shader_stage: limits.max_uniform_buffers_per_shader_stage(), - max_uniform_buffer_binding_size: limits.max_uniform_buffer_binding_size() as u32, - max_storage_buffer_binding_size: limits.max_storage_buffer_binding_size() as u32, - max_vertex_buffers: limits.max_vertex_buffers(), - max_vertex_attributes: limits.max_vertex_attributes(), - max_vertex_buffer_array_stride: limits.max_vertex_buffer_array_stride(), - ..wgt::Limits::default() - } + map_wgt_limits(adapter_data.0.limits()) } fn adapter_downlevel_capabilities( @@ -1256,10 +1345,9 @@ impl crate::context::Context for Context { fn device_limits( &self, _device: &Self::DeviceId, - _device_data: &Self::DeviceData, + device_data: &Self::DeviceData, ) -> wgt::Limits { - // TODO - wgt::Limits::default() + map_wgt_limits(device_data.0.limits()) } fn device_downlevel_properties( diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 1c3e1a58b5..94345f1adb 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -15,8 +15,7 @@ mod macros; use std::{ any::Any, borrow::Cow, - error, - fmt::{Debug, Display}, + error, fmt, future::Future, marker::PhantomData, num::NonZeroU32, @@ -1700,8 +1699,8 @@ pub enum SurfaceError { } static_assertions::assert_impl_all!(SurfaceError: Send, Sync); -impl Display for SurfaceError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for SurfaceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", match self { Self::Timeout => "A timeout was encountered while trying to acquire the next frame", Self::Outdated => "The underlying surface has changed, and therefore the swap chain must be updated", @@ -2744,8 +2743,8 @@ impl Drop for Device { pub struct RequestDeviceError; static_assertions::assert_impl_all!(RequestDeviceError: Send, Sync); -impl Display for RequestDeviceError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for RequestDeviceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Requesting a device failed") } } @@ -2753,28 +2752,76 @@ impl Display for RequestDeviceError { impl error::Error for RequestDeviceError {} /// [`Instance::create_surface()`] or a related function failed. -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, Debug)] #[non_exhaustive] pub struct CreateSurfaceError { - // TODO: Report diagnostic clues + inner: CreateSurfaceErrorKind, +} +#[derive(Clone, Debug)] +enum CreateSurfaceErrorKind { + /// Error from [`wgpu_hal`]. + #[cfg(any( + not(target_arch = "wasm32"), + target_os = "emscripten", + feature = "webgl" + ))] + // must match dependency cfg + Hal(hal::InstanceError), + + /// Error from WebGPU surface creation. + #[allow(dead_code)] // may be unused depending on target and features + Web(String), } static_assertions::assert_impl_all!(CreateSurfaceError: Send, Sync); -impl Display for CreateSurfaceError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Creating a surface failed") +impl fmt::Display for CreateSurfaceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.inner { + #[cfg(any( + not(target_arch = "wasm32"), + target_os = "emscripten", + feature = "webgl" + ))] + CreateSurfaceErrorKind::Hal(e) => e.fmt(f), + CreateSurfaceErrorKind::Web(e) => e.fmt(f), + } } } -impl error::Error for CreateSurfaceError {} +impl error::Error for CreateSurfaceError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match &self.inner { + #[cfg(any( + not(target_arch = "wasm32"), + target_os = "emscripten", + feature = "webgl" + ))] + CreateSurfaceErrorKind::Hal(e) => e.source(), + CreateSurfaceErrorKind::Web(_) => None, + } + } +} + +#[cfg(any( + not(target_arch = "wasm32"), + target_os = "emscripten", + feature = "webgl" +))] +impl From for CreateSurfaceError { + fn from(e: hal::InstanceError) -> Self { + Self { + inner: CreateSurfaceErrorKind::Hal(e), + } + } +} /// Error occurred when trying to async map a buffer. #[derive(Clone, PartialEq, Eq, Debug)] pub struct BufferAsyncError; static_assertions::assert_impl_all!(BufferAsyncError: Send, Sync); -impl Display for BufferAsyncError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for BufferAsyncError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Error occurred when trying to async map a buffer") } } @@ -4849,8 +4896,8 @@ impl Clone for Id { impl Copy for Id {} #[cfg(feature = "expose-ids")] -impl Debug for Id { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +impl fmt::Debug for Id { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("Id").field(&self.0).finish() } } @@ -5150,8 +5197,8 @@ impl error::Error for Error { } } -impl Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::OutOfMemory { .. } => f.write_str("Out of Memory"), Error::Validation { description, .. } => f.write_str(description),