wgpu_core/
error.rs

1use alloc::string::ToString as _;
2use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec};
3use core::fmt;
4
5use thiserror::Error;
6
7use alloc::format;
8use core::error;
9
10use hashbrown::HashMap;
11use wgpu_sync::Mutex;
12use wgt::error::{Error, ErrorFilter, ErrorSource, ErrorType, UncapturedErrorHandler, WebGpuError};
13use wgt::WasmNotSendSync;
14
15use crate::device::Device;
16
17/// Implementation of thread IDs for error scope tracking.
18///
19/// Supports both std and no_std environments, though
20/// the no_std implementation is a stub that does not
21/// actually distinguish between threads.
22mod thread_id {
23    #[cfg(feature = "std")]
24    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25    pub struct ThreadId(std::thread::ThreadId);
26
27    #[cfg(feature = "std")]
28    impl ThreadId {
29        pub fn current() -> Self {
30            ThreadId(std::thread::current().id())
31        }
32    }
33
34    #[cfg(not(feature = "std"))]
35    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36    pub struct ThreadId(());
37
38    #[cfg(not(feature = "std"))]
39    impl ThreadId {
40        pub fn current() -> Self {
41            // A simple stub implementation for non-std environments. On
42            // no_std but multithreaded platforms, this will work, but
43            // make error scope global rather than thread-local.
44            ThreadId(())
45        }
46    }
47}
48
49struct ErrorScope {
50    pub error: Option<Error>,
51    pub filter: ErrorFilter,
52}
53
54struct InternalErrorSink {
55    scopes: HashMap<thread_id::ThreadId, Vec<ErrorScope>>,
56    uncaptured_handler: Option<Arc<dyn UncapturedErrorHandler>>,
57}
58
59pub struct ErrorSink(Mutex<InternalErrorSink>);
60
61impl ErrorSink {
62    pub fn new() -> ErrorSink {
63        // The mutex is unranked as it's shortlived
64        ErrorSink(Mutex::new(InternalErrorSink::new()))
65    }
66
67    #[cold]
68    #[track_caller]
69    #[inline(never)]
70    fn handle_error_inner(
71        &self,
72        error_type: ErrorType,
73        source: ErrorSource,
74        label: Option<&str>,
75        fn_ident: &'static str,
76    ) {
77        let source: ErrorSource = Box::new(ContextError {
78            fn_ident,
79            source,
80            label: label.unwrap_or_default().to_string(),
81        });
82        let final_error_handling = {
83            let mut sink = self.0.lock();
84            let error = match error_type {
85                ErrorType::Internal => {
86                    let description = format_error(&*source);
87                    Error::Internal {
88                        source,
89                        description,
90                    }
91                }
92                ErrorType::OutOfMemory => Error::OutOfMemory { source },
93                ErrorType::Validation => {
94                    let description = format_error(&*source);
95                    Error::Validation {
96                        source,
97                        description,
98                    }
99                }
100                ErrorType::DeviceLost => return, // will be surfaced via callback
101            };
102            sink.handle_error_or_return_handler(error)
103        };
104
105        if let Some(f) = final_error_handling {
106            // If the user has provided their own `uncaptured_handler` callback, invoke it now,
107            // having released our lock on `sink_mutex`. See the comments on
108            // `handle_error_or_return_handler` for details.
109            f();
110        }
111    }
112
113    #[inline]
114    #[track_caller]
115    pub fn handle_error(
116        &self,
117        source: impl WebGpuError + WasmNotSendSync + 'static,
118        label: Option<&str>,
119        fn_ident: &'static str,
120    ) {
121        let error_type = source.webgpu_error_type();
122        self.handle_error_inner(error_type, Box::new(source), label, fn_ident)
123    }
124
125    #[inline]
126    #[track_caller]
127    pub fn handle_error_nolabel(
128        &self,
129        source: impl WebGpuError + WasmNotSendSync + 'static,
130        fn_ident: &'static str,
131    ) {
132        let error_type = source.webgpu_error_type();
133        self.handle_error_inner(error_type, Box::new(source), None, fn_ident)
134    }
135}
136
137impl InternalErrorSink {
138    fn new() -> InternalErrorSink {
139        InternalErrorSink {
140            scopes: HashMap::new(),
141            uncaptured_handler: None,
142        }
143    }
144
145    /// Deliver the error to
146    ///
147    /// * the innermost error scope, if any, or
148    /// * the uncaptured error handler, if there is one, or
149    /// * [`default_error_handler()`].
150    ///
151    /// If a closure is returned, the caller should call it immediately after dropping the
152    /// [`ErrorSink`] mutex guard. This makes sure that the user callback is not called with
153    /// a wgpu mutex held.
154    #[track_caller]
155    #[must_use]
156    fn handle_error_or_return_handler(&mut self, err: Error) -> Option<impl FnOnce()> {
157        let filter = match err {
158            Error::OutOfMemory { .. } => ErrorFilter::OutOfMemory,
159            Error::Validation { .. } => ErrorFilter::Validation,
160            Error::Internal { .. } => ErrorFilter::Internal,
161        };
162        let thread_id = thread_id::ThreadId::current();
163        let scopes = self.scopes.entry(thread_id).or_default();
164        match scopes.iter_mut().rev().find(|scope| scope.filter == filter) {
165            Some(scope) => {
166                if scope.error.is_none() {
167                    scope.error = Some(err);
168                }
169                None
170            }
171            None => {
172                if let Some(custom_handler) = &self.uncaptured_handler {
173                    let custom_handler = Arc::clone(custom_handler);
174                    Some(move || (custom_handler)(err))
175                } else {
176                    // direct call preserves #[track_caller] where dyn can't
177                    default_error_handler(err)
178                }
179            }
180        }
181    }
182}
183
184impl fmt::Debug for InternalErrorSink {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(f, "ErrorSink")
187    }
188}
189
190#[track_caller]
191fn default_error_handler(err: Error) -> ! {
192    log::error!("Handling wgpu errors as fatal by default");
193    panic!("wgpu error: {err}\n");
194}
195
196#[derive(Debug, Error)]
197#[error("Error scope stack is empty")]
198pub struct EmptyErrorScopeStack;
199
200impl Device {
201    pub fn on_uncaptured_error(&self, handler: Arc<dyn UncapturedErrorHandler>) {
202        let mut error_sink = self.error_sink.0.lock();
203        error_sink.uncaptured_handler = Some(handler);
204    }
205
206    /// <https://gpuweb.github.io/gpuweb/#dom-gpudevice-pusherrorscope>
207    pub fn push_error_scope(&self, filter: ErrorFilter) {
208        let mut error_sink = self.error_sink.0.lock();
209        let thread_id = thread_id::ThreadId::current();
210        let scopes = error_sink.scopes.entry(thread_id).or_default();
211        scopes.push(ErrorScope {
212            error: None,
213            filter,
214        });
215    }
216
217    /// <https://gpuweb.github.io/gpuweb/#dom-gpudevice-poperrorscope>
218    pub fn pop_error_scope(&self) -> Result<Option<Error>, EmptyErrorScopeStack> {
219        // 1. If this is lost:
220        if !self.is_valid() {
221            // Resolve promise with null.
222            return Ok(None);
223        }
224        let mut error_sink = self.error_sink.0.lock();
225
226        let thread_id = thread_id::ThreadId::current();
227        let scopes = error_sink.scopes.entry(thread_id).or_default();
228        // 2. this.[[errorScopeStack]].size must be > 0.
229        match scopes.pop() {
230            // 3. Let scope be the result of popping an item off of this.[[errorScopeStack]].
231            // 4. Let error be any one of the items in scope.[[errors]], or null if there are none.
232            Some(scope) => Ok(scope.error),
233            // otherwise Reject promise with an OperationError.
234            None => Err(EmptyErrorScopeStack),
235        }
236    }
237}
238
239impl Device {
240    // wgpu manipulates the error sink directly
241    // to handle panicking
242    pub fn error_sink(&self) -> &ErrorSink {
243        &self.error_sink
244    }
245}
246
247#[inline(never)]
248pub fn format_error(err: &(dyn error::Error + 'static)) -> String {
249    let mut output = String::new();
250    let mut level = 1;
251
252    fn print_tree(output: &mut String, level: &mut usize, e: &(dyn error::Error + 'static)) {
253        let mut print = |e: &(dyn error::Error + 'static)| {
254            use core::fmt::Write;
255            writeln!(output, "{}{}", " ".repeat(*level * 2), e).unwrap();
256
257            if let Some(e) = e.source() {
258                *level += 1;
259                print_tree(output, level, e);
260                *level -= 1;
261            }
262        };
263        if let Some(multi) = e.downcast_ref::<MultiError>() {
264            for e in multi.errors() {
265                print(e);
266            }
267        } else {
268            print(e);
269        }
270    }
271
272    print_tree(&mut output, &mut level, err);
273
274    format!("Validation Error\n\nCaused by:\n{output}")
275}
276
277impl Device {
278    #[inline]
279    #[track_caller]
280    pub fn handle_error(
281        &self,
282        source: impl WebGpuError + WasmNotSendSync + 'static,
283        label: Option<&str>,
284        fn_ident: &'static str,
285    ) {
286        self.error_sink.handle_error(source, label, fn_ident);
287    }
288
289    #[inline]
290    #[track_caller]
291    pub fn handle_error_nolabel(
292        &self,
293        source: impl WebGpuError + WasmNotSendSync + 'static,
294        fn_ident: &'static str,
295    ) {
296        self.error_sink.handle_error_nolabel(source, fn_ident);
297    }
298}
299
300#[derive(Debug, Error)]
301#[error(
302    "In {fn_ident}{}{}{}",
303    if self.label.is_empty() { "" } else { ", label = '" },
304    self.label,
305    if self.label.is_empty() { "" } else { "'" }
306)]
307pub struct ContextError {
308    pub fn_ident: &'static str,
309    #[source]
310    pub source: ErrorSource,
311    pub label: String,
312}
313
314/// Don't use this error type with thiserror's #[error(transparent)]
315#[derive(Clone)]
316pub struct MultiError {
317    inner: Vec<Arc<dyn error::Error + Send + Sync + 'static>>,
318}
319
320impl MultiError {
321    pub fn new<T: error::Error + Send + Sync + 'static>(
322        iter: impl ExactSizeIterator<Item = T>,
323    ) -> Option<Self> {
324        if iter.len() == 0 {
325            return None;
326        }
327        Some(Self {
328            inner: iter.map(Box::from).map(Arc::from).collect(),
329        })
330    }
331
332    pub fn errors(
333        &self,
334    ) -> Box<dyn Iterator<Item = &(dyn error::Error + Send + Sync + 'static)> + '_> {
335        Box::new(self.inner.iter().map(|e| e.as_ref()))
336    }
337}
338
339impl fmt::Debug for MultiError {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
341        fmt::Debug::fmt(&self.inner[0], f)
342    }
343}
344
345impl fmt::Display for MultiError {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
347        fmt::Display::fmt(&self.inner[0], f)
348    }
349}
350
351impl error::Error for MultiError {
352    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
353        self.inner[0].source()
354    }
355}
356
357// special implementations for wgpu
358impl Device {
359    pub fn push_error_scope_with_index(&self, filter: ErrorFilter) -> u32 {
360        let index = {
361            let mut error_sink = self.error_sink.0.lock();
362            let thread_id = thread_id::ThreadId::current();
363            let scopes = error_sink.scopes.entry(thread_id).or_default();
364            scopes
365                .len()
366                .try_into()
367                .expect("Greater than 2^32 nested error scopes")
368        };
369        self.push_error_scope(filter);
370        index
371    }
372
373    pub fn pop_error_scope_checked(&self, index: u32) -> Option<Error> {
374        #[cfg(feature = "std")]
375        fn is_panicking() -> bool {
376            std::thread::panicking()
377        }
378
379        #[cfg(not(feature = "std"))]
380        fn is_panicking() -> bool {
381            false
382        }
383
384        let mut error_sink = self.error_sink.0.lock();
385
386        // We go out of our way to avoid panicking while unwinding, because that would abort the process,
387        // and we are supposed to just drop the error scope on the floor.
388        let is_panicking = is_panicking();
389        let thread_id = thread_id::ThreadId::current();
390        let err = "Mismatched pop_error_scope call: no error scope for this thread. Error scopes are thread-local.";
391        let scopes = match error_sink.scopes.get_mut(&thread_id) {
392            Some(s) => s,
393            None => {
394                if !is_panicking {
395                    panic!("{err}");
396                } else {
397                    return None;
398                }
399            }
400        };
401        if scopes.is_empty() && !is_panicking {
402            panic!("{err}");
403        }
404        if index as usize != scopes.len() - 1 && !is_panicking {
405            panic!(
406                "Mismatched pop_error_scope call: error scopes must be popped in reverse order."
407            );
408        }
409
410        // It would be more correct in this case to use `remove` here so that when unwinding is occurring
411        // we would remove the correct error scope, but we don't have such a primitive on the web
412        // and having consistent behavior here is more important. If you are unwinding and it unwinds
413        // the guards in the wrong order, it's totally reasonable to have incorrect behavior.
414        let scope = match scopes.pop() {
415            Some(s) => s,
416            None if !is_panicking => unreachable!(),
417            None => return None,
418        };
419
420        scope.error
421    }
422}