1use 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#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
21pub enum ErrorCode {
22 InvalidInput,
24 MissingInput,
26 IncompleteComposition,
29 EffectError,
31 EncodeError,
33 GpuDeviceLost,
35 GpuTimeout,
37 Alpha2Error,
39}
40
41impl ErrorCode {
42 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 #[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 #[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 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#[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#[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 #[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 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 _ => 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 _ => 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 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 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 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 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}