Skip to content

Commit 2d835bf

Browse files
kpreidcwfitzgerald
authored andcommitted
Call uncaptured error handler without the lock held.
This prevents a possible deadlock if the callback itself calls wgpu operations.
1 parent ea8315e commit 2d835bf

File tree

4 files changed

+90
-23
lines changed

4 files changed

+90
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ By @Vecvec in [#7913](https://github.com/gfx-rs/wgpu/pull/7913).
7373
- The offset for `set_vertex_buffer` and `set_index_buffer` must be 4B aligned. By @andyleiserson in [#7929](https://github.com/gfx-rs/wgpu/pull/7929).
7474
- The offset and size of bindings are validated as fitting within the underlying buffer in more cases. By @andyleiserson in [#7911](https://github.com/gfx-rs/wgpu/pull/7911).
7575
- The function you pass to `Device::on_uncaptured_error()` must now implement `Sync` in addition to `Send`, and be wrapped in `Arc` instead of `Box`.
76+
In exchange for this, it is no longer possible for calling `wgpu` functions while in that callback to cause a deadlock (not that we encourage you to actually do that).
7677
By @kpreid in [#8011](https://github.com/gfx-rs/wgpu/pull/8011).
7778

7879
#### Naga
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/// Test that if another error occurs reentrantly in the [`wgpu::Device::on_uncaptured_error`]
2+
/// handler, this does not result in a deadlock (as a previous implementation would have had).
3+
#[cfg(not(target_family = "wasm"))] // test needs wgpu::Device: Send + Sync to achieve reentrance
4+
#[test]
5+
fn recursive_uncaptured_error() {
6+
use std::sync::atomic::{AtomicU32, Ordering::Relaxed};
7+
use std::sync::Arc;
8+
9+
const ERRONEOUS_TEXTURE_DESC: wgpu::TextureDescriptor = wgpu::TextureDescriptor {
10+
label: None,
11+
size: wgpu::Extent3d {
12+
width: 1,
13+
height: 1,
14+
depth_or_array_layers: 10,
15+
},
16+
mip_level_count: 0,
17+
sample_count: 0,
18+
dimension: wgpu::TextureDimension::D2,
19+
format: wgpu::TextureFormat::Rgba8UnormSrgb,
20+
usage: wgpu::TextureUsages::COPY_SRC,
21+
view_formats: &[],
22+
};
23+
24+
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
25+
26+
let errors_seen: Arc<AtomicU32> = Arc::default();
27+
let handler = Arc::new({
28+
let errors_seen = errors_seen.clone();
29+
let device = device.clone();
30+
move |_error| {
31+
let previous_count = errors_seen.fetch_add(1, Relaxed);
32+
if previous_count == 0 {
33+
// Trigger another error recursively
34+
_ = device.create_texture(&ERRONEOUS_TEXTURE_DESC);
35+
}
36+
}
37+
});
38+
39+
// Trigger one error which will call the handler
40+
device.on_uncaptured_error(handler);
41+
_ = device.create_texture(&ERRONEOUS_TEXTURE_DESC);
42+
43+
assert_eq!(errors_seen.load(Relaxed), 2);
44+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod binding_arrays;
22
mod buffer;
33
mod buffer_slice;
4+
mod device;
45
mod external_texture;
56
mod instance;
67
mod texture;

wgpu/src/backend/wgpu_core.rs

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -281,27 +281,36 @@ impl ContextWgpuCore {
281281
source,
282282
label: label.unwrap_or_default().to_string(),
283283
});
284-
let mut sink = sink_mutex.lock();
285-
let description = || self.format_error(&*source);
286-
let error = match error_type {
287-
ErrorType::Internal => {
288-
let description = description();
289-
crate::Error::Internal {
290-
source,
291-
description,
284+
let final_error_handling = {
285+
let mut sink = sink_mutex.lock();
286+
let description = || self.format_error(&*source);
287+
let error = match error_type {
288+
ErrorType::Internal => {
289+
let description = description();
290+
crate::Error::Internal {
291+
source,
292+
description,
293+
}
292294
}
293-
}
294-
ErrorType::OutOfMemory => crate::Error::OutOfMemory { source },
295-
ErrorType::Validation => {
296-
let description = description();
297-
crate::Error::Validation {
298-
source,
299-
description,
295+
ErrorType::OutOfMemory => crate::Error::OutOfMemory { source },
296+
ErrorType::Validation => {
297+
let description = description();
298+
crate::Error::Validation {
299+
source,
300+
description,
301+
}
300302
}
301-
}
302-
ErrorType::DeviceLost => return, // will be surfaced via callback
303+
ErrorType::DeviceLost => return, // will be surfaced via callback
304+
};
305+
sink.handle_error_or_return_handler(error)
303306
};
304-
sink.handle_error(error);
307+
308+
if let Some(f) = final_error_handling {
309+
// If the user has provided their own `uncaptured_handler` callback, invoke it now,
310+
// having released our lock on `sink_mutex`. See the comments on
311+
// `handle_error_or_return_handler` for details.
312+
f();
313+
}
305314
}
306315

307316
#[inline]
@@ -628,8 +637,18 @@ impl ErrorSinkRaw {
628637
}
629638
}
630639

640+
/// Deliver the error to
641+
///
642+
/// * the innermost error scope, if any, or
643+
/// * the uncaptured error handler, if there is one, or
644+
/// * [`default_error_handler()`].
645+
///
646+
/// If a closure is returned, the caller should call it immediately after dropping the
647+
/// [`ErrorSink`] mutex guard. This makes sure that the user callback is not called with
648+
/// a wgpu mutex held.
631649
#[track_caller]
632-
fn handle_error(&mut self, err: crate::Error) {
650+
#[must_use]
651+
fn handle_error_or_return_handler(&mut self, err: crate::Error) -> Option<impl FnOnce()> {
633652
let filter = match err {
634653
crate::Error::OutOfMemory { .. } => crate::ErrorFilter::OutOfMemory,
635654
crate::Error::Validation { .. } => crate::ErrorFilter::Validation,
@@ -645,13 +664,15 @@ impl ErrorSinkRaw {
645664
if scope.error.is_none() {
646665
scope.error = Some(err);
647666
}
667+
None
648668
}
649669
None => {
650-
if let Some(custom_handler) = self.uncaptured_handler.as_ref() {
651-
(custom_handler)(err);
670+
if let Some(custom_handler) = &self.uncaptured_handler {
671+
let custom_handler = Arc::clone(custom_handler);
672+
Some(move || (custom_handler)(err))
652673
} else {
653674
// direct call preserves #[track_caller] where dyn can't
654-
default_error_handler(err);
675+
default_error_handler(err)
655676
}
656677
}
657678
}
@@ -665,7 +686,7 @@ impl fmt::Debug for ErrorSinkRaw {
665686
}
666687

667688
#[track_caller]
668-
fn default_error_handler(err: crate::Error) {
689+
fn default_error_handler(err: crate::Error) -> ! {
669690
log::error!("Handling wgpu errors as fatal by default");
670691
panic!("wgpu error: {err}\n");
671692
}

0 commit comments

Comments
 (0)