Skip to content

Commit 2209463

Browse files
authored
Return an error instead of panicking when canvas context is unavailable (#3052)
* Low-level error handling in canvas context creation (for WebGPU and WebGL 2). Part of fixing #3017. This commit changes the handling of `web_sys` errors and nulls from `expect()` to returning `Err`, but it doesn't actually affect the public API — that will be done in the next commit. * Breaking: Change type of `create_surface()` functions to expose canvas errors. Part of fixing #3017.
1 parent 7960c5e commit 2209463

File tree

10 files changed

+193
-81
lines changed

10 files changed

+193
-81
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ Additionally `Surface::get_default_config` now returns an Option and returns Non
6363
+ let config = surface.get_default_config(&adapter).expect("Surface unsupported by adapter");
6464
```
6565

66+
#### Fallible surface creation
67+
68+
`Instance::create_surface()` now returns `Result<Surface, CreateSurfaceError>` instead of `Surface`. This allows an error to be returned instead of panicking if the given window is a HTML canvas and obtaining a WebGPU or WebGL 2 context fails. (No other platforms currently report any errors through this path.) By @kpreid in [#3052](https://github.com/gfx-rs/wgpu/pull/3052/)
69+
6670
### Changes
6771

6872
#### General

wgpu-core/src/instance.rs

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -502,45 +502,53 @@ impl<G: GlobalIdentityHandlerFactory> Global<G> {
502502
&self,
503503
canvas: &web_sys::HtmlCanvasElement,
504504
id_in: Input<G, SurfaceId>,
505-
) -> SurfaceId {
505+
) -> Result<SurfaceId, hal::InstanceError> {
506506
profiling::scope!("Instance::create_surface_webgl_canvas");
507507

508508
let surface = Surface {
509509
presentation: None,
510-
gl: self.instance.gl.as_ref().map(|inst| HalSurface {
511-
raw: {
512-
inst.create_surface_from_canvas(canvas)
513-
.expect("Create surface from canvas")
514-
},
515-
}),
510+
gl: self
511+
.instance
512+
.gl
513+
.as_ref()
514+
.map(|inst| {
515+
Ok(HalSurface {
516+
raw: inst.create_surface_from_canvas(canvas)?,
517+
})
518+
})
519+
.transpose()?,
516520
};
517521

518522
let mut token = Token::root();
519523
let id = self.surfaces.prepare(id_in).assign(surface, &mut token);
520-
id.0
524+
Ok(id.0)
521525
}
522526

523527
#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
524528
pub fn create_surface_webgl_offscreen_canvas(
525529
&self,
526530
canvas: &web_sys::OffscreenCanvas,
527531
id_in: Input<G, SurfaceId>,
528-
) -> SurfaceId {
532+
) -> Result<SurfaceId, hal::InstanceError> {
529533
profiling::scope!("Instance::create_surface_webgl_offscreen_canvas");
530534

531535
let surface = Surface {
532536
presentation: None,
533-
gl: self.instance.gl.as_ref().map(|inst| HalSurface {
534-
raw: {
535-
inst.create_surface_from_offscreen_canvas(canvas)
536-
.expect("Create surface from offscreen canvas")
537-
},
538-
}),
537+
gl: self
538+
.instance
539+
.gl
540+
.as_ref()
541+
.map(|inst| {
542+
Ok(HalSurface {
543+
raw: inst.create_surface_from_offscreen_canvas(canvas)?,
544+
})
545+
})
546+
.transpose()?,
539547
};
540548

541549
let mut token = Token::root();
542550
let id = self.surfaces.prepare(id_in).assign(surface, &mut token);
543-
id.0
551+
Ok(id.0)
544552
}
545553

546554
#[cfg(dx12)]

wgpu-hal/src/gles/web.rs

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,32 +33,53 @@ impl Instance {
3333
&self,
3434
canvas: &web_sys::HtmlCanvasElement,
3535
) -> Result<Surface, crate::InstanceError> {
36-
let webgl2_context = canvas
37-
.get_context_with_context_options("webgl2", &Self::create_context_options())
38-
.expect("Cannot create WebGL2 context")
39-
.and_then(|context| context.dyn_into::<web_sys::WebGl2RenderingContext>().ok())
40-
.expect("Cannot convert into WebGL2 context");
41-
42-
*self.webgl2_context.lock() = Some(webgl2_context.clone());
43-
44-
Ok(Surface {
45-
webgl2_context,
46-
srgb_present_program: None,
47-
swapchain: None,
48-
texture: None,
49-
presentable: true,
50-
})
36+
self.create_surface_from_context(
37+
canvas.get_context_with_context_options("webgl2", &Self::create_context_options()),
38+
)
5139
}
5240

5341
pub fn create_surface_from_offscreen_canvas(
5442
&self,
5543
canvas: &web_sys::OffscreenCanvas,
5644
) -> Result<Surface, crate::InstanceError> {
57-
let webgl2_context = canvas
58-
.get_context_with_context_options("webgl2", &Self::create_context_options())
59-
.expect("Cannot create WebGL2 context")
60-
.and_then(|context| context.dyn_into::<web_sys::WebGl2RenderingContext>().ok())
61-
.expect("Cannot convert into WebGL2 context");
45+
self.create_surface_from_context(
46+
canvas.get_context_with_context_options("webgl2", &Self::create_context_options()),
47+
)
48+
}
49+
50+
/// Common portion of public `create_surface_from_*` functions.
51+
///
52+
/// Note: Analogous code also exists in the WebGPU backend at
53+
/// `wgpu::backend::web::Context`.
54+
fn create_surface_from_context(
55+
&self,
56+
context_result: Result<Option<js_sys::Object>, wasm_bindgen::JsValue>,
57+
) -> Result<Surface, crate::InstanceError> {
58+
let context_object: js_sys::Object = match context_result {
59+
Ok(Some(context)) => context,
60+
Ok(None) => {
61+
// <https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-getcontext-dev>
62+
// A getContext() call “returns null if contextId is not supported, or if the
63+
// canvas has already been initialized with another context type”. Additionally,
64+
// “not supported” could include “insufficient GPU resources” or “the GPU process
65+
// previously crashed”. So, we must return it as an `Err` since it could occur
66+
// for circumstances outside the application author's control.
67+
return Err(crate::InstanceError);
68+
}
69+
Err(js_error) => {
70+
// <https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-getcontext>
71+
// A thrown exception indicates misuse of the canvas state. Ideally we wouldn't
72+
// panic in this case, but for now, `InstanceError` conveys no detail, so it
73+
// is more informative to panic with a specific message.
74+
panic!("canvas.getContext() threw {js_error:?}")
75+
}
76+
};
77+
78+
// Not returning this error because it is a type error that shouldn't happen unless
79+
// the browser, JS builtin objects, or wasm bindings are misbehaving somehow.
80+
let webgl2_context: web_sys::WebGl2RenderingContext = context_object
81+
.dyn_into()
82+
.expect("canvas context is not a WebGl2RenderingContext");
6283

6384
*self.webgl2_context.lock() = Some(webgl2_context.clone());
6485

wgpu/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ wgsl = ["wgc?/wgsl"]
8383
trace = ["serde", "wgc/trace"]
8484
replay = ["serde", "wgc/replay"]
8585
angle = ["wgc/angle"]
86-
webgl = ["wgc"]
86+
webgl = ["hal", "wgc"]
8787
emscripten = ["webgl"]
8888
vulkan-portability = ["wgc/vulkan-portability"]
8989
expose-ids = []
@@ -100,9 +100,13 @@ optional = true
100100
[dependencies.wgt]
101101
workspace = true
102102

103-
[target.'cfg(any(not(target_arch = "wasm32"), target_os = "emscripten"))'.dependencies.hal]
103+
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.hal]
104104
workspace = true
105105

106+
[target.'cfg(target_arch = "wasm32")'.dependencies.hal]
107+
workspace = true
108+
optional = true
109+
106110
[dependencies]
107111
arrayvec.workspace = true
108112
log.workspace = true

wgpu/examples/framework.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ async fn setup<E: Example>(title: &str) -> Setup {
164164
let size = window.inner_size();
165165

166166
#[cfg(any(not(target_arch = "wasm32"), target_os = "emscripten"))]
167-
let surface = instance.create_surface(&window);
167+
let surface = instance.create_surface(&window).unwrap();
168168
#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
169169
let surface = {
170170
if let Some(offscreen_canvas_setup) = &offscreen_canvas_setup {
@@ -174,7 +174,8 @@ async fn setup<E: Example>(title: &str) -> Setup {
174174
} else {
175175
instance.create_surface(&window)
176176
}
177-
};
177+
}
178+
.unwrap();
178179

179180
(size, surface)
180181
};

wgpu/examples/hello-triangle/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use winit::{
88
async fn run(event_loop: EventLoop<()>, window: Window) {
99
let size = window.inner_size();
1010
let instance = wgpu::Instance::new(wgpu::Backends::all());
11-
let surface = unsafe { instance.create_surface(&window) };
11+
let surface = unsafe { instance.create_surface(&window) }.unwrap();
1212
let adapter = instance
1313
.request_adapter(&wgpu::RequestAdapterOptions {
1414
power_preference: wgpu::PowerPreference::default(),

wgpu/examples/hello-windows/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ struct Viewport {
2020

2121
impl ViewportDesc {
2222
fn new(window: Window, background: wgpu::Color, instance: &wgpu::Instance) -> Self {
23-
let surface = unsafe { instance.create_surface(&window) };
23+
let surface = unsafe { instance.create_surface(&window) }.unwrap();
2424
Self {
2525
window,
2626
background,

wgpu/src/backend/direct.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -219,24 +219,30 @@ impl Context {
219219
pub fn instance_create_surface_from_canvas(
220220
self: &Arc<Self>,
221221
canvas: &web_sys::HtmlCanvasElement,
222-
) -> Surface {
223-
let id = self.0.create_surface_webgl_canvas(canvas, ());
224-
Surface {
222+
) -> Result<Surface, crate::CreateSurfaceError> {
223+
let id = self
224+
.0
225+
.create_surface_webgl_canvas(canvas, ())
226+
.map_err(|hal::InstanceError| crate::CreateSurfaceError {})?;
227+
Ok(Surface {
225228
id,
226229
configured_device: Mutex::default(),
227-
}
230+
})
228231
}
229232

230233
#[cfg(all(target_arch = "wasm32", feature = "webgl", not(feature = "emscripten")))]
231234
pub fn instance_create_surface_from_offscreen_canvas(
232235
self: &Arc<Self>,
233236
canvas: &web_sys::OffscreenCanvas,
234-
) -> Surface {
235-
let id = self.0.create_surface_webgl_offscreen_canvas(canvas, ());
236-
Surface {
237+
) -> Result<Surface, crate::CreateSurfaceError> {
238+
let id = self
239+
.0
240+
.create_surface_webgl_offscreen_canvas(canvas, ())
241+
.map_err(|hal::InstanceError| crate::CreateSurfaceError {})?;
242+
Ok(Surface {
237243
id,
238244
configured_device: Mutex::default(),
239-
}
245+
})
240246
}
241247

242248
#[cfg(target_os = "windows")]
@@ -943,13 +949,13 @@ impl crate::Context for Context {
943949
&self,
944950
display_handle: raw_window_handle::RawDisplayHandle,
945951
window_handle: raw_window_handle::RawWindowHandle,
946-
) -> Self::SurfaceId {
947-
Surface {
952+
) -> Result<Self::SurfaceId, crate::CreateSurfaceError> {
953+
Ok(Surface {
948954
id: self
949955
.0
950956
.instance_create_surface(display_handle, window_handle, ()),
951957
configured_device: Mutex::new(None),
952-
}
958+
})
953959
}
954960

955961
fn instance_request_adapter(

wgpu/src/backend/web.rs

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,23 +1026,51 @@ impl Context {
10261026
pub fn instance_create_surface_from_canvas(
10271027
&self,
10281028
canvas: &web_sys::HtmlCanvasElement,
1029-
) -> <Self as crate::Context>::SurfaceId {
1030-
let context: wasm_bindgen::JsValue = match canvas.get_context("webgpu") {
1031-
Ok(Some(ctx)) => ctx.into(),
1032-
_ => panic!("expected to get context from canvas"),
1033-
};
1034-
create_identified(context.into())
1029+
) -> Result<<Self as crate::Context>::SurfaceId, crate::CreateSurfaceError> {
1030+
self.create_surface_from_context(canvas.get_context("webgpu"))
10351031
}
10361032

10371033
pub fn instance_create_surface_from_offscreen_canvas(
10381034
&self,
10391035
canvas: &web_sys::OffscreenCanvas,
1040-
) -> <Self as crate::Context>::SurfaceId {
1041-
let context: wasm_bindgen::JsValue = match canvas.get_context("webgpu") {
1042-
Ok(Some(ctx)) => ctx.into(),
1043-
_ => panic!("expected to get context from canvas"),
1036+
) -> Result<<Self as crate::Context>::SurfaceId, crate::CreateSurfaceError> {
1037+
self.create_surface_from_context(canvas.get_context("webgpu"))
1038+
}
1039+
1040+
/// Common portion of public `instance_create_surface_from_*` functions.
1041+
///
1042+
/// Note: Analogous code also exists in the WebGL2 backend at
1043+
/// `wgpu_hal::gles::web::Instance`.
1044+
fn create_surface_from_context(
1045+
&self,
1046+
context_result: Result<Option<js_sys::Object>, wasm_bindgen::JsValue>,
1047+
) -> Result<<Self as crate::Context>::SurfaceId, crate::CreateSurfaceError> {
1048+
let context: js_sys::Object = match context_result {
1049+
Ok(Some(context)) => context,
1050+
Ok(None) => {
1051+
// <https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-getcontext-dev>
1052+
// A getContext() call “returns null if contextId is not supported, or if the
1053+
// canvas has already been initialized with another context type”. Additionally,
1054+
// “not supported” could include “insufficient GPU resources” or “the GPU process
1055+
// previously crashed”. So, we must return it as an `Err` since it could occur
1056+
// for circumstances outside the application author's control.
1057+
return Err(crate::CreateSurfaceError {});
1058+
}
1059+
Err(js_error) => {
1060+
// <https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-getcontext>
1061+
// A thrown exception indicates misuse of the canvas state. Ideally we wouldn't
1062+
// panic in this case ... TODO
1063+
panic!("canvas.getContext() threw {js_error:?}")
1064+
}
10441065
};
1045-
create_identified(context.into())
1066+
1067+
// Not returning this error because it is a type error that shouldn't happen unless
1068+
// the browser, JS builtin objects, or wasm bindings are misbehaving somehow.
1069+
let context: web_sys::GpuCanvasContext = context
1070+
.dyn_into()
1071+
.expect("canvas context is not a GPUCanvasContext");
1072+
1073+
Ok(create_identified(context))
10461074
}
10471075

10481076
pub fn queue_copy_external_image_to_texture(
@@ -1141,7 +1169,7 @@ impl crate::Context for Context {
11411169
&self,
11421170
_display_handle: raw_window_handle::RawDisplayHandle,
11431171
window_handle: raw_window_handle::RawWindowHandle,
1144-
) -> Self::SurfaceId {
1172+
) -> Result<Self::SurfaceId, crate::CreateSurfaceError> {
11451173
let canvas_attribute = match window_handle {
11461174
raw_window_handle::RawWindowHandle::Web(web_handle) => web_handle.id,
11471175
_ => panic!("expected valid handle for canvas"),

0 commit comments

Comments
 (0)