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}