Skip to content

Commit ed6b789

Browse files
authored
Make error scopes thread local (#8585)
1 parent f91d9f3 commit ed6b789

File tree

9 files changed

+121
-12
lines changed

9 files changed

+121
-12
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ One other breaking change worth noting is that in WGSL `@builtin(view_index)` no
106106

107107
By @SupaMaggie70Incorporated in [#8206](https://github.com/gfx-rs/wgpu/pull/8206).
108108

109+
#### Error Scopes are now thread-local
110+
111+
Device error scopes now operate on a per-thread basis. This allows them to be used easily within multithreaded contexts,
112+
without having the error scope capture errors from other threads.
113+
114+
When the `std` feature is **not** enabled, we have no way to differentiate between threads, so error scopes return to be
115+
global operations.
116+
117+
By @cwfitzgerald in [#8685](https://github.com/gfx-rs/wgpu/pull/8685)
118+
109119
#### Log Levels
110120

111121
We have received complaints about wgpu being way too log spammy at log levels `info`/`warn`/`error`. We have

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ authors = ["gfx-rs developers"]
6868
naga = { version = "27.0.0", path = "./naga" }
6969
naga-test = { path = "./naga-test" }
7070
wgpu = { version = "27.0.0", path = "./wgpu", default-features = false, features = [
71+
"std",
7172
"serde",
7273
"wgsl",
7374
"vulkan",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#![cfg(not(target_arch = "wasm32"))]
2+
use std::sync::{
3+
atomic::{AtomicBool, Ordering},
4+
Arc,
5+
};
6+
7+
// Test that error scopes are thread-local: an error scope pushed on one thread
8+
// does not capture errors generated on another thread.
9+
#[test]
10+
fn multi_threaded_scopes() {
11+
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
12+
13+
let other_thread_error = Arc::new(AtomicBool::new(false));
14+
let other_thread_error_clone = other_thread_error.clone();
15+
16+
// Start an error scope on the main thread.
17+
device.push_error_scope(wgpu::ErrorFilter::Validation);
18+
// Register an uncaptured error handler to catch errors from other threads.
19+
device.on_uncaptured_error(Arc::new(move |_error| {
20+
other_thread_error_clone.store(true, Ordering::Relaxed);
21+
}));
22+
23+
// Do something invalid on another thread.
24+
std::thread::scope(|s| {
25+
s.spawn(|| {
26+
let _buffer = device.create_buffer(&wgpu::BufferDescriptor {
27+
label: None,
28+
size: 1 << 63, // Too large!
29+
usage: wgpu::BufferUsages::COPY_SRC,
30+
mapped_at_creation: false,
31+
});
32+
});
33+
});
34+
35+
// Pop the error scope on the main thread.
36+
let error = pollster::block_on(device.pop_error_scope());
37+
38+
// The main thread's error scope should not have captured the other thread's error.
39+
assert!(error.is_none());
40+
// The other thread's error should have been reported to the uncaptured error handler.
41+
assert!(other_thread_error.load(Ordering::Relaxed));
42+
}

tests/tests/wgpu-validation/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod buffer_slice;
55
mod command_buffer_actions;
66
mod device;
77
mod encoding;
8+
mod error_scopes;
89
mod experimental;
910
mod external_texture;
1011
mod instance;

wgpu/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ bitflags.workspace = true
193193
cfg-if.workspace = true
194194
document-features.workspace = true
195195
log.workspace = true
196+
hashbrown.workspace = true
196197
parking_lot = { workspace = true, optional = true }
197198
profiling.workspace = true
198199
raw-window-handle = { workspace = true, features = ["alloc"] }

wgpu/src/api/device.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,30 @@ impl Device {
410410
self.inner.on_uncaptured_error(handler)
411411
}
412412

413-
/// Push an error scope.
413+
/// Push an error scope on this device's error scope stack.
414+
/// All operations on this device, or on resources created from
415+
/// this device, will have their errors captured by this scope
416+
/// until a corresponding pop is made.
417+
///
418+
/// Multiple error scopes may be active at one time, forming a stack.
419+
/// Each error will be reported to the inner-most scope that matches
420+
/// its filter.
421+
///
422+
/// With the `std` feature enabled, this stack is **thread-local**.
423+
/// Without, this is **global** to all threads.
414424
pub fn push_error_scope(&self, filter: ErrorFilter) {
415425
self.inner.push_error_scope(filter)
416426
}
417427

418-
/// Pop an error scope.
428+
/// Pop an error scope from this device's error scope stack. Returns
429+
/// a future which resolves to the error captured by this scope, if any.
430+
///
431+
/// This will pop the most recently pushed error scope on this device.
432+
///
433+
/// If there are no error scopes on this device, this will panic.
434+
///
435+
/// With the `std` feature enabled, the error stack is **thread-local**.
436+
/// Without, this is **global** to all threads.
419437
pub fn pop_error_scope(&self) -> impl Future<Output = Option<Error>> + WasmNotSend {
420438
self.inner.pop_error_scope()
421439
}

wgpu/src/backend/wgpu_core.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use core::{
1616
ptr::NonNull,
1717
slice,
1818
};
19+
use hashbrown::HashMap;
1920

2021
use arrayvec::ArrayVec;
2122
use smallvec::SmallVec;
@@ -37,6 +38,8 @@ use crate::{
3738
};
3839
use crate::{dispatch::DispatchAdapter, util::Mutex};
3940

41+
mod thread_id;
42+
4043
#[derive(Clone)]
4144
pub struct ContextWgpuCore(Arc<wgc::global::Global>);
4245

@@ -627,14 +630,14 @@ struct ErrorScope {
627630
}
628631

629632
struct ErrorSinkRaw {
630-
scopes: Vec<ErrorScope>,
633+
scopes: HashMap<thread_id::ThreadId, Vec<ErrorScope>>,
631634
uncaptured_handler: Option<Arc<dyn crate::UncapturedErrorHandler>>,
632635
}
633636

634637
impl ErrorSinkRaw {
635638
fn new() -> ErrorSinkRaw {
636639
ErrorSinkRaw {
637-
scopes: Vec::new(),
640+
scopes: HashMap::new(),
638641
uncaptured_handler: None,
639642
}
640643
}
@@ -656,12 +659,9 @@ impl ErrorSinkRaw {
656659
crate::Error::Validation { .. } => crate::ErrorFilter::Validation,
657660
crate::Error::Internal { .. } => crate::ErrorFilter::Internal,
658661
};
659-
match self
660-
.scopes
661-
.iter_mut()
662-
.rev()
663-
.find(|scope| scope.filter == filter)
664-
{
662+
let thread_id = thread_id::ThreadId::current();
663+
let scopes = self.scopes.entry(thread_id).or_default();
664+
match scopes.iter_mut().rev().find(|scope| scope.filter == filter) {
665665
Some(scope) => {
666666
if scope.error.is_none() {
667667
scope.error = Some(err);
@@ -1805,15 +1805,20 @@ impl dispatch::DeviceInterface for CoreDevice {
18051805

18061806
fn push_error_scope(&self, filter: crate::ErrorFilter) {
18071807
let mut error_sink = self.error_sink.lock();
1808-
error_sink.scopes.push(ErrorScope {
1808+
let thread_id = thread_id::ThreadId::current();
1809+
let scopes = error_sink.scopes.entry(thread_id).or_default();
1810+
scopes.push(ErrorScope {
18091811
error: None,
18101812
filter,
18111813
});
18121814
}
18131815

18141816
fn pop_error_scope(&self) -> Pin<Box<dyn dispatch::PopErrorScopeFuture>> {
18151817
let mut error_sink = self.error_sink.lock();
1816-
let scope = error_sink.scopes.pop().unwrap();
1818+
let thread_id = thread_id::ThreadId::current();
1819+
let err = "Mismatched pop_error_scope call: no error scope for this thread. Error scopes are thread-local.";
1820+
let scopes = error_sink.scopes.get_mut(&thread_id).expect(err);
1821+
let scope = scopes.pop().expect(err);
18171822
Box::pin(ready(scope.error))
18181823
}
18191824

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//! Implementation of thread IDs for error scope tracking.
2+
//!
3+
//! Supports both std and no_std environments, though
4+
//! the no_std implementation is a stub that does not
5+
//! actually distinguish between threads.
6+
7+
#[cfg(feature = "std")]
8+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9+
pub struct ThreadId(std::thread::ThreadId);
10+
11+
#[cfg(feature = "std")]
12+
impl ThreadId {
13+
pub fn current() -> Self {
14+
ThreadId(std::thread::current().id())
15+
}
16+
}
17+
18+
#[cfg(not(feature = "std"))]
19+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20+
pub struct ThreadId(());
21+
22+
#[cfg(not(feature = "std"))]
23+
impl ThreadId {
24+
pub fn current() -> Self {
25+
// A simple stub implementation for non-std environments. On
26+
// no_std but multithreaded platforms, this will work, but
27+
// make error scope global rather than thread-local.
28+
ThreadId(())
29+
}
30+
}

0 commit comments

Comments
 (0)