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 both answer with these codes, so a
4//! failure is the same code in a browser and on a server. The code comes from
5//! the variant that made the error, never from words in its message: messages
6//! carry project IDs and file paths a user chose. An error no type here
7//! names is the binding's to code where it refuses, and `ALPHA2_ERROR`
8//! otherwise.
9
10use std::error::Error;
11
12use alpha2_eval::{EvalError, PlanError};
13use alpha2_media::{DecodeError, EncodeError, GpuFault};
14use alpha2_render::{ColorError, GraphError, LutParseError, RenderError};
15use alpha2_text::TextRasterError;
16
17use crate::{EngineError, TextError};
18
19/// A failure's protocol code.
20#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
21pub enum ErrorCode {
22    /// The project, a range, a rate, a size, or another argument is invalid.
23    InvalidInput,
24    /// An effect asset, font, or frame the render needs is absent.
25    MissingInput,
26    /// A composition graph is still being built: an output or an input is
27    /// not connected yet.
28    IncompleteComposition,
29    /// A runtime effect's definition, parameters, or shader failed.
30    EffectError,
31    /// Encoding or muxing failed.
32    EncodeError,
33    /// The GPU device was lost, or no longer runs submitted work.
34    GpuDeviceLost,
35    /// GPU work did not complete within the wait timeout.
36    GpuTimeout,
37    /// A failure nothing classified.
38    Alpha2Error,
39}
40
41impl ErrorCode {
42    /// Every code, for tables and tests.
43    pub const ALL: [Self; 8] = [
44        Self::InvalidInput,
45        Self::MissingInput,
46        Self::IncompleteComposition,
47        Self::EffectError,
48        Self::EncodeError,
49        Self::GpuDeviceLost,
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::GpuTimeout => "GPU_TIMEOUT",
65            Self::Alpha2Error => "ALPHA2_ERROR",
66        }
67    }
68
69    /// The code of the first error in `error`'s source chain whose type the
70    /// engine knows, or `None` when none is.
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        // A decoder's or an encoder's failure, by its variant. A GPU call's fault is the binding's
106        // to read first (`gpu_fault_of`), and an output the encoder could not create the file
107        // system's.
108        if let Some(error) = error.downcast_ref::<DecodeError>() {
109            return Some(match error {
110                DecodeError::OutOfRange => Self::InvalidInput,
111                _ => Self::Alpha2Error,
112            });
113        }
114        if error.is::<EncodeError>() {
115            return Some(Self::EncodeError);
116        }
117        let invalid = error.is::<alpha2_model::ModelError>()
118            || error.is::<alpha2_model::ProjectLoadError>()
119            || error.is::<alpha2_time::TimeRangeError>()
120            || error.is::<alpha2_time::FrameRateError>()
121            || error.is::<alpha2_media::RateControlError>()
122            || error.is::<ColorError>()
123            || error.is::<LutParseError>();
124        invalid.then_some(Self::InvalidInput)
125    }
126}
127
128/// The class of error the GPU reported when it rejected a frame's work
129/// ([`RenderError::Gpu`]), for the first such error in `error`'s source
130/// chain. Out of memory and validation have no protocol code of their own;
131/// this is the evidence a failure report classifies them by.
132#[must_use]
133pub fn gpu_error_of(error: &(dyn Error + 'static)) -> Option<alpha2_gpu::GpuErrorKind> {
134    let render = |error: &RenderError| match error {
135        RenderError::Gpu { kind } => Some(*kind),
136        _ => None,
137    };
138    let mut cause = Some(error);
139    while let Some(error) = cause {
140        let kind = if let Some(error) = error.downcast_ref::<RenderError>() {
141            render(error)
142        } else if let Some(EngineError::Render(error)) = error.downcast_ref::<EngineError>() {
143            render(error)
144        } else if let Some(GraphError::Render(error)) = error.downcast_ref::<GraphError>() {
145            render(error)
146        } else {
147            None
148        };
149        if kind.is_some() {
150            return kind;
151        }
152        cause = error.source();
153    }
154    None
155}
156
157/// What the platform's status for a failed GPU call said about the device,
158/// for the first error in `error`'s source chain that carries one: a native
159/// synchronization the renderer met, or a decoder's or encoder's GPU call.
160/// A memory fault has no protocol code of its own; this is the evidence a
161/// failure report classifies it by.
162#[must_use]
163pub fn gpu_fault_of(error: &(dyn Error + 'static)) -> Option<GpuFault> {
164    let render = |error: &RenderError| match error {
165        RenderError::ExternalTextureSync(fault) => *fault,
166        _ => None,
167    };
168    let mut cause = Some(error);
169    while let Some(error) = cause {
170        let fault = if let Some(error) = error.downcast_ref::<RenderError>() {
171            render(error)
172        } else if let Some(EngineError::Render(error)) = error.downcast_ref::<EngineError>() {
173            render(error)
174        } else if let Some(GraphError::Render(error)) = error.downcast_ref::<GraphError>() {
175            render(error)
176        } else if let Some(EncodeError::Gpu { fault, .. }) = error.downcast_ref::<EncodeError>() {
177            Some(*fault)
178        } else if let Some(DecodeError::Gpu { fault, .. }) = error.downcast_ref::<DecodeError>() {
179            Some(*fault)
180        } else {
181            None
182        };
183        if fault.is_some() {
184            return fault;
185        }
186        cause = error.source();
187    }
188    None
189}
190
191impl EngineError {
192    /// This failure's protocol code.
193    #[must_use]
194    pub fn code(&self) -> ErrorCode {
195        match self {
196            Self::Evaluate(error) => eval_code(error),
197            Self::Render(error) => render_code(*error),
198            Self::Text(error) => text_error_code(error),
199            Self::InvalidQualityProfile
200            | Self::OutOfRange
201            | Self::EmptyExportRange
202            | Self::ExportTooLarge { .. }
203            | Self::InvalidAudioConfiguration
204            | Self::InvalidNormalizationGain
205            | Self::OddExportDimensions { .. } => ErrorCode::InvalidInput,
206            // Like an index built for another project: the caller's mistake, not the
207            // project's.
208            Self::ExportProjectChanged => ErrorCode::Alpha2Error,
209            Self::UnregisteredEffect { .. } => ErrorCode::EffectError,
210        }
211    }
212}
213
214fn text_error_code(error: &TextError) -> ErrorCode {
215    match error {
216        TextError::Raster(error) => text_code(error),
217        TextError::Render(error) => render_code(*error),
218    }
219}
220
221fn text_code(error: &TextRasterError) -> ErrorCode {
222    match error {
223        TextRasterError::NoFonts => ErrorCode::MissingInput,
224        _ => ErrorCode::InvalidInput,
225    }
226}
227
228fn eval_code(error: &EvalError) -> ErrorCode {
229    match error {
230        EvalError::IncompleteComposition { reason, .. } => match reason {
231            PlanError::NoOutput
232            | PlanError::MissingInput { .. }
233            | PlanError::UnboundGroupInput { .. }
234            | PlanError::ClipWithoutClip { .. } => ErrorCode::IncompleteComposition,
235            PlanError::NestedTooDeep { .. } | PlanError::TooLarge => ErrorCode::InvalidInput,
236        },
237        EvalError::InvalidProject(_) | EvalError::UnmappableClip { .. } => ErrorCode::InvalidInput,
238        // An index built for another project: the caller's mistake, not the project's.
239        _ => ErrorCode::Alpha2Error,
240    }
241}
242
243fn render_code(error: RenderError) -> ErrorCode {
244    match error {
245        RenderError::ZeroDimension
246        | RenderError::DimensionTooLarge
247        | RenderError::InvalidColor
248        | RenderError::InvalidVisual
249        | RenderError::InvalidImageData
250        | RenderError::InvalidGraph
251        | RenderError::UnsupportedHdr => ErrorCode::InvalidInput,
252        RenderError::InvalidEffectParameter
253        | RenderError::UnknownEffect
254        | RenderError::InvalidEffectParams
255        | RenderError::InvalidEffectResource
256        | RenderError::EffectResourceKindMismatch
257        | RenderError::MissingEffectInput => ErrorCode::EffectError,
258        RenderError::MissingEffectResource => ErrorCode::MissingInput,
259        RenderError::GpuTimeout | RenderError::ExternalTextureSync(Some(GpuFault::Timeout)) => {
260            ErrorCode::GpuTimeout
261        }
262        RenderError::GpuWorkDropped
263        | RenderError::GpuDeviceLost
264        | RenderError::ExternalTextureSync(Some(GpuFault::DeviceLost)) => ErrorCode::GpuDeviceLost,
265        // The GPU rejected the frame's work: one of the engine's own steps failed, not the caller's
266        // input.
267        _ => ErrorCode::Alpha2Error,
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn a_code_is_the_variants_whatever_its_message_says() {
277        // The odd-size refusal's message says "must": the code must come from the variant.
278        let odd = EngineError::OddExportDimensions {
279            width: 65,
280            height: 49,
281            suggested: (64, 48),
282        };
283        assert_eq!(odd.code(), ErrorCode::InvalidInput);
284        assert_eq!(ErrorCode::of(&odd), Some(ErrorCode::InvalidInput));
285        let too_large = EngineError::ExportTooLarge {
286            frames: 2,
287            maximum: 1,
288        };
289        assert_eq!(too_large.code(), ErrorCode::InvalidInput);
290        let no_fonts = EngineError::Text(TextError::Raster(TextRasterError::NoFonts));
291        assert_eq!(no_fonts.code(), ErrorCode::MissingInput);
292        assert_eq!(
293            EngineError::Render(RenderError::GpuWorkDropped).code(),
294            ErrorCode::GpuDeviceLost
295        );
296        assert_eq!(
297            EngineError::Render(RenderError::GpuDeviceLost).code(),
298            ErrorCode::GpuDeviceLost
299        );
300        // A rejected frame is not a lost device, whatever the device said about it.
301        for kind in [
302            alpha2_gpu::GpuErrorKind::OutOfMemory,
303            alpha2_gpu::GpuErrorKind::Validation,
304            alpha2_gpu::GpuErrorKind::Internal,
305        ] {
306            let rejected = EngineError::Render(RenderError::Gpu { kind });
307            assert_eq!(rejected.code(), ErrorCode::Alpha2Error);
308            assert_eq!(gpu_error_of(&rejected), Some(kind));
309            assert_eq!(
310                gpu_error_of(&GraphError::Render(RenderError::Gpu { kind })),
311                Some(kind)
312            );
313        }
314        assert_eq!(gpu_error_of(&RenderError::GpuWorkDropped), None);
315        assert_eq!(
316            ErrorCode::of(&RenderError::GpuTimeout),
317            Some(ErrorCode::GpuTimeout)
318        );
319        // The project's color policy refused the source: the input, not an effect parameter.
320        assert_eq!(
321            ErrorCode::of(&RenderError::UnsupportedHdr),
322            Some(ErrorCode::InvalidInput)
323        );
324        let model = alpha2_model::ModelError::UnsupportedSchemaVersion { found: 0 };
325        assert_eq!(ErrorCode::of(&model), Some(ErrorCode::InvalidInput));
326        assert_eq!(ErrorCode::of(&std::io::Error::other("device lost")), None);
327    }
328
329    #[test]
330    fn media_errors_take_the_code_their_variant_names_whatever_their_words() {
331        // Words that read like other codes (an adapter's text quotes paths and FFmpeg's messages)
332        // decide nothing.
333        let words = "missing shader: GPU device lost, invalid encode".to_owned();
334        for (error, code) in [
335            (
336                &DecodeError::Adapter(words.clone()) as &(dyn Error + 'static),
337                ErrorCode::Alpha2Error,
338            ),
339            (
340                &DecodeError::Unsupported(words.clone()),
341                ErrorCode::Alpha2Error,
342            ),
343            (&DecodeError::Cancelled, ErrorCode::Alpha2Error),
344            (&DecodeError::OutOfRange, ErrorCode::InvalidInput),
345            (&EncodeError::Adapter(words.clone()), ErrorCode::EncodeError),
346            (&EncodeError::Cancelled, ErrorCode::EncodeError),
347            (&EncodeError::InvalidTimestamp, ErrorCode::EncodeError),
348        ] {
349            assert_eq!(ErrorCode::of(error), Some(code), "{error}");
350        }
351    }
352
353    #[test]
354    fn every_code_has_its_own_name() {
355        let mut names: Vec<_> = ErrorCode::ALL.iter().map(|code| code.as_str()).collect();
356        names.sort_unstable();
357        names.dedup();
358        assert_eq!(names.len(), ErrorCode::ALL.len());
359    }
360}