Skip to main content

alpha2_render/
error.rs

1use std::fmt;
2
3/// Render input or output failure.
4#[derive(Copy, Clone, Debug, Eq, PartialEq)]
5#[non_exhaustive]
6pub enum RenderError {
7    /// Render targets must have non-zero dimensions.
8    ZeroDimension,
9    /// Render dimensions exceed the configured or physical device limit.
10    DimensionTooLarge,
11    /// Colors must be finite, in the unit interval, and premultiplied.
12    InvalidColor,
13    /// A layer transform is non-finite, has zero scale, or invalid opacity.
14    InvalidVisual,
15    /// An effect parameter is non-finite or outside its supported range.
16    InvalidEffectParameter,
17    /// No effect with the requested identity is registered.
18    UnknownEffect,
19    /// Supplied parameter names or types do not match the effect schema.
20    InvalidEffectParams,
21    /// An uploaded effect resource has an invalid identity, shape, or payload.
22    InvalidEffectResource,
23    /// An effect invocation omitted a required registered resource.
24    MissingEffectResource,
25    /// A resource identity resolves to a different representation than required.
26    EffectResourceKindMismatch,
27    /// A transition invocation omitted its second texture.
28    MissingEffectInput,
29    /// A frame needs a caption, text generator or layout its host did not prepare at this size.
30    MissingText,
31    /// RGBA input byte length does not match the declared image size.
32    InvalidImageData,
33    /// A rendered texture could not be read back to host memory, and where.
34    TextureReadback(ReadbackFailure),
35    /// An internally constructed render graph was structurally invalid.
36    InvalidGraph,
37    /// A native texture could not synchronize with the submission, with the platform's device
38    /// status when available.
39    ExternalTextureSync(Option<alpha2_media::GpuFault>),
40    /// Caller NV12 planes are not R8 luma and RG8 chroma render attachments at full and half size.
41    InvalidPlaneTarget,
42    /// GPU work did not complete within the renderer's wait timeout.
43    GpuTimeout,
44    /// The device completed a submission without running it (a readback lacked its canary). The
45    /// renderer refuses further work; only a new process gets a working device.
46    GpuWorkDropped,
47    /// The GPU rejected this frame's work (failed allocation or invalid use). Nothing ran, the
48    /// device is fine, and the renderer kept nothing of the frame.
49    Gpu {
50        /// The class of error the GPU reported.
51        kind: alpha2_gpu::GpuErrorKind,
52    },
53    /// The device can no longer create the resources a frame needs; the renderer refuses further
54    /// work.
55    GpuDeviceLost,
56    /// The device was lost after a recent submission kept the GPU busy for at least half the OS
57    /// watchdog ([`alpha2_gpu::hung_threshold_ms`]). Rerunning the same work will hang again.
58    GpuDeviceHung {
59        /// The export frame the long submission rendered, when known.
60        frame: Option<u64>,
61        /// That frame's timeline time, in ticks, when known.
62        time_ticks: Option<i64>,
63        /// How long the submission kept the GPU busy, in milliseconds.
64        gpu_ms: u32,
65        /// The watchdog it was measured against, in milliseconds.
66        watchdog_ms: u32,
67    },
68    /// An HDR (PQ or HLG) source met [`crate::ColorPolicy::RejectHdr`].
69    UnsupportedHdr,
70    /// A source belongs to the other backend (CPU source on GPU renderer or vice versa).
71    InvalidSource,
72}
73
74impl fmt::Display for RenderError {
75    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            Self::ZeroDimension => formatter.write_str("render dimensions must be non-zero"),
78            Self::DimensionTooLarge => {
79                formatter.write_str("render dimensions exceed the supported texture budget")
80            }
81            Self::InvalidColor => {
82                formatter.write_str(
83                    "color components must be finite values from zero to one, premultiplied: none above alpha",
84                )
85            }
86            Self::InvalidVisual => formatter.write_str("layer presentation values are invalid"),
87            Self::InvalidEffectParameter => formatter.write_str("effect parameter is invalid"),
88            Self::UnknownEffect => formatter.write_str("effect is not registered"),
89            Self::InvalidEffectParams => {
90                formatter.write_str("effect parameters do not match schema")
91            }
92            Self::InvalidEffectResource => formatter.write_str("effect resource is invalid"),
93            Self::MissingEffectResource => {
94                formatter.write_str("effect resource is missing or not registered")
95            }
96            Self::MissingText => {
97                formatter.write_str("text was not prepared for this frame and size")
98            }
99            Self::EffectResourceKindMismatch => {
100                formatter.write_str("effect resource kind does not match schema")
101            }
102            Self::MissingEffectInput => formatter.write_str("effect input texture is missing"),
103            Self::InvalidImageData => {
104                formatter.write_str("RGBA byte length does not match the image size")
105            }
106            Self::TextureReadback(failure) => {
107                write!(formatter, "GPU texture readback failed: {failure}")
108            }
109            Self::InvalidGraph => formatter.write_str("render graph is structurally invalid"),
110            Self::ExternalTextureSync(None) => {
111                formatter.write_str("external texture synchronization failed")
112            }
113            Self::ExternalTextureSync(Some(fault)) => write!(
114                formatter,
115                "external texture synchronization failed ({})",
116                fault.as_str()
117            ),
118            Self::InvalidPlaneTarget => formatter.write_str(
119                "NV12 plane targets must be an R8 luma and an RG8 chroma render attachment at the output size and half of it",
120            ),
121            Self::GpuTimeout => formatter.write_str(GPU_TIMEOUT_MESSAGE),
122            Self::GpuWorkDropped => formatter.write_str(GPU_WORK_DROPPED_MESSAGE),
123            Self::Gpu { kind } => write!(
124                formatter,
125                "the GPU rejected this frame's work with a {} error",
126                kind.as_str()
127            ),
128            Self::GpuDeviceLost => formatter.write_str(GPU_DEVICE_LOST_MESSAGE),
129            Self::GpuDeviceHung {
130                frame,
131                gpu_ms,
132                watchdog_ms,
133                ..
134            } => {
135                formatter.write_str("GPU device hung: ")?;
136                match frame {
137                    Some(frame) => write!(formatter, "frame {frame}")?,
138                    None => formatter.write_str("a submission")?,
139                }
140                write!(
141                    formatter,
142                    " ran {gpu_ms} ms in one submission against a {watchdog_ms} ms watchdog; this \
143                     work stops the GPU, so it is not retried on the GPU"
144                )
145            }
146            Self::UnsupportedHdr => {
147                formatter.write_str("the color policy refuses HDR (PQ or HLG) sources")
148            }
149            Self::InvalidSource => {
150                formatter.write_str("the source belongs to another renderer backend")
151            }
152        }
153    }
154}
155
156impl std::error::Error for RenderError {}
157
158/// Where a texture readback failed.
159///
160/// `wgpu`'s `BufferAsyncError` carries no cause, so [`Self::MapFailed`] only says mapping was
161/// refused; device diagnostics (`alpha2_gpu::GpuDiagnosticLog`) may explain it. None of these
162/// implies memory exhaustion.
163#[derive(Copy, Clone, Debug, Eq, PartialEq)]
164#[non_exhaustive]
165pub enum ReadbackFailure {
166    /// The rendered texture's format or size is not one the readback packs.
167    UnsupportedTarget,
168    /// A row, plane, or buffer size overflowed or could not be represented.
169    Layout,
170    /// Mapping was requested twice for one readback.
171    DuplicateMapRequest,
172    /// The GPU refused to map the staging buffer.
173    MapFailed,
174    /// The mapping callback was dropped without reporting.
175    CallbackDropped,
176    /// The staging buffer was read before its mapping succeeded.
177    NotMapped,
178    /// A caller-supplied plane destination is too small for the frame.
179    DestinationTooSmall,
180    /// The device was polled for a submission it did not recognise.
181    SubmissionMismatch,
182}
183
184impl ReadbackFailure {
185    /// Stable lowercase name for logs and protocols.
186    #[must_use]
187    pub const fn as_str(self) -> &'static str {
188        match self {
189            Self::UnsupportedTarget => "unsupported-target",
190            Self::Layout => "layout",
191            Self::DuplicateMapRequest => "duplicate-map-request",
192            Self::MapFailed => "map-failed",
193            Self::CallbackDropped => "callback-dropped",
194            Self::NotMapped => "not-mapped",
195            Self::DestinationTooSmall => "destination-too-small",
196            Self::SubmissionMismatch => "submission-mismatch",
197        }
198    }
199}
200
201impl fmt::Display for ReadbackFailure {
202    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
203        formatter.write_str(self.as_str())
204    }
205}
206
207/// Message every GPU wait timeout carries, so hosts can classify errors from text alone.
208pub use alpha2_media::GPU_TIMEOUT_MESSAGE;
209
210/// Message of [`RenderError::GpuWorkDropped`]; names a lost device, as hosts classify it.
211pub const GPU_WORK_DROPPED_MESSAGE: &str = "GPU device lost: it completed submitted work without \
212     running it (a readback lacked its canary), so this process can no longer render";
213
214/// The message of [`RenderError::GpuDeviceLost`].
215pub const GPU_DEVICE_LOST_MESSAGE: &str = "GPU device lost: it can no longer make the resources \
216     a frame needs, so this process can no longer render";
217
218/// Default native wait for GPU work before giving up.
219pub const DEFAULT_GPU_WAIT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
220
221#[cfg(not(target_arch = "wasm32"))]
222#[allow(clippy::needless_pass_by_value)] // Shaped for `map_err`.
223pub(crate) fn poll_error(error: wgpu::PollError) -> RenderError {
224    match error {
225        wgpu::PollError::Timeout => RenderError::GpuTimeout,
226        wgpu::PollError::WrongSubmissionIndex(..) => {
227            RenderError::TextureReadback(ReadbackFailure::SubmissionMismatch)
228        }
229    }
230}