Properly handle the case where Navigator.gpu is undefined and WebGPU is the only compiled backend (#6197)

* Properly handle the case where `Navigator.gpu` is undefined and WebGPU is the only compiled backend.

Previously, `Instance::request_adapter` would invoke a wasm binding with an undefined arg0,
thus crashing the program. Now it will cleanly return `None` instead.

Fixes #6196.

* Fix typo in `Instance::new` doc comment.
* Add note to CHANGELOG.md
* Introduce `DefinedNonNullJsValue` type.
* Assert definedness of self.gpu in surface_get_capabilities.
* Use DefinedNonNullJsValue in signature of get_browser_gpu_property().
* Clarify meaning of gpu field with a comment.
This commit is contained in:
Ben Reeves 2024-09-15 03:07:40 -05:00 committed by GitHub
parent d79ebc4db3
commit 2fac5e983e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 126 additions and 24 deletions

View File

@ -88,6 +88,7 @@ By @bradwerth [#6216](https://github.com/gfx-rs/wgpu/pull/6216).
### Bug Fixes ### Bug Fixes
- Fix incorrect hlsl image output type conversion. By @atlv24 in [#6123](https://github.com/gfx-rs/wgpu/pull/6123) - Fix incorrect hlsl image output type conversion. By @atlv24 in [#6123](https://github.com/gfx-rs/wgpu/pull/6123)
- Fix JS `TypeError` exception in `Instance::request_adapter` when browser doesn't support WebGPU but `wgpu` not compiled with `webgl` support. By @bgr360 in [#6197](https://github.com/gfx-rs/wgpu/pull/6197).
#### Naga #### Naga

View File

@ -95,7 +95,7 @@ impl Instance {
/// [`Backends::BROWSER_WEBGPU`] takes a special role: /// [`Backends::BROWSER_WEBGPU`] takes a special role:
/// If it is set and WebGPU support is detected, this instance will *only* be able to create /// 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 /// 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`] /// disable the `webgpu` compile-time feature or don't add the [`Backends::BROWSER_WEBGPU`]
/// flag to the the `instance_desc`'s `backends` field. /// 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 /// 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 /// to create adapters. Meaning that if the `webgl` feature is enabled, it is able to create
@ -118,8 +118,9 @@ impl Instance {
{ {
let is_only_available_backend = !cfg!(wgpu_core); let is_only_available_backend = !cfg!(wgpu_core);
let requested_webgpu = _instance_desc.backends.contains(Backends::BROWSER_WEBGPU); let requested_webgpu = _instance_desc.backends.contains(Backends::BROWSER_WEBGPU);
let support_webgpu = let support_webgpu = crate::backend::get_browser_gpu_property()
crate::backend::get_browser_gpu_property().map_or(false, |gpu| !gpu.is_undefined()); .map(|maybe_gpu| maybe_gpu.is_some())
.unwrap_or(false);
if is_only_available_backend || (requested_webgpu && support_webgpu) { if is_only_available_backend || (requested_webgpu && support_webgpu) {
return Self { return Self {

View File

@ -1,5 +1,6 @@
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
mod defined_non_null_js_value;
mod ext_bindings; mod ext_bindings;
mod webgpu_sys; mod webgpu_sys;
@ -22,6 +23,8 @@ use crate::{
CompilationInfo, SurfaceTargetUnsafe, UncapturedErrorHandler, CompilationInfo, SurfaceTargetUnsafe, UncapturedErrorHandler,
}; };
use defined_non_null_js_value::DefinedNonNullJsValue;
// We need to make a wrapper for some of the handle types returned by the web backend to make them // We need to make a wrapper for some of the handle types returned by the web backend to make them
// implement `Send` and `Sync` to match native. // implement `Send` and `Sync` to match native.
// //
@ -38,7 +41,10 @@ unsafe impl<T> Send for Sendable<T> {}
#[cfg(send_sync)] #[cfg(send_sync)]
unsafe impl<T> Sync for Sendable<T> {} unsafe impl<T> Sync for Sendable<T> {}
pub(crate) struct ContextWebGpu(webgpu_sys::Gpu); pub(crate) struct ContextWebGpu {
/// `None` if browser does not advertise support for WebGPU.
gpu: Option<DefinedNonNullJsValue<webgpu_sys::Gpu>>,
}
#[cfg(send_sync)] #[cfg(send_sync)]
unsafe impl Send for ContextWebGpu {} unsafe impl Send for ContextWebGpu {}
#[cfg(send_sync)] #[cfg(send_sync)]
@ -189,6 +195,36 @@ impl<F, M> MakeSendFuture<F, M> {
#[cfg(send_sync)] #[cfg(send_sync)]
unsafe impl<F, M> Send for MakeSendFuture<F, M> {} unsafe impl<F, M> Send for MakeSendFuture<F, M> {}
/// Wraps a future that returns `Option<T>` and adds the ability to immediately
/// return None.
pub(crate) struct OptionFuture<F>(Option<F>);
impl<F: Future<Output = Option<T>>, T> Future for OptionFuture<F> {
type Output = Option<T>;
fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Self::Output> {
// This is safe because we have no Drop implementation to violate the Pin requirements and
// do not provide any means of moving the inner future.
unsafe {
let this = self.get_unchecked_mut();
match &mut this.0 {
Some(future) => Pin::new_unchecked(future).poll(cx),
None => task::Poll::Ready(None),
}
}
}
}
impl<F> OptionFuture<F> {
fn some(future: F) -> Self {
Self(Some(future))
}
fn none() -> Self {
Self(None)
}
}
fn map_texture_format(texture_format: wgt::TextureFormat) -> webgpu_sys::GpuTextureFormat { fn map_texture_format(texture_format: wgt::TextureFormat) -> webgpu_sys::GpuTextureFormat {
use webgpu_sys::GpuTextureFormat as tf; use webgpu_sys::GpuTextureFormat as tf;
use wgt::TextureFormat; use wgt::TextureFormat;
@ -1046,27 +1082,34 @@ pub enum Canvas {
Offscreen(web_sys::OffscreenCanvas), Offscreen(web_sys::OffscreenCanvas),
} }
/// Returns the browsers gpu object or `None` if the current context is neither the main thread nor a dedicated worker. #[derive(Debug, Clone, Copy)]
pub struct BrowserGpuPropertyInaccessible;
/// Returns the browser's gpu object or `Err(BrowserGpuPropertyInaccessible)` 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`). /// If WebGPU is not supported, the Gpu property is `undefined`, and so this
/// function will return `Ok(None)`.
/// ///
/// See: /// See:
/// * <https://developer.mozilla.org/en-US/docs/Web/API/Navigator/gpu> /// * <https://developer.mozilla.org/en-US/docs/Web/API/Navigator/gpu>
/// * <https://developer.mozilla.org/en-US/docs/Web/API/WorkerNavigator/gpu> /// * <https://developer.mozilla.org/en-US/docs/Web/API/WorkerNavigator/gpu>
pub fn get_browser_gpu_property() -> Option<webgpu_sys::Gpu> { pub fn get_browser_gpu_property(
) -> Result<Option<DefinedNonNullJsValue<webgpu_sys::Gpu>>, BrowserGpuPropertyInaccessible> {
let global: Global = js_sys::global().unchecked_into(); let global: Global = js_sys::global().unchecked_into();
if !global.window().is_undefined() { let maybe_undefined_gpu: webgpu_sys::Gpu = if !global.window().is_undefined() {
let navigator = global.unchecked_into::<web_sys::Window>().navigator(); let navigator = global.unchecked_into::<web_sys::Window>().navigator();
Some(ext_bindings::NavigatorGpu::gpu(&navigator)) ext_bindings::NavigatorGpu::gpu(&navigator)
} else if !global.worker().is_undefined() { } else if !global.worker().is_undefined() {
let navigator = global let navigator = global
.unchecked_into::<web_sys::WorkerGlobalScope>() .unchecked_into::<web_sys::WorkerGlobalScope>()
.navigator(); .navigator();
Some(ext_bindings::NavigatorGpu::gpu(&navigator)) ext_bindings::NavigatorGpu::gpu(&navigator)
} else { } else {
None return Err(BrowserGpuPropertyInaccessible);
} };
Ok(DefinedNonNullJsValue::new(maybe_undefined_gpu))
} }
impl crate::context::Context for ContextWebGpu { impl crate::context::Context for ContextWebGpu {
@ -1096,9 +1139,11 @@ impl crate::context::Context for ContextWebGpu {
type SubmissionIndexData = (); type SubmissionIndexData = ();
type PipelineCacheData = (); type PipelineCacheData = ();
type RequestAdapterFuture = MakeSendFuture< type RequestAdapterFuture = OptionFuture<
wasm_bindgen_futures::JsFuture, MakeSendFuture<
fn(JsFutureResult) -> Option<Self::AdapterData>, wasm_bindgen_futures::JsFuture,
fn(JsFutureResult) -> Option<Self::AdapterData>,
>,
>; >;
type RequestDeviceFuture = MakeSendFuture< type RequestDeviceFuture = MakeSendFuture<
wasm_bindgen_futures::JsFuture, wasm_bindgen_futures::JsFuture,
@ -1115,12 +1160,13 @@ impl crate::context::Context for ContextWebGpu {
>; >;
fn init(_instance_desc: wgt::InstanceDescriptor) -> Self { fn init(_instance_desc: wgt::InstanceDescriptor) -> Self {
let Some(gpu) = get_browser_gpu_property() else { let Ok(gpu) = get_browser_gpu_property() else {
panic!( panic!(
"Accessing the GPU is only supported on the main thread or from a dedicated worker" "Accessing the GPU is only supported on the main thread or from a dedicated worker"
); );
}; };
ContextWebGpu(gpu)
ContextWebGpu { gpu }
} }
unsafe fn instance_create_surface( unsafe fn instance_create_surface(
@ -1190,12 +1236,16 @@ impl crate::context::Context for ContextWebGpu {
if let Some(mapped_pref) = mapped_power_preference { if let Some(mapped_pref) = mapped_power_preference {
mapped_options.power_preference(mapped_pref); mapped_options.power_preference(mapped_pref);
} }
let adapter_promise = self.0.request_adapter_with_options(&mapped_options); if let Some(gpu) = &self.gpu {
let adapter_promise = gpu.request_adapter_with_options(&mapped_options);
MakeSendFuture::new( OptionFuture::some(MakeSendFuture::new(
wasm_bindgen_futures::JsFuture::from(adapter_promise), wasm_bindgen_futures::JsFuture::from(adapter_promise),
future_request_adapter, future_request_adapter,
) ))
} else {
// Gpu is undefined; WebGPU is not supported in this browser.
OptionFuture::none()
}
} }
fn adapter_request_device( fn adapter_request_device(
@ -1316,7 +1366,11 @@ impl crate::context::Context for ContextWebGpu {
let mut mapped_formats = formats.iter().map(|format| map_texture_format(*format)); let mut mapped_formats = formats.iter().map(|format| map_texture_format(*format));
// Preferred canvas format will only be either "rgba8unorm" or "bgra8unorm". // Preferred canvas format will only be either "rgba8unorm" or "bgra8unorm".
// https://www.w3.org/TR/webgpu/#dom-gpu-getpreferredcanvasformat // https://www.w3.org/TR/webgpu/#dom-gpu-getpreferredcanvasformat
let preferred_format = self.0.get_preferred_canvas_format(); let gpu = self
.gpu
.as_ref()
.expect("Caller could not have created an adapter if gpu is undefined.");
let preferred_format = gpu.get_preferred_canvas_format();
if let Some(index) = mapped_formats.position(|format| format == preferred_format) { if let Some(index) = mapped_formats.position(|format| format == preferred_format) {
formats.swap(0, index); formats.swap(0, index);
} }

View File

@ -0,0 +1,46 @@
use std::ops::{Deref, DerefMut};
use wasm_bindgen::JsValue;
/// Derefs to a [`JsValue`] that's known not to be `undefined` or `null`.
#[derive(Debug)]
pub struct DefinedNonNullJsValue<T>(T);
impl<T> DefinedNonNullJsValue<T>
where
T: AsRef<JsValue>,
{
pub fn new(value: T) -> Option<Self> {
if value.as_ref().is_undefined() || value.as_ref().is_null() {
None
} else {
Some(Self(value))
}
}
}
impl<T> Deref for DefinedNonNullJsValue<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for DefinedNonNullJsValue<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T> AsRef<T> for DefinedNonNullJsValue<T> {
fn as_ref(&self) -> &T {
&self.0
}
}
impl<T> AsMut<T> for DefinedNonNullJsValue<T> {
fn as_mut(&mut self) -> &mut T {
&mut self.0
}
}