From 7774f310218bd688e27e7688f3ddf8cd39226c96 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Sun, 14 Jan 2024 10:45:52 +0100 Subject: [PATCH] Allow WebGPU & WebGL in same wasm and detect WebGPU availability (#5044) * Rename backends: web -> webgpu, direct -> wgpu_core * rename context objects for web & core * allow webgpu & webgl features side-by-side * make sure webgl ci doesn't use webgpu * update any_backend_feature_enabled * add panicing generate_report method for compatibility * RequestDeviceErrorKind::Web rename, fixup various cfg attributes * automatic webgpu support detection * changelog entry * fix emscripten * fix weird cfg, fix comment typo * remove try_generate_report again * Make get_mapped_range_as_array_buffer WebGPU only again --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 9 + README.md | 2 +- tests/src/init.rs | 12 +- tests/tests/device.rs | 4 +- tests/tests/mem_leaks.rs | 32 +-- wgpu-types/src/lib.rs | 7 +- wgpu/Cargo.toml | 6 +- wgpu/build.rs | 3 +- wgpu/src/backend/mod.rs | 12 +- wgpu/src/backend/{web.rs => webgpu.rs} | 108 ++++---- wgpu/src/backend/{direct.rs => wgpu_core.rs} | 14 +- wgpu/src/context.rs | 27 +- wgpu/src/lib.rs | 263 +++++++++++-------- xtask/src/run_wasm.rs | 2 +- 15 files changed, 291 insertions(+), 212 deletions(-) rename wgpu/src/backend/{web.rs => webgpu.rs} (97%) rename wgpu/src/backend/{direct.rs => wgpu_core.rs} (99%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84ec9d0155..f6d9a3e366 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -367,7 +367,7 @@ jobs: - name: execute tests run: | cd wgpu - wasm-pack test --headless --chrome --features webgl --workspace + wasm-pack test --headless --chrome --no-default-features --features wgsl,webgl --workspace gpu-test: # runtime is normally 5-15 minutes diff --git a/CHANGELOG.md b/CHANGELOG.md index 4483dd1e83..7bdb11de29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,15 @@ Conversion to `wgpu::SurfaceTarget` is automatic for anything implementing `raw- meaning that you can continue to e.g. pass references to winit windows as before. By @wumpf in [#4984](https://github.com/gfx-rs/wgpu/pull/4984) +### WebGPU & WebGL in the same binary + +Enabling `webgl` no longer removes the `webgpu` backend. +Instead, there's a new (default enabled) `webgpu` feature that allows to explicitly opt-out of `webgpu` if so desired. +If both `webgl` & `webgpu` are enabled, `wgpu::Instance` decides upon creation whether to target wgpu-core/WebGL or WebGPU. +This means that adapter selection is not handled as with regular adapters, but still allows to decide at runtime whether +`webgpu` or the `webgl` backend should be used using a single wasm binary. +By @wumpf in [#5044](https://github.com/gfx-rs/wgpu/pull/5044) + ### New Features #### General diff --git a/README.md b/README.md index ae3edf774b..4dbcd243ef 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ To run the test suite on WebGL (currently incomplete): ``` cd wgpu -wasm-pack test --headless --chrome --features webgl --workspace +wasm-pack test --headless --chrome --no-default-features --features webgl --workspace ``` This will automatically run the tests using a packaged browser. Remove `--headless` to run the tests with whatever browser you wish at `http://localhost:8000`. diff --git a/tests/src/init.rs b/tests/src/init.rs index 6a24f5faec..9a21c98471 100644 --- a/tests/src/init.rs +++ b/tests/src/init.rs @@ -16,7 +16,17 @@ pub fn initialize_instance() -> Instance { // // We can potentially work support back into the test runner in the future, but as the adapters are matched up // based on adapter index, removing some backends messes up the indexes in annoying ways. - let backends = Backends::all(); + // + // WORKAROUND for https://github.com/rust-lang/cargo/issues/7160: + // `--no-default-features` is not passed through correctly to the test runner. + // We use it whenever we want to explicitly run with webgl instead of webgpu. + // To "disable" webgpu regardless, we do this by removing the webgpu backend whenever we see + // the webgl feature. + let backends = if cfg!(feature = "webgl") { + Backends::all() - Backends::BROWSER_WEBGPU + } 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 { diff --git a/tests/tests/device.rs b/tests/tests/device.rs index b6070de14f..5ab54927f8 100644 --- a/tests/tests/device.rs +++ b/tests/tests/device.rs @@ -52,11 +52,11 @@ fn device_lifetime_check() { instance.poll_all(false); - let pre_report = instance.generate_report(); + let pre_report = instance.generate_report().unwrap().unwrap(); drop(queue); drop(device); - let post_report = instance.generate_report(); + let post_report = instance.generate_report().unwrap().unwrap(); assert_ne!( pre_report, post_report, "Queue and Device has not been dropped as expected" diff --git a/tests/tests/mem_leaks.rs b/tests/tests/mem_leaks.rs index 5b5f0ce820..91a329d20c 100644 --- a/tests/tests/mem_leaks.rs +++ b/tests/tests/mem_leaks.rs @@ -12,7 +12,7 @@ async fn draw_test_with_reports( use wgpu::util::DeviceExt; - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.devices.num_allocated, 1); assert_eq!(report.queues.num_allocated, 1); @@ -21,7 +21,7 @@ async fn draw_test_with_reports( .device .create_shader_module(wgpu::include_wgsl!("./vertex_indices/draw.vert.wgsl")); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.shader_modules.num_allocated, 1); @@ -41,7 +41,7 @@ async fn draw_test_with_reports( }], }); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.buffers.num_allocated, 0); assert_eq!(report.bind_groups.num_allocated, 0); @@ -54,7 +54,7 @@ async fn draw_test_with_reports( mapped_at_creation: false, }); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.buffers.num_allocated, 1); @@ -67,7 +67,7 @@ async fn draw_test_with_reports( }], }); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.buffers.num_allocated, 1); assert_eq!(report.bind_groups.num_allocated, 1); @@ -81,7 +81,7 @@ async fn draw_test_with_reports( push_constant_ranges: &[], }); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.buffers.num_allocated, 1); assert_eq!(report.pipeline_layouts.num_allocated, 1); @@ -113,7 +113,7 @@ async fn draw_test_with_reports( multiview: None, }); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.buffers.num_allocated, 1); assert_eq!(report.bind_groups.num_allocated, 1); @@ -125,7 +125,7 @@ async fn draw_test_with_reports( drop(shader); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.shader_modules.num_allocated, 1); assert_eq!(report.shader_modules.num_kept_from_user, 0); @@ -153,7 +153,7 @@ async fn draw_test_with_reports( ); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.buffers.num_allocated, 1); assert_eq!(report.texture_views.num_allocated, 1); @@ -161,7 +161,7 @@ async fn draw_test_with_reports( drop(texture); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.buffers.num_allocated, 1); assert_eq!(report.texture_views.num_allocated, 1); @@ -173,7 +173,7 @@ async fn draw_test_with_reports( .device .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.command_buffers.num_allocated, 1); assert_eq!(report.buffers.num_allocated, 1); @@ -193,7 +193,7 @@ async fn draw_test_with_reports( rpass.set_pipeline(&pipeline); rpass.set_bind_group(0, &bg, &[]); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.buffers.num_allocated, 1); assert_eq!(report.bind_groups.num_allocated, 1); @@ -216,7 +216,7 @@ async fn draw_test_with_reports( drop(bg); drop(buffer); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.command_buffers.num_kept_from_user, 1); assert_eq!(report.render_pipelines.num_kept_from_user, 0); @@ -237,7 +237,7 @@ async fn draw_test_with_reports( let submit_index = ctx.queue.submit(Some(encoder.finish())); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.command_buffers.num_allocated, 0); @@ -245,7 +245,7 @@ async fn draw_test_with_reports( .await .panic_on_timeout(); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.render_pipelines.num_allocated, 0); @@ -260,7 +260,7 @@ async fn draw_test_with_reports( drop(ctx.device); drop(ctx.adapter); - let global_report = ctx.instance.generate_report(); + let global_report = ctx.instance.generate_report().unwrap(); let report = global_report.hub_report(ctx.adapter_info.backend); assert_eq!(report.queues.num_kept_from_user, 0); diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs index ac8992d25b..2b800a35c0 100644 --- a/wgpu-types/src/lib.rs +++ b/wgpu-types/src/lib.rs @@ -155,7 +155,12 @@ bitflags::bitflags! { const METAL = 1 << Backend::Metal as u32; /// Supported on Windows 10 const DX12 = 1 << Backend::Dx12 as u32; - /// Supported when targeting the web through webassembly + /// Supported when targeting the web through webassembly with the `webgpu` feature enabled. + /// + /// The WebGPU backend is special in several ways: + /// It is not not implemented by `wgpu_core` and instead by the higher level `wgpu` crate. + /// Whether WebGPU is targeted is decided upon the creation of the `wgpu::Instance`, + /// *not* upon adapter creation. See `wgpu::Instance::new`. const BROWSER_WEBGPU = 1 << Backend::BrowserWebGpu as u32; /// All the apis that wgpu offers first tier of support for. /// diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 2a83aa364c..8848371b9d 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -24,7 +24,7 @@ targets = [ [lib] [features] -default = ["wgsl", "dx12", "metal"] +default = ["wgsl", "dx12", "metal", "webgpu"] #! ### Backends # -------------------------------------------------------------------- @@ -44,10 +44,12 @@ angle = ["wgc?/gles"] ## Enables the Vulkan backend on macOS & iOS. vulkan-portability = ["wgc?/vulkan"] +## Enables the WebGPU backend on Wasm. Disabled when targeting `emscripten`. +webgpu = [] + ## Enables the GLES backend on Wasm ## ## * ⚠️ WIP: Currently will also enable GLES dependencies on any other targets. -## * ⚠️ WIP: This automatically disables use of WebGPU. See [#2804](https://github.com/gfx-rs/wgpu/issues/3514). webgl = ["hal", "wgc/gles"] #! ### Shading language support diff --git a/wgpu/build.rs b/wgpu/build.rs index 97e17e687c..6f3d0f58ab 100644 --- a/wgpu/build.rs +++ b/wgpu/build.rs @@ -2,8 +2,9 @@ fn main() { cfg_aliases::cfg_aliases! { native: { not(target_arch = "wasm32") }, webgl: { all(target_arch = "wasm32", not(target_os = "emscripten"), feature = "webgl") }, - webgpu: { all(target_arch = "wasm32", not(target_os = "emscripten"), not(feature = "webgl")) }, + webgpu: { all(target_arch = "wasm32", not(target_os = "emscripten"), feature = "webgpu") }, Emscripten: { all(target_arch = "wasm32", target_os = "emscripten") }, + wgpu_core: { any(native, webgl, emscripten) }, send_sync: { any( not(target_arch = "wasm32"), all(feature = "fragile-send-sync-non-atomic-wasm", not(target_feature = "atomics")) diff --git a/wgpu/src/backend/mod.rs b/wgpu/src/backend/mod.rs index 9b8157e3ba..02d9632efb 100644 --- a/wgpu/src/backend/mod.rs +++ b/wgpu/src/backend/mod.rs @@ -1,9 +1,9 @@ #[cfg(webgpu)] -mod web; +mod webgpu; #[cfg(webgpu)] -pub(crate) use web::Context; +pub(crate) use webgpu::{get_browser_gpu_property, ContextWebGpu}; -#[cfg(not(webgpu))] -mod direct; -#[cfg(not(webgpu))] -pub(crate) use direct::Context; +#[cfg(wgpu_core)] +mod wgpu_core; +#[cfg(wgpu_core)] +pub(crate) use wgpu_core::ContextWgpuCore; diff --git a/wgpu/src/backend/web.rs b/wgpu/src/backend/webgpu.rs similarity index 97% rename from wgpu/src/backend/web.rs rename to wgpu/src/backend/webgpu.rs index 749a7ba329..f8a3380c99 100644 --- a/wgpu/src/backend/web.rs +++ b/wgpu/src/backend/webgpu.rs @@ -69,19 +69,21 @@ unsafe impl Send for Identified {} #[cfg(send_sync)] unsafe impl Sync for Identified {} -pub(crate) struct Context(web_sys::Gpu); +pub(crate) struct ContextWebGpu(web_sys::Gpu); #[cfg(send_sync)] -unsafe impl Send for Context {} +unsafe impl Send for ContextWebGpu {} #[cfg(send_sync)] -unsafe impl Sync for Context {} +unsafe impl Sync for ContextWebGpu {} #[cfg(send_sync)] unsafe impl Send for BufferMappedRange {} #[cfg(send_sync)] unsafe impl Sync for BufferMappedRange {} -impl fmt::Debug for Context { +impl fmt::Debug for ContextWebGpu { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Context").field("type", &"Web").finish() + f.debug_struct("ContextWebGpu") + .field("type", &"Web") + .finish() } } @@ -540,7 +542,8 @@ fn map_texture_view_dimension( } fn map_buffer_copy_view(view: crate::ImageCopyBuffer<'_>) -> web_sys::GpuImageCopyBuffer { - let buffer: &::BufferData = downcast_ref(view.buffer.data.as_ref()); + let buffer: &::BufferData = + downcast_ref(view.buffer.data.as_ref()); let mut mapped = web_sys::GpuImageCopyBuffer::new(&buffer.0.buffer); if let Some(bytes_per_row) = view.layout.bytes_per_row { mapped.bytes_per_row(bytes_per_row); @@ -553,7 +556,7 @@ fn map_buffer_copy_view(view: crate::ImageCopyBuffer<'_>) -> web_sys::GpuImageCo } fn map_texture_copy_view(view: crate::ImageCopyTexture<'_>) -> web_sys::GpuImageCopyTexture { - let texture: &::TextureData = + let texture: &::TextureData = downcast_ref(view.texture.data.as_ref()); let mut mapped = web_sys::GpuImageCopyTexture::new(&texture.0); mapped.mip_level(view.mip_level); @@ -564,7 +567,7 @@ fn map_texture_copy_view(view: crate::ImageCopyTexture<'_>) -> web_sys::GpuImage fn map_tagged_texture_copy_view( view: crate::ImageCopyTextureTagged<'_>, ) -> web_sys::GpuImageCopyTextureTagged { - let texture: &::TextureData = + let texture: &::TextureData = downcast_ref(view.texture.data.as_ref()); let mut mapped = web_sys::GpuImageCopyTextureTagged::new(&texture.0); mapped.mip_level(view.mip_level); @@ -821,7 +824,7 @@ fn future_request_device( (device_id, device_data, queue_id, queue_data) }) .map_err(|error_value| crate::RequestDeviceError { - inner: crate::RequestDeviceErrorKind::Web(error_value), + inner: crate::RequestDeviceErrorKind::WebGpu(error_value), }) } @@ -877,7 +880,7 @@ where *rc_callback.borrow_mut() = Some((closure_success, closure_rejected, callback)); } -impl Context { +impl ContextWebGpu { /// Common portion of the internal branches of the public `instance_create_surface` function. /// /// Note: Analogous code also exists in the WebGL2 backend at @@ -929,6 +932,15 @@ impl Context { Ok(create_identified((canvas, context))) } + + /// Get mapped buffer range directly as a `js_sys::ArrayBuffer`. + pub fn buffer_get_mapped_range_as_array_buffer( + &self, + buffer_data: &::BufferData, + sub_range: Range, + ) -> js_sys::ArrayBuffer { + buffer_data.0.get_mapped_array_buffer(sub_range) + } } // Represents the global object in the JavaScript context. @@ -952,7 +964,31 @@ pub enum Canvas { Offscreen(web_sys::OffscreenCanvas), } -impl crate::context::Context for Context { +/// Returns the browsers gpu object or `None` if the current context is neither the main thread nor a dedicated worker. +/// +/// If WebGPU is not supported, the Gpu property is `undefined` (but *not* necessarily `None`). +/// +/// See: +/// * +/// * +pub fn get_browser_gpu_property() -> Option { + let global: Global = js_sys::global().unchecked_into(); + + if !global.window().is_undefined() { + Some(global.unchecked_into::().navigator().gpu()) + } else if !global.worker().is_undefined() { + Some( + global + .unchecked_into::() + .navigator() + .gpu(), + ) + } else { + None + } +} + +impl crate::context::Context for ContextWebGpu { type AdapterId = Identified; type AdapterData = Sendable; type DeviceId = Identified; @@ -1022,20 +1058,12 @@ impl crate::context::Context for Context { MakeSendFuture Option>; fn init(_instance_desc: wgt::InstanceDescriptor) -> Self { - let global: Global = js_sys::global().unchecked_into(); - let gpu = if !global.window().is_undefined() { - global.unchecked_into::().navigator().gpu() - } else if !global.worker().is_undefined() { - global - .unchecked_into::() - .navigator() - .gpu() - } else { + let Some(gpu) = get_browser_gpu_property() else { panic!( "Accessing the GPU is only supported on the main thread or from a dedicated worker" ); }; - Context(gpu) + ContextWebGpu(gpu) } unsafe fn instance_create_surface( @@ -1587,7 +1615,7 @@ impl crate::context::Context for Context { offset, size, }) => { - let buffer: &::BufferData = + let buffer: &::BufferData = downcast_ref(buffer.data.as_ref()); let mut mapped_buffer_binding = web_sys::GpuBufferBinding::new(&buffer.0.buffer); @@ -1601,7 +1629,7 @@ impl crate::context::Context for Context { panic!("Web backend does not support arrays of buffers") } crate::BindingResource::Sampler(sampler) => { - let sampler: &::SamplerData = + let sampler: &::SamplerData = downcast_ref(sampler.data.as_ref()); JsValue::from(&sampler.0) } @@ -1609,7 +1637,7 @@ impl crate::context::Context for Context { panic!("Web backend does not support arrays of samplers") } crate::BindingResource::TextureView(texture_view) => { - let texture_view: &::TextureViewData = + let texture_view: &::TextureViewData = downcast_ref(texture_view.data.as_ref()); JsValue::from(&texture_view.0) } @@ -1622,7 +1650,7 @@ impl crate::context::Context for Context { }) .collect::(); - let bgl: &::BindGroupLayoutData = + let bgl: &::BindGroupLayoutData = downcast_ref(desc.layout.data.as_ref()); let mut mapped_desc = web_sys::GpuBindGroupDescriptor::new(&mapped_entries, &bgl.0); if let Some(label) = desc.label { @@ -1641,7 +1669,7 @@ impl crate::context::Context for Context { .bind_group_layouts .iter() .map(|bgl| { - let bgl: &::BindGroupLayoutData = + let bgl: &::BindGroupLayoutData = downcast_ref(bgl.data.as_ref()); &bgl.0 }) @@ -1659,7 +1687,7 @@ impl crate::context::Context for Context { device_data: &Self::DeviceData, desc: &crate::RenderPipelineDescriptor<'_>, ) -> (Self::RenderPipelineId, Self::RenderPipelineData) { - let module: &::ShaderModuleData = + let module: &::ShaderModuleData = downcast_ref(desc.vertex.module.data.as_ref()); let mut mapped_vertex_state = web_sys::GpuVertexState::new(desc.vertex.entry_point, &module.0); @@ -1696,7 +1724,7 @@ impl crate::context::Context for Context { let mut mapped_desc = web_sys::GpuRenderPipelineDescriptor::new( &match desc.layout { Some(layout) => { - let layout: &::PipelineLayoutData = + let layout: &::PipelineLayoutData = downcast_ref(layout.data.as_ref()); JsValue::from(&layout.0) } @@ -1734,7 +1762,7 @@ impl crate::context::Context for Context { None => wasm_bindgen::JsValue::null(), }) .collect::(); - let module: &::ShaderModuleData = + let module: &::ShaderModuleData = downcast_ref(frag.module.data.as_ref()); let mapped_fragment_desc = web_sys::GpuFragmentState::new(frag.entry_point, &module.0, &targets); @@ -1759,7 +1787,7 @@ impl crate::context::Context for Context { device_data: &Self::DeviceData, desc: &crate::ComputePipelineDescriptor<'_>, ) -> (Self::ComputePipelineId, Self::ComputePipelineData) { - let shader_module: &::ShaderModuleData = + let shader_module: &::ShaderModuleData = downcast_ref(desc.module.data.as_ref()); let mapped_compute_stage = web_sys::GpuProgrammableStage::new(desc.entry_point, &shader_module.0); @@ -1767,7 +1795,7 @@ impl crate::context::Context for Context { let mut mapped_desc = web_sys::GpuComputePipelineDescriptor::new( &match desc.layout { Some(layout) => { - let layout: &::PipelineLayoutData = + let layout: &::PipelineLayoutData = downcast_ref(layout.data.as_ref()); JsValue::from(&layout.0) } @@ -2029,15 +2057,6 @@ impl crate::context::Context for Context { }) } - fn buffer_get_mapped_range_as_array_buffer( - &self, - _buffer: &Self::BufferId, - buffer_data: &Self::BufferData, - sub_range: Range, - ) -> js_sys::ArrayBuffer { - buffer_data.0.get_mapped_array_buffer(sub_range) - } - fn buffer_unmap(&self, _buffer: &Self::BufferId, buffer_data: &Self::BufferData) { buffer_data.0.buffer.unmap(); buffer_data.0.mapping.borrow_mut().mapped_buffer = None; @@ -2322,7 +2341,7 @@ impl crate::context::Context for Context { crate::LoadOp::Load => web_sys::GpuLoadOp::Load, }; - let view: &::TextureViewData = + let view: &::TextureViewData = downcast_ref(ca.view.data.as_ref()); let mut mapped_color_attachment = web_sys::GpuRenderPassColorAttachment::new( @@ -2334,7 +2353,7 @@ impl crate::context::Context for Context { mapped_color_attachment.clear_value(&cv); } if let Some(rt) = ca.resolve_target { - let resolve_target_view: &::TextureViewData = + let resolve_target_view: &::TextureViewData = downcast_ref(rt.data.as_ref()); mapped_color_attachment.resolve_target(&resolve_target_view.0); } @@ -2353,7 +2372,7 @@ impl crate::context::Context for Context { } if let Some(dsa) = &desc.depth_stencil_attachment { - let depth_stencil_attachment: &::TextureViewData = + let depth_stencil_attachment: &::TextureViewData = downcast_ref(dsa.view.data.as_ref()); let mut mapped_depth_stencil_attachment = web_sys::GpuRenderPassDepthStencilAttachment::new(&depth_stencil_attachment.0); @@ -2430,7 +2449,8 @@ impl crate::context::Context for Context { offset: wgt::BufferAddress, size: Option, ) { - let buffer: &::BufferData = downcast_ref(buffer.data.as_ref()); + let buffer: &::BufferData = + downcast_ref(buffer.data.as_ref()); match size { Some(size) => encoder_data.0.clear_buffer_with_f64_and_f64( &buffer.0.buffer, diff --git a/wgpu/src/backend/direct.rs b/wgpu/src/backend/wgpu_core.rs similarity index 99% rename from wgpu/src/backend/direct.rs rename to wgpu/src/backend/wgpu_core.rs index 060558719d..c141eddb67 100644 --- a/wgpu/src/backend/direct.rs +++ b/wgpu/src/backend/wgpu_core.rs @@ -29,21 +29,23 @@ use wgt::WasmNotSendSync; const LABEL: &str = "label"; -pub struct Context(wgc::global::Global); +pub struct ContextWgpuCore(wgc::global::Global); -impl Drop for Context { +impl Drop for ContextWgpuCore { fn drop(&mut self) { //nothing } } -impl fmt::Debug for Context { +impl fmt::Debug for ContextWgpuCore { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Context").field("type", &"Native").finish() + f.debug_struct("ContextWgpuCore") + .field("type", &"Native") + .finish() } } -impl Context { +impl ContextWgpuCore { pub unsafe fn from_hal_instance(hal_instance: A::Instance) -> Self { Self(unsafe { wgc::global::Global::from_hal_instance::( @@ -443,7 +445,7 @@ pub struct CommandEncoder { open: bool, } -impl crate::Context for Context { +impl crate::Context for ContextWgpuCore { type AdapterId = wgc::id::AdapterId; type AdapterData = (); type DeviceId = wgc::id::DeviceId; diff --git a/wgpu/src/context.rs b/wgpu/src/context.rs index cec51a57ba..ba1e52ef71 100644 --- a/wgpu/src/context.rs +++ b/wgpu/src/context.rs @@ -320,13 +320,6 @@ pub trait Context: Debug + WasmNotSendSync + Sized { buffer_data: &Self::BufferData, sub_range: Range, ) -> Box; - #[cfg(webgpu)] - fn buffer_get_mapped_range_as_array_buffer( - &self, - buffer: &Self::BufferId, - buffer_data: &Self::BufferData, - sub_range: Range, - ) -> js_sys::ArrayBuffer; fn buffer_unmap(&self, buffer: &Self::BufferId, buffer_data: &Self::BufferData); fn texture_create_view( &self, @@ -1037,6 +1030,7 @@ impl ObjectId { global_id: None, }; + #[allow(dead_code)] pub fn new(id: NonZeroU64, global_id: NonZeroU64) -> Self { Self { id: Some(id), @@ -1346,13 +1340,6 @@ pub(crate) trait DynContext: Debug + WasmNotSendSync { buffer_data: &crate::Data, sub_range: Range, ) -> Box; - #[cfg(webgpu)] - fn buffer_get_mapped_range_as_array_buffer( - &self, - buffer: &ObjectId, - buffer_data: &crate::Data, - sub_range: Range, - ) -> js_sys::ArrayBuffer; fn buffer_unmap(&self, buffer: &ObjectId, buffer_data: &crate::Data); fn texture_create_view( &self, @@ -2465,18 +2452,6 @@ where Context::buffer_get_mapped_range(self, &buffer, buffer_data, sub_range) } - #[cfg(webgpu)] - fn buffer_get_mapped_range_as_array_buffer( - &self, - buffer: &ObjectId, - buffer_data: &crate::Data, - sub_range: Range, - ) -> js_sys::ArrayBuffer { - let buffer = ::from(*buffer); - let buffer_data = downcast_ref(buffer_data); - Context::buffer_get_mapped_range_as_array_buffer(self, &buffer, buffer_data, sub_range) - } - fn buffer_unmap(&self, buffer: &ObjectId, buffer_data: &crate::Data) { let buffer = ::from(*buffer); let buffer_data = downcast_ref(buffer_data); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 89fbb23a48..5a518d3c8a 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -16,11 +16,10 @@ //! - **`angle`** --- Enables the GLES backend via [ANGLE](https://github.com/google/angle) on macOS //! using. //! - **`vulkan-portability`** --- Enables the Vulkan backend on macOS & iOS. +//! - **`webgpu`** --- Enables the WebGPU backend on Wasm. Disabled when targeting `emscripten`. //! - **`webgl`** --- Enables the GLES backend on Wasm //! //! - ⚠️ WIP: Currently will also enable GLES dependencies on any other targets. -//! - ⚠️ WIP: This automatically disables use of WebGPU. See -//! [#2804](https://github.com/gfx-rs/wgpu/issues/3514). //! //! ### Shading language support //! @@ -73,7 +72,10 @@ use std::{ thread, }; -use context::{Context, DeviceRequest, DynContext, ObjectId}; +#[allow(unused_imports)] // Unused if all backends are disabled. +use context::Context; + +use context::{DeviceRequest, DynContext, ObjectId}; use parking_lot::Mutex; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; @@ -1722,8 +1724,6 @@ impl Instance { /// See /// * Windows: always enables Vulkan and GLES with no way to opt out /// * Linux: always enables Vulkan and GLES with no way to opt out - /// * Web: either targets WebGPU backend or, if `webgl` enabled, WebGL - /// * TODO: Support both WebGPU and WebGL at the same time, see pub const fn any_backend_feature_enabled() -> bool { // Method intentionally kept verbose to keep it a bit easier to follow! @@ -1733,6 +1733,9 @@ impl Instance { cfg!(feature = "metal") || cfg!(feature = "vulkan-portability") || cfg!(feature = "angle") + // On the web, either WebGPU or WebGL must be enabled. + } else if cfg!(target_arch = "wasm32") { + cfg!(feature = "webgpu") || cfg!(feature = "webgl") } else { true } @@ -1745,11 +1748,21 @@ impl Instance { /// - `instance_desc` - Has fields for which [backends][Backends] wgpu will choose /// during instantiation, and which [DX12 shader compiler][Dx12Compiler] wgpu will use. /// + /// [`Backends::BROWSER_WEBGPU`] takes a special role: + /// If it is set and WebGPU support is detected, this instance will *only* be able to create + /// WebGPU adapters. If you instead want to force use of WebGL, either + /// disable the `webgpu` compile-time feature or do add the [`Backends::BROWSER_WEBGPU`] + /// flag to the the `instance_desc`'s `backends` field. + /// If it is set and WebGPU support is *not* detected, the instance will use wgpu-core + /// to create adapters. Meaning that if the `webgl` feature is enabled, it is able to create + /// a WebGL adapter. + /// /// # Panics /// /// If no backend feature for the active target platform is enabled, /// this method will panic, see [`Instance::any_backend_feature_enabled()`]. - pub fn new(instance_desc: InstanceDescriptor) -> Self { + #[allow(unreachable_code)] + pub fn new(_instance_desc: InstanceDescriptor) -> Self { if !Self::any_backend_feature_enabled() { panic!( "No wgpu backend feature that is implemented for the target platform was enabled. \ @@ -1757,9 +1770,25 @@ impl Instance { ); } - Self { - context: Arc::from(crate::backend::Context::init(instance_desc)), + #[cfg(webgpu)] + if _instance_desc.backends.contains(Backends::BROWSER_WEBGPU) + && crate::backend::get_browser_gpu_property().map_or(false, |gpu| !gpu.is_undefined()) + { + return Self { + context: Arc::from(crate::backend::ContextWebGpu::init(_instance_desc)), + }; + } + + #[cfg(wgpu_core)] + { + return Self { + context: Arc::from(crate::backend::ContextWgpuCore::init(_instance_desc)), + }; } + + unreachable!( + "Earlier check of `any_backend_feature_enabled` should have prevented getting here!" + ); } /// Create an new instance of wgpu from a wgpu-hal instance. @@ -1771,11 +1800,11 @@ impl Instance { /// # Safety /// /// Refer to the creation of wgpu-hal Instance for every backend. - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn from_hal(hal_instance: A::Instance) -> Self { Self { context: Arc::new(unsafe { - crate::backend::Context::from_hal_instance::(hal_instance) + crate::backend::ContextWgpuCore::from_hal_instance::(hal_instance) }), } } @@ -1790,15 +1819,13 @@ impl Instance { /// - The raw instance handle returned must not be manually destroyed. /// /// [`Instance`]: hal::Api::Instance - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn as_hal(&self) -> Option<&A::Instance> { - unsafe { - self.context - .as_any() - .downcast_ref::() - .unwrap() - .instance_as_hal::() - } + self.context + .as_any() + // If we don't have a wgpu-core instance, we don't have a hal instance either. + .downcast_ref::() + .and_then(|ctx| unsafe { ctx.instance_as_hal::() }) } /// Create an new instance of wgpu from a wgpu-core instance. @@ -1810,34 +1837,40 @@ impl Instance { /// # Safety /// /// Refer to the creation of wgpu-core Instance. - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn from_core(core_instance: wgc::instance::Instance) -> Self { Self { context: Arc::new(unsafe { - crate::backend::Context::from_core_instance(core_instance) + crate::backend::ContextWgpuCore::from_core_instance(core_instance) }), } } /// Retrieves all available [`Adapter`]s that match the given [`Backends`]. /// + /// Always returns an empty vector if the instance decided upon creation to + /// target WebGPU since adapter creation is always async on WebGPU. + /// /// # Arguments /// /// - `backends` - Backends from which to enumerate adapters. - #[cfg(not(webgpu))] - pub fn enumerate_adapters(&self, backends: Backends) -> impl ExactSizeIterator { + #[cfg(wgpu_core)] + pub fn enumerate_adapters(&self, backends: Backends) -> Vec { let context = Arc::clone(&self.context); self.context .as_any() - .downcast_ref::() - .unwrap() - .enumerate_adapters(backends) - .into_iter() - .map(move |id| crate::Adapter { - context: Arc::clone(&context), - id: ObjectId::from(id), - data: Box::new(()), + .downcast_ref::() + .map(|ctx| { + ctx.enumerate_adapters(backends) + .into_iter() + .map(move |id| crate::Adapter { + context: Arc::clone(&context), + id: ObjectId::from(id), + data: Box::new(()), + }) + .collect() }) + .unwrap_or_default() } /// Retrieves an [`Adapter`] which matches the given [`RequestAdapterOptions`]. @@ -1863,7 +1896,7 @@ impl Instance { /// # Safety /// /// `hal_adapter` must be created from this instance internal handle. - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn create_adapter_from_hal( &self, hal_adapter: hal::ExposedAdapter, @@ -1872,7 +1905,7 @@ impl Instance { let id = unsafe { context .as_any() - .downcast_ref::() + .downcast_ref::() .unwrap() .create_adapter_from_hal(hal_adapter) .into() @@ -2000,13 +2033,15 @@ impl Instance { } /// Generates memory report. - #[cfg(not(webgpu))] - pub fn generate_report(&self) -> wgc::global::GlobalReport { + /// + /// Returns `None` if the feature is not supported by the backend + /// which happens only when WebGPU is pre-selected by the instance creation. + #[cfg(wgpu_core)] + pub fn generate_report(&self) -> Option { self.context .as_any() - .downcast_ref::() - .unwrap() - .generate_report() + .downcast_ref::() + .map(|ctx| ctx.generate_report()) } } @@ -2071,7 +2106,7 @@ impl Adapter { /// /// - `hal_device` must be created from this adapter internal handle. /// - `desc.features` must be a subset of `hal_device` features. - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn create_device_from_hal( &self, hal_device: hal::OpenDevice, @@ -2082,7 +2117,9 @@ impl Adapter { unsafe { self.context .as_any() - .downcast_ref::() + .downcast_ref::() + // Part of the safety requirements is that the device was generated from the same adapter. + // Therefore, unwrap is fine here since only WgpuCoreContext based adapters have the ability to create hal devices. .unwrap() .create_device_from_hal(&self.id.into(), hal_device, desc, trace_path) } @@ -2121,17 +2158,19 @@ impl Adapter { /// - The raw handle passed to the callback must not be manually destroyed. /// /// [`A::Adapter`]: hal::Api::Adapter - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn as_hal) -> R, R>( &self, hal_adapter_callback: F, ) -> R { - unsafe { - self.context - .as_any() - .downcast_ref::() - .unwrap() - .adapter_as_hal::(self.id.into(), hal_adapter_callback) + if let Some(ctx) = self + .context + .as_any() + .downcast_ref::() + { + unsafe { ctx.adapter_as_hal::(self.id.into(), hal_adapter_callback) } + } else { + hal_adapter_callback(None) } } @@ -2463,7 +2502,7 @@ impl Device { /// - `hal_texture` must be created from this device internal handle /// - `hal_texture` must be created respecting `desc` /// - `hal_texture` must be initialized - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn create_texture_from_hal( &self, hal_texture: A::Texture, @@ -2472,7 +2511,9 @@ impl Device { let texture = unsafe { self.context .as_any() - .downcast_ref::() + .downcast_ref::() + // Part of the safety requirements is that the texture was generated from the same hal device. + // Therefore, unwrap is fine here since only WgpuCoreContext has the ability to create hal textures. .unwrap() .create_texture_from_hal::( hal_texture, @@ -2500,7 +2541,7 @@ impl Device { /// - `hal_buffer` must be created from this device internal handle /// - `hal_buffer` must be created respecting `desc` /// - `hal_buffer` must be initialized - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn create_buffer_from_hal( &self, hal_buffer: A::Buffer, @@ -2514,7 +2555,9 @@ impl Device { let (id, buffer) = unsafe { self.context .as_any() - .downcast_ref::() + .downcast_ref::() + // Part of the safety requirements is that the buffer was generated from the same hal device. + // Therefore, unwrap is fine here since only WgpuCoreContext has the ability to create hal buffers. .unwrap() .create_buffer_from_hal::( hal_buffer, @@ -2604,21 +2647,20 @@ impl Device { /// - The raw handle passed to the callback must not be manually destroyed. /// /// [`A::Device`]: hal::Api::Device - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn as_hal) -> R, R>( &self, hal_device_callback: F, - ) -> R { - unsafe { - self.context - .as_any() - .downcast_ref::() - .unwrap() - .device_as_hal::( + ) -> Option { + self.context + .as_any() + .downcast_ref::() + .map(|ctx| unsafe { + ctx.device_as_hal::( self.data.as_ref().downcast_ref().unwrap(), hal_device_callback, ) - } + }) } /// Destroy this device. @@ -2657,14 +2699,14 @@ pub struct RequestDeviceError { enum RequestDeviceErrorKind { /// Error from [`wgpu_core`]. // must match dependency cfg - #[cfg(not(webgpu))] - Core(core::instance::RequestDeviceError), + #[cfg(wgpu_core)] + Core(wgc::instance::RequestDeviceError), /// Error from web API that was called by `wgpu` to request a device. /// /// (This is currently never used by the webgl backend, but it could be.) #[cfg(webgpu)] - Web(wasm_bindgen::JsValue), + WebGpu(wasm_bindgen::JsValue), } #[cfg(send_sync)] @@ -2676,15 +2718,17 @@ unsafe impl Sync for RequestDeviceErrorKind {} static_assertions::assert_impl_all!(RequestDeviceError: Send, Sync); impl fmt::Display for RequestDeviceError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.inner { - #[cfg(not(webgpu))] - RequestDeviceErrorKind::Core(error) => error.fmt(f), + #[cfg(wgpu_core)] + RequestDeviceErrorKind::Core(error) => error.fmt(_f), #[cfg(webgpu)] - RequestDeviceErrorKind::Web(error_js_value) => { + RequestDeviceErrorKind::WebGpu(error_js_value) => { // wasm-bindgen provides a reasonable error stringification via `Debug` impl - write!(f, "{error_js_value:?}") + write!(_f, "{error_js_value:?}") } + #[cfg(not(any(webgpu, wgpu_core)))] + _ => unimplemented!("unknown `RequestDeviceErrorKind`"), } } } @@ -2692,17 +2736,19 @@ impl fmt::Display for RequestDeviceError { impl error::Error for RequestDeviceError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match &self.inner { - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] RequestDeviceErrorKind::Core(error) => error.source(), #[cfg(webgpu)] - RequestDeviceErrorKind::Web(_) => None, + RequestDeviceErrorKind::WebGpu(_) => None, + #[cfg(not(any(webgpu, wgpu_core)))] + _ => unimplemented!("unknown `RequestDeviceErrorKind`"), } } } -#[cfg(not(webgpu))] -impl From for RequestDeviceError { - fn from(error: core::instance::RequestDeviceError) -> Self { +#[cfg(wgpu_core)] +impl From for RequestDeviceError { + fn from(error: wgc::instance::RequestDeviceError) -> Self { Self { inner: RequestDeviceErrorKind::Core(error), } @@ -2718,7 +2764,7 @@ pub struct CreateSurfaceError { #[derive(Clone, Debug)] enum CreateSurfaceErrorKind { /// Error from [`wgpu_hal`]. - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] Hal(hal::InstanceError), /// Error from WebGPU surface creation. @@ -2734,7 +2780,7 @@ static_assertions::assert_impl_all!(CreateSurfaceError: Send, Sync); impl fmt::Display for CreateSurfaceError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.inner { - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] CreateSurfaceErrorKind::Hal(e) => e.fmt(f), CreateSurfaceErrorKind::Web(e) => e.fmt(f), CreateSurfaceErrorKind::RawHandle(e) => e.fmt(f), @@ -2745,7 +2791,7 @@ impl fmt::Display for CreateSurfaceError { impl error::Error for CreateSurfaceError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match &self.inner { - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] CreateSurfaceErrorKind::Hal(e) => e.source(), CreateSurfaceErrorKind::Web(_) => None, CreateSurfaceErrorKind::RawHandle(e) => e.source(), @@ -2753,7 +2799,7 @@ impl error::Error for CreateSurfaceError { } } -#[cfg(not(webgpu))] +#[cfg(wgpu_core)] impl From for CreateSurfaceError { fn from(e: hal::InstanceError) -> Self { Self { @@ -2986,20 +3032,24 @@ impl<'a> BufferSlice<'a> { } /// Synchronously and immediately map a buffer for reading. If the buffer is not immediately mappable - /// through [`BufferDescriptor::mapped_at_creation`] or [`BufferSlice::map_async`], will panic. + /// through [`BufferDescriptor::mapped_at_creation`] or [`BufferSlice::map_async`], will fail. + /// + /// This is useful when targeting WebGPU and you want to pass mapped data directly to js. + /// Unlike `get_mapped_range` which unconditionally copies mapped data into the wasm heap, + /// this function directly hands you the ArrayBuffer that we mapped the data into in js. /// - /// This is useful in wasm builds when you want to pass mapped data directly to js. Unlike `get_mapped_range` - /// which unconditionally copies mapped data into the wasm heap, this function directly hands you the - /// ArrayBuffer that we mapped the data into in js. + /// This is only available on WebGPU, on any other backends this will return `None`. #[cfg(webgpu)] - pub fn get_mapped_range_as_array_buffer(&self) -> js_sys::ArrayBuffer { - let end = self.buffer.map_context.lock().add(self.offset, self.size); - DynContext::buffer_get_mapped_range_as_array_buffer( - &*self.buffer.context, - &self.buffer.id, - self.buffer.data.as_ref(), - self.offset..end, - ) + pub fn get_mapped_range_as_array_buffer(&self) -> Option { + self.buffer + .context + .as_any() + .downcast_ref::() + .map(|ctx| { + let buffer_data = crate::context::downcast_ref(self.buffer.data.as_ref()); + let end = self.buffer.map_context.lock().add(self.offset, self.size); + ctx.buffer_get_mapped_range_as_array_buffer(buffer_data, self.offset..end) + }) } /// Synchronously and immediately map a buffer for writing. If the buffer is not immediately mappable @@ -3035,18 +3085,21 @@ impl Texture { /// # Safety /// /// - The raw handle obtained from the hal Texture must not be manually destroyed - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn as_hal)>( &self, hal_texture_callback: F, ) { let texture = self.data.as_ref().downcast_ref().unwrap(); - unsafe { - self.context - .as_any() - .downcast_ref::() - .unwrap() - .texture_as_hal::(texture, hal_texture_callback) + + if let Some(ctx) = self + .context + .as_any() + .downcast_ref::() + { + unsafe { ctx.texture_as_hal::(texture, hal_texture_callback) } + } else { + hal_texture_callback(None) } } @@ -4783,18 +4836,20 @@ impl Surface<'_> { /// # Safety /// /// - The raw handle obtained from the hal Surface must not be manually destroyed - #[cfg(not(webgpu))] + #[cfg(wgpu_core)] pub unsafe fn as_hal) -> R, R>( &mut self, hal_surface_callback: F, - ) -> R { - unsafe { - self.context - .as_any() - .downcast_ref::() - .unwrap() - .surface_as_hal::(self.data.downcast_ref().unwrap(), hal_surface_callback) - } + ) -> Option { + self.context + .as_any() + .downcast_ref::() + .map(|ctx| unsafe { + ctx.surface_as_hal::( + self.data.downcast_ref().unwrap(), + hal_surface_callback, + ) + }) } } diff --git a/xtask/src/run_wasm.rs b/xtask/src/run_wasm.rs index d53d33a2c1..280ef82775 100644 --- a/xtask/src/run_wasm.rs +++ b/xtask/src/run_wasm.rs @@ -61,7 +61,7 @@ pub(crate) fn run_wasm(mut args: Arguments) -> Result<(), anyhow::Error> { xshell::cmd!( shell, - "cargo build --target wasm32-unknown-unknown --bin wgpu-examples --features webgl {release_flag...}" + "cargo build --target wasm32-unknown-unknown --bin wgpu-examples --no-default-features --features wgsl,webgl {release_flag...}" ) .args(&cargo_args) .quiet()