Skip to main content

alpha2_render/
gpu_errors.rs

1//! GPU error tracking for a frame's work.
2//!
3//! An out-of-memory texture or invalid pass makes its whole submission rejected, and
4//! `queue.submit` does not say so; the error goes to an error scope or the uncaptured-error
5//! handler. Unchecked, the renderer would keep results never drawn, reuse failed allocations,
6//! and misread the missing readback canary as a device dropping work.
7//!
8//! Every renderer encoder therefore opens a [`FrameErrorScope`] that its submission closes.
9//! Natively scopes resolve at once and an error fails the frame ([`crate::RenderError::Gpu`]).
10//! In a browser scopes resolve only on the event loop, so the embedding reports uncaptured
11//! errors ([`GpuErrorReports::report`]) and the renderer drops kept state before its next frame.
12//!
13//! Either way, everything pooled before the error is distrusted
14//! ([`crate::retire::Retirement::doubt`]), since `wgpu` cannot say which object is invalid.
15
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
18
19use alpha2_gpu::GpuErrorKind;
20
21use crate::Shared;
22use crate::retire::Retirement;
23
24/// Validation and out-of-memory scopes of one renderer encoder, from creation to submission.
25///
26/// Scopes are per thread and close in reverse order, so this is neither `Send` nor `Sync`, and
27/// a nested encoder must be submitted or dropped first.
28#[derive(Debug)]
29pub(crate) struct FrameErrorScope {
30    #[cfg(not(target_arch = "wasm32"))]
31    scopes: Option<[wgpu::ErrorScopeGuard; 2]>,
32    /// Told when an encoder is dropped unsubmitted with an error, since it may have pooled an
33    /// invalid resource.
34    retirement: Shared<Retirement>,
35    #[cfg(target_arch = "wasm32")]
36    _thread_bound: std::marker::PhantomData<*mut ()>,
37}
38
39impl FrameErrorScope {
40    /// Opens the scopes on this thread; a no-op in a browser.
41    pub(crate) fn open(
42        #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] device: &wgpu::Device,
43        retirement: Shared<Retirement>,
44    ) -> Self {
45        Self {
46            #[cfg(not(target_arch = "wasm32"))]
47            scopes: Some([
48                device.push_error_scope(wgpu::ErrorFilter::OutOfMemory),
49                device.push_error_scope(wgpu::ErrorFilter::Validation),
50            ]),
51            retirement,
52            #[cfg(target_arch = "wasm32")]
53            _thread_bound: std::marker::PhantomData,
54        }
55    }
56
57    /// Closes the scopes and returns the error, out-of-memory first because a failed allocation
58    /// also causes validation errors downstream.
59    pub(crate) fn close(mut self) -> Option<GpuErrorKind> {
60        self.pop()
61    }
62
63    #[cfg(not(target_arch = "wasm32"))]
64    fn pop(&mut self) -> Option<GpuErrorKind> {
65        let [out_of_memory, validation] = self.scopes.take()?;
66        let validation = resolved(validation.pop());
67        let out_of_memory = resolved(out_of_memory.pop());
68        out_of_memory
69            .or(validation)
70            .map(|error| GpuErrorKind::of(&error))
71    }
72
73    #[cfg(target_arch = "wasm32")]
74    #[allow(clippy::unused_self)] // The browser opens no scope to pop.
75    const fn pop(&mut self) -> Option<GpuErrorKind> {
76        None
77    }
78}
79
80impl Drop for FrameErrorScope {
81    fn drop(&mut self) {
82        if self.pop().is_some() {
83            self.retirement.doubt();
84        }
85    }
86}
87
88/// Runs `create` under out-of-memory and validation scopes, returning what they caught as
89/// [`FrameErrorScope::close`] does.
90///
91/// For resources made outside a frame, whose failure must fail the call. A no-op in a browser,
92/// where the uncaptured-error report covers it.
93pub(crate) fn capture_creation<T>(
94    #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] device: &wgpu::Device,
95    create: impl FnOnce() -> T,
96) -> (T, Option<GpuErrorKind>) {
97    #[cfg(not(target_arch = "wasm32"))]
98    {
99        let out_of_memory = device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
100        let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
101        let created = create();
102        let validation = resolved(validation.pop());
103        let out_of_memory = resolved(out_of_memory.pop());
104        (
105            created,
106            out_of_memory
107                .or(validation)
108                .map(|error| GpuErrorKind::of(&error)),
109        )
110    }
111    #[cfg(target_arch = "wasm32")]
112    {
113        (create(), None)
114    }
115}
116
117/// A native scope's result, ready when popped on `wgpu-core`.
118#[cfg(not(target_arch = "wasm32"))]
119fn resolved<T>(future: impl std::future::Future<Output = Option<T>>) -> Option<T> {
120    let mut future = std::pin::pin!(future);
121    let mut context = std::task::Context::from_waker(std::task::Waker::noop());
122    match future.as_mut().poll(&mut context) {
123        std::task::Poll::Ready(result) => result,
124        std::task::Poll::Pending => None,
125    }
126}
127
128/// GPU errors the embedding saw outside the renderer's scopes (in a browser, all of them).
129///
130/// Clones share one record, so the embedding keeps one in its error handler. The renderer acts
131/// on reports at its next encoder.
132#[derive(Clone, Debug, Default)]
133pub struct GpuErrorReports {
134    inner: Arc<ReportState>,
135}
136
137#[derive(Debug, Default)]
138struct ReportState {
139    /// Errors reported so far.
140    reported: AtomicU64,
141    /// How many reports the renderer has dropped its kept state for.
142    handled: AtomicU64,
143    /// The last kind, encoded by [`kind_code`].
144    last: AtomicU8,
145}
146
147impl GpuErrorReports {
148    /// Records an error reported outside the renderer's scopes. Lock-free; callable from any
149    /// thread.
150    pub fn report(&self, kind: GpuErrorKind) {
151        self.inner.last.store(kind_code(kind), Ordering::Release);
152        self.inner.reported.fetch_add(1, Ordering::AcqRel);
153    }
154
155    /// How many errors have been reported.
156    pub(crate) fn count(&self) -> u64 {
157        self.inner.reported.load(Ordering::Acquire)
158    }
159
160    pub(crate) fn last_kind(&self) -> Option<GpuErrorKind> {
161        match self.inner.last.load(Ordering::Acquire) {
162            1 => Some(GpuErrorKind::OutOfMemory),
163            2 => Some(GpuErrorKind::Validation),
164            3 => Some(GpuErrorKind::Internal),
165            _ => None,
166        }
167    }
168
169    /// Whether errors were reported since the last call, marking them handled.
170    pub(crate) fn take_unhandled(&self) -> bool {
171        let reported = self.count();
172        self.inner.handled.swap(reported, Ordering::AcqRel) != reported
173    }
174}
175
176const fn kind_code(kind: GpuErrorKind) -> u8 {
177    match kind {
178        GpuErrorKind::OutOfMemory => 1,
179        GpuErrorKind::Validation => 2,
180        _ => 3,
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn reports_are_counted_and_handled_once() {
190        let reports = GpuErrorReports::default();
191        assert!(!reports.take_unhandled());
192        assert_eq!(reports.last_kind(), None);
193        let handler = reports.clone();
194        handler.report(GpuErrorKind::OutOfMemory);
195        handler.report(GpuErrorKind::Validation);
196        assert_eq!(reports.count(), 2);
197        assert_eq!(reports.last_kind(), Some(GpuErrorKind::Validation));
198        assert!(reports.take_unhandled());
199        assert!(!reports.take_unhandled());
200        handler.report(GpuErrorKind::Internal);
201        assert_eq!(reports.last_kind(), Some(GpuErrorKind::Internal));
202        assert!(reports.take_unhandled());
203    }
204}