Skip to main content

alpha2_engine/
error_code.rs

1//! The protocol code of an engine failure, decided by the error's variant.
2//!
3//! The native host and the wasm binding share these codes. The code comes from the error variant,
4//! never from message text, which can carry user-chosen IDs and paths. Errors of unknown types
5//! are coded by the binding, defaulting to `ALPHA2_ERROR`.
6
7use std::error::Error;
8
9use alpha2_eval::{EvalError, PlanError};
10use alpha2_media::{DecodeError, EncodeError, GpuFault};
11use alpha2_render::{ColorError, GraphError, LutParseError, RenderError};
12use alpha2_text::TextRasterError;
13
14use crate::{EngineError, TextError};
15
16/// A failure's protocol code.
17#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
18pub enum ErrorCode {
19    /// The project, a range, a rate, a size, or another argument is invalid.
20    InvalidInput,
21    /// An effect asset, font, or frame the render needs is absent.
22    MissingInput,
23    /// A composition graph has an unconnected output or input.
24    IncompleteComposition,
25    /// A runtime effect's definition, parameters, or shader failed.
26    EffectError,
27    /// Encoding or muxing failed.
28    EncodeError,
29    /// The GPU device was lost, or no longer runs submitted work.
30    GpuDeviceLost,
31    /// The device was lost to this work: a submission ran for most of the OS GPU watchdog, or the
32    /// platform reported a reset for a long command. Retrying will hang the GPU again.
33    GpuDeviceHung,
34    /// GPU work did not complete within the wait timeout.
35    GpuTimeout,
36    /// A failure nothing classified.
37    Alpha2Error,
38}
39
40impl ErrorCode {
41    /// Every code, for tables and tests.
42    pub const ALL: [Self; 9] = [
43        Self::InvalidInput,
44        Self::MissingInput,
45        Self::IncompleteComposition,
46        Self::EffectError,
47        Self::EncodeError,
48        Self::GpuDeviceLost,
49        Self::GpuDeviceHung,
50        Self::GpuTimeout,
51        Self::Alpha2Error,
52    ];
53
54    /// The code as the protocol spells it.
55    #[must_use]
56    pub const fn as_str(self) -> &'static str {
57        match self {
58            Self::InvalidInput => "INVALID_INPUT",
59            Self::MissingInput => "MISSING_INPUT",
60            Self::IncompleteComposition => "INCOMPLETE_COMPOSITION",
61            Self::EffectError => "EFFECT_ERROR",
62            Self::EncodeError => "ENCODE_ERROR",
63            Self::GpuDeviceLost => "GPU_DEVICE_LOST",
64            Self::GpuDeviceHung => "GPU_DEVICE_HUNG",
65            Self::GpuTimeout => "GPU_TIMEOUT",
66            Self::Alpha2Error => "ALPHA2_ERROR",
67        }
68    }
69
70    /// The code of the first error in `error`'s source chain with a known type.
71    #[must_use]
72    pub fn of(error: &(dyn Error + 'static)) -> Option<Self> {
73        let mut cause = Some(error);
74        while let Some(error) = cause {
75            if let Some(code) = Self::of_one(error) {
76                return Some(code);
77            }
78            cause = error.source();
79        }
80        None
81    }
82
83    fn of_one(error: &(dyn Error + 'static)) -> Option<Self> {
84        if let Some(error) = error.downcast_ref::<EngineError>() {
85            return Some(error.code());
86        }
87        if let Some(error) = error.downcast_ref::<EvalError>() {
88            return Some(eval_code(error));
89        }
90        if let Some(error) = error.downcast_ref::<RenderError>() {
91            return Some(render_code(*error));
92        }
93        if let Some(error) = error.downcast_ref::<TextError>() {
94            return Some(text_error_code(error));
95        }
96        if let Some(error) = error.downcast_ref::<TextRasterError>() {
97            return Some(text_code(error));
98        }
99        if let Some(error) = error.downcast_ref::<GraphError>() {
100            return Some(match error {
101                GraphError::Render(error) => render_code(*error),
102                _ => Self::InvalidInput,
103            });
104        }
105        // GPU call faults are read first by the binding (`gpu_fault_of`).
106        if let Some(error) = error.downcast_ref::<DecodeError>() {
107            return Some(match error {
108                DecodeError::OutOfRange => Self::InvalidInput,
109                _ => Self::Alpha2Error,
110            });
111        }
112        if error.is::<EncodeError>() {
113            return Some(Self::EncodeError);
114        }
115        let invalid = error.is::<alpha2_model::ModelError>()
116            || error.is::<alpha2_model::ProjectLoadError>()
117            || error.is::<alpha2_time::TimeRangeError>()
118            || error.is::<alpha2_time::FrameRateError>()
119            || error.is::<alpha2_media::RateControlError>()
120            || error.is::<ColorError>()
121            || error.is::<LutParseError>();
122        invalid.then_some(Self::InvalidInput)
123    }
124}
125
126/// The GPU error class of the first [`RenderError::Gpu`] in `error`'s source chain. Out of memory
127/// and validation have no protocol code; failure reports classify them with this.
128#[must_use]
129pub fn gpu_error_of(error: &(dyn Error + 'static)) -> Option<alpha2_gpu::GpuErrorKind> {
130    let render = |error: &RenderError| match error {
131        RenderError::Gpu { kind } => Some(*kind),
132        _ => None,
133    };
134    let mut cause = Some(error);
135    while let Some(error) = cause {
136        let kind = if let Some(error) = error.downcast_ref::<RenderError>() {
137            render(error)
138        } else if let Some(EngineError::Render(error)) = error.downcast_ref::<EngineError>() {
139            render(error)
140        } else if let Some(GraphError::Render(error)) = error.downcast_ref::<GraphError>() {
141            render(error)
142        } else {
143            None
144        };
145        if kind.is_some() {
146            return kind;
147        }
148        cause = error.source();
149    }
150    None
151}
152
153/// The device fault reported for the first failed GPU call in `error`'s source chain (renderer
154/// synchronization, decoder or encoder). Failure reports classify memory faults with this.
155#[must_use]
156pub fn gpu_fault_of(error: &(dyn Error + 'static)) -> Option<GpuFault> {
157    let render = |error: &RenderError| match error {
158        RenderError::ExternalTextureSync(fault) => *fault,
159        _ => None,
160    };
161    let mut cause = Some(error);
162    while let Some(error) = cause {
163        let fault = if let Some(error) = error.downcast_ref::<RenderError>() {
164            render(error)
165        } else if let Some(EngineError::Render(error)) = error.downcast_ref::<EngineError>() {
166            render(error)
167        } else if let Some(GraphError::Render(error)) = error.downcast_ref::<GraphError>() {
168            render(error)
169        } else if let Some(EncodeError::Gpu { fault, .. }) = error.downcast_ref::<EncodeError>() {
170            Some(*fault)
171        } else if let Some(DecodeError::Gpu { fault, .. }) = error.downcast_ref::<DecodeError>() {
172            Some(*fault)
173        } else {
174            None
175        };
176        if fault.is_some() {
177            return fault;
178        }
179        cause = error.source();
180    }
181    None
182}
183
184impl EngineError {
185    /// This failure's protocol code.
186    #[must_use]
187    pub fn code(&self) -> ErrorCode {
188        match self {
189            Self::Evaluate(error) => eval_code(error),
190            Self::Render(error) => render_code(*error),
191            Self::Text(error) => text_error_code(error),
192            Self::InvalidQualityProfile
193            | Self::OutOfRange
194            | Self::EmptyExportRange
195            | Self::ExportTooLarge { .. }
196            | Self::InvalidAudioConfiguration
197            | Self::InvalidNormalizationGain
198            | Self::OddExportDimensions { .. } => ErrorCode::InvalidInput,
199            // The caller's mistake, not the project's.
200            Self::ExportProjectChanged => ErrorCode::Alpha2Error,
201            Self::UnregisteredEffect { .. } => ErrorCode::EffectError,
202        }
203    }
204}
205
206fn text_error_code(error: &TextError) -> ErrorCode {
207    match error {
208        TextError::Raster(error) => text_code(error),
209        TextError::Render(error) => render_code(*error),
210    }
211}
212
213fn text_code(error: &TextRasterError) -> ErrorCode {
214    match error {
215        TextRasterError::NoFonts => ErrorCode::MissingInput,
216        _ => ErrorCode::InvalidInput,
217    }
218}
219
220fn eval_code(error: &EvalError) -> ErrorCode {
221    match error {
222        EvalError::IncompleteComposition { reason, .. } => match reason {
223            PlanError::NoOutput
224            | PlanError::MissingInput { .. }
225            | PlanError::UnboundGroupInput { .. }
226            | PlanError::ClipWithoutClip { .. } => ErrorCode::IncompleteComposition,
227            PlanError::NestedTooDeep { .. } | PlanError::TooLarge => ErrorCode::InvalidInput,
228        },
229        EvalError::InvalidProject(_) | EvalError::UnmappableClip { .. } => ErrorCode::InvalidInput,
230        // An index built for another project is the caller's mistake.
231        _ => ErrorCode::Alpha2Error,
232    }
233}
234
235fn render_code(error: RenderError) -> ErrorCode {
236    match error {
237        RenderError::ZeroDimension
238        | RenderError::DimensionTooLarge
239        | RenderError::InvalidColor
240        | RenderError::InvalidVisual
241        | RenderError::InvalidImageData
242        | RenderError::InvalidGraph
243        | RenderError::UnsupportedHdr => ErrorCode::InvalidInput,
244        RenderError::InvalidEffectParameter
245        | RenderError::UnknownEffect
246        | RenderError::InvalidEffectParams
247        | RenderError::InvalidEffectResource
248        | RenderError::EffectResourceKindMismatch
249        | RenderError::MissingEffectInput => ErrorCode::EffectError,
250        RenderError::MissingEffectResource => ErrorCode::MissingInput,
251        RenderError::GpuTimeout | RenderError::ExternalTextureSync(Some(GpuFault::Timeout)) => {
252            ErrorCode::GpuTimeout
253        }
254        RenderError::GpuWorkDropped
255        | RenderError::GpuDeviceLost
256        | RenderError::ExternalTextureSync(Some(GpuFault::DeviceLost)) => ErrorCode::GpuDeviceLost,
257        RenderError::GpuDeviceHung { .. }
258        | RenderError::ExternalTextureSync(Some(GpuFault::DeviceHung)) => ErrorCode::GpuDeviceHung,
259        // A GPU rejection is the engine's failure, not the caller's input.
260        _ => ErrorCode::Alpha2Error,
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn a_code_is_the_variants_whatever_its_message_says() {
270        let odd = EngineError::OddExportDimensions {
271            width: 65,
272            height: 49,
273            suggested: (64, 48),
274        };
275        assert_eq!(odd.code(), ErrorCode::InvalidInput);
276        assert_eq!(ErrorCode::of(&odd), Some(ErrorCode::InvalidInput));
277        let too_large = EngineError::ExportTooLarge {
278            frames: 2,
279            maximum: 1,
280        };
281        assert_eq!(too_large.code(), ErrorCode::InvalidInput);
282        let no_fonts = EngineError::Text(TextError::Raster(TextRasterError::NoFonts));
283        assert_eq!(no_fonts.code(), ErrorCode::MissingInput);
284        assert_eq!(
285            EngineError::Render(RenderError::GpuWorkDropped).code(),
286            ErrorCode::GpuDeviceLost
287        );
288        assert_eq!(
289            EngineError::Render(RenderError::GpuDeviceLost).code(),
290            ErrorCode::GpuDeviceLost
291        );
292        // A rejected frame is not a lost device, whatever the device said about it.
293        for kind in [
294            alpha2_gpu::GpuErrorKind::OutOfMemory,
295            alpha2_gpu::GpuErrorKind::Validation,
296            alpha2_gpu::GpuErrorKind::Internal,
297        ] {
298            let rejected = EngineError::Render(RenderError::Gpu { kind });
299            assert_eq!(rejected.code(), ErrorCode::Alpha2Error);
300            assert_eq!(gpu_error_of(&rejected), Some(kind));
301            assert_eq!(
302                gpu_error_of(&GraphError::Render(RenderError::Gpu { kind })),
303                Some(kind)
304            );
305        }
306        assert_eq!(gpu_error_of(&RenderError::GpuWorkDropped), None);
307        assert_eq!(
308            ErrorCode::of(&RenderError::GpuTimeout),
309            Some(ErrorCode::GpuTimeout)
310        );
311        assert_eq!(
312            ErrorCode::of(&RenderError::UnsupportedHdr),
313            Some(ErrorCode::InvalidInput)
314        );
315        let model = alpha2_model::ModelError::UnsupportedSchemaVersion { found: 0 };
316        assert_eq!(ErrorCode::of(&model), Some(ErrorCode::InvalidInput));
317        assert_eq!(ErrorCode::of(&std::io::Error::other("device lost")), None);
318    }
319
320    #[test]
321    fn media_errors_take_the_code_their_variant_names_whatever_their_words() {
322        let words = "missing shader: GPU device lost, invalid encode".to_owned();
323        for (error, code) in [
324            (
325                &DecodeError::Adapter(words.clone()) as &(dyn Error + 'static),
326                ErrorCode::Alpha2Error,
327            ),
328            (
329                &DecodeError::Unsupported(words.clone()),
330                ErrorCode::Alpha2Error,
331            ),
332            (&DecodeError::Cancelled, ErrorCode::Alpha2Error),
333            (&DecodeError::OutOfRange, ErrorCode::InvalidInput),
334            (&EncodeError::Adapter(words.clone()), ErrorCode::EncodeError),
335            (&EncodeError::Cancelled, ErrorCode::EncodeError),
336            (&EncodeError::InvalidTimestamp, ErrorCode::EncodeError),
337        ] {
338            assert_eq!(ErrorCode::of(error), Some(code), "{error}");
339        }
340    }
341
342    #[test]
343    fn every_code_has_its_own_name() {
344        let mut names: Vec<_> = ErrorCode::ALL.iter().map(|code| code.as_str()).collect();
345        names.sort_unstable();
346        names.dedup();
347        assert_eq!(names.len(), ErrorCode::ALL.len());
348    }
349}