Skip to main content

alpha2_render/
gpu_errors.rs

1//! What the GPU said about a frame's work, and what the renderer stops
2//! trusting when it said no.
3//!
4//! An out-of-memory texture or an invalid pass makes the submission that
5//! uses it invalid, and WebGPU and `wgpu-core` alike reject such a
6//! submission whole: nothing in it runs. `queue.submit` returns nothing to
7//! say so; the error goes to an error scope, or else to the device's
8//! uncaptured-error handler. A renderer that does not look would keep the
9//! results the frame never drew, hand the textures that failed to allocate
10//! to later frames, and read the readback canary the rejected submission
11//! never wrote as a device that drops work.
12//!
13//! So every renderer command encoder opens a [`FrameErrorScope`], one
14//! out-of-memory and one validation scope, which its submission closes.
15//! Natively a scope resolves at once: an error it caught is the frame's
16//! failure ([`crate::RenderError::Gpu`]), and the frame stops before it keeps
17//! a result, tracks a canary, or hands back a readback. A browser resolves
18//! scopes only on its event loop, so there the embedding reports the
19//! uncaptured errors it receives ([`GpuErrorReports::report`]) instead, and
20//! the renderer drops what it kept before its next frame and reads a missing
21//! canary after such a report as the rejection, not as a device that drops
22//! work.
23//!
24//! Either way, what the renderer pooled before the error is no longer
25//! trusted ([`crate::retire::Retirement::doubt`]): a pooled texture or buffer
26//! created then may be the invalid one, and nothing can ask `wgpu` which.
27
28use std::sync::Arc;
29use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
30
31use alpha2_gpu::GpuErrorKind;
32
33use crate::Shared;
34use crate::retire::Retirement;
35
36/// The validation and out-of-memory scopes of one renderer command encoder,
37/// from its creation until its submission.
38///
39/// Error scopes belong to the thread that pushed them and close in reverse
40/// order, so a renderer encoder stays on the thread that created it (this is
41/// neither `Send` nor `Sync`), and one opened inside another's lifetime is
42/// submitted or dropped first.
43#[derive(Debug)]
44pub(crate) struct FrameErrorScope {
45    #[cfg(not(target_arch = "wasm32"))]
46    scopes: Option<[wgpu::ErrorScopeGuard; 2]>,
47    /// Told when an encoder is dropped unsubmitted with an error caught: its
48    /// failed encode may have pooled an invalid resource.
49    retirement: Shared<Retirement>,
50    #[cfg(target_arch = "wasm32")]
51    _thread_bound: std::marker::PhantomData<*mut ()>,
52}
53
54impl FrameErrorScope {
55    /// Opens the scopes on this thread. On the browser nothing is opened:
56    /// its scopes resolve on the event loop, after the frame has returned.
57    pub(crate) fn open(
58        #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] device: &wgpu::Device,
59        retirement: Shared<Retirement>,
60    ) -> Self {
61        Self {
62            #[cfg(not(target_arch = "wasm32"))]
63            scopes: Some([
64                device.push_error_scope(wgpu::ErrorFilter::OutOfMemory),
65                device.push_error_scope(wgpu::ErrorFilter::Validation),
66            ]),
67            retirement,
68            #[cfg(target_arch = "wasm32")]
69            _thread_bound: std::marker::PhantomData,
70        }
71    }
72
73    /// Closes the scopes and returns what the GPU said about the work done
74    /// under them, out of memory before validation: an allocation that
75    /// failed makes every later use of it a validation error too, and the
76    /// allocation is the cause.
77    pub(crate) fn close(mut self) -> Option<GpuErrorKind> {
78        self.pop()
79    }
80
81    #[cfg(not(target_arch = "wasm32"))]
82    fn pop(&mut self) -> Option<GpuErrorKind> {
83        let [out_of_memory, validation] = self.scopes.take()?;
84        // Reverse order of the pushes.
85        let validation = resolved(validation.pop());
86        let out_of_memory = resolved(out_of_memory.pop());
87        out_of_memory
88            .or(validation)
89            .map(|error| GpuErrorKind::of(&error))
90    }
91
92    #[cfg(target_arch = "wasm32")]
93    #[allow(clippy::unused_self)] // The browser opens no scope to pop.
94    const fn pop(&mut self) -> Option<GpuErrorKind> {
95        None
96    }
97}
98
99impl Drop for FrameErrorScope {
100    fn drop(&mut self) {
101        if self.pop().is_some() {
102            self.retirement.doubt();
103        }
104    }
105}
106
107/// Runs `create` under out-of-memory and validation scopes and returns what
108/// they caught, out of memory first, as [`FrameErrorScope::close`] does.
109///
110/// For resources made outside a frame, such as an effect's lookup table,
111/// whose failure must refuse the call instead of reaching a later frame. On
112/// the browser no scope opens, as for a frame: its scopes resolve only on the
113/// event loop, and the uncaptured-error report covers it instead.
114pub(crate) fn capture_creation<T>(
115    #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] device: &wgpu::Device,
116    create: impl FnOnce() -> T,
117) -> (T, Option<GpuErrorKind>) {
118    #[cfg(not(target_arch = "wasm32"))]
119    {
120        let out_of_memory = device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
121        let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
122        let created = create();
123        let validation = resolved(validation.pop());
124        let out_of_memory = resolved(out_of_memory.pop());
125        (
126            created,
127            out_of_memory
128                .or(validation)
129                .map(|error| GpuErrorKind::of(&error)),
130        )
131    }
132    #[cfg(target_arch = "wasm32")]
133    {
134        (create(), None)
135    }
136}
137
138/// A native scope's result, which `wgpu-core` has ready when it is popped.
139#[cfg(not(target_arch = "wasm32"))]
140fn resolved<T>(future: impl std::future::Future<Output = Option<T>>) -> Option<T> {
141    let mut future = std::pin::pin!(future);
142    let mut context = std::task::Context::from_waker(std::task::Waker::noop());
143    match future.as_mut().poll(&mut context) {
144        std::task::Poll::Ready(result) => result,
145        // Not on wgpu-core; nothing to report then.
146        std::task::Poll::Pending => None,
147    }
148}
149
150/// GPU errors the embedding saw that no scope of the renderer's caught:
151/// on the browser, every uncaptured error its device reports.
152///
153/// Cloning shares one record, so the embedding keeps a clone in its
154/// device's error handler. Reporting only records; the renderer acts on it
155/// at its next encoder, on its own thread.
156#[derive(Clone, Debug, Default)]
157pub struct GpuErrorReports {
158    inner: Arc<ReportState>,
159}
160
161#[derive(Debug, Default)]
162struct ReportState {
163    /// Errors reported so far.
164    reported: AtomicU64,
165    /// Of those, how many the renderer has dropped its kept state for.
166    handled: AtomicU64,
167    /// The last one's kind, as [`kind_code`] encodes it.
168    last: AtomicU8,
169}
170
171impl GpuErrorReports {
172    /// Records an error the device reported outside any of the renderer's
173    /// scopes. Cheap and lock-free: a device's error handler may call it
174    /// from any thread.
175    pub fn report(&self, kind: GpuErrorKind) {
176        self.inner.last.store(kind_code(kind), Ordering::Release);
177        self.inner.reported.fetch_add(1, Ordering::AcqRel);
178    }
179
180    /// How many errors have been reported.
181    pub(crate) fn count(&self) -> u64 {
182        self.inner.reported.load(Ordering::Acquire)
183    }
184
185    /// The kind of the last error reported, if any was.
186    pub(crate) fn last_kind(&self) -> Option<GpuErrorKind> {
187        match self.inner.last.load(Ordering::Acquire) {
188            1 => Some(GpuErrorKind::OutOfMemory),
189            2 => Some(GpuErrorKind::Validation),
190            3 => Some(GpuErrorKind::Internal),
191            _ => None,
192        }
193    }
194
195    /// Whether errors were reported since the last call, marking them handled.
196    pub(crate) fn take_unhandled(&self) -> bool {
197        let reported = self.count();
198        self.inner.handled.swap(reported, Ordering::AcqRel) != reported
199    }
200}
201
202const fn kind_code(kind: GpuErrorKind) -> u8 {
203    match kind {
204        GpuErrorKind::OutOfMemory => 1,
205        GpuErrorKind::Validation => 2,
206        _ => 3,
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn reports_are_counted_and_handled_once() {
216        let reports = GpuErrorReports::default();
217        assert!(!reports.take_unhandled());
218        assert_eq!(reports.last_kind(), None);
219        let handler = reports.clone();
220        handler.report(GpuErrorKind::OutOfMemory);
221        handler.report(GpuErrorKind::Validation);
222        assert_eq!(reports.count(), 2);
223        assert_eq!(reports.last_kind(), Some(GpuErrorKind::Validation));
224        assert!(reports.take_unhandled());
225        assert!(!reports.take_unhandled());
226        handler.report(GpuErrorKind::Internal);
227        assert_eq!(reports.last_kind(), Some(GpuErrorKind::Internal));
228        assert!(reports.take_unhandled());
229    }
230}