1use 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#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
18pub enum ErrorCode {
19 InvalidInput,
21 MissingInput,
23 IncompleteComposition,
25 EffectError,
27 EncodeError,
29 GpuDeviceLost,
31 GpuDeviceHung,
34 GpuTimeout,
36 Alpha2Error,
38}
39
40impl ErrorCode {
41 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 #[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 #[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>() {
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#[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#[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 #[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 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 _ => 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 _ => 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 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}