1mod native_source;
4
5use j2k::J2kError;
6#[cfg(target_os = "macos")]
7use j2k::{BackendError, BackendErrorKind};
8use j2k_core::{
9 adapter_error_is_buffer_error, adapter_error_is_not_implemented, adapter_error_is_truncated,
10 adapter_error_is_unsupported, AdapterErrorKind, AdapterErrorParts, BackendRequest,
11 BatchInfrastructureError, BufferError, CodecError,
12};
13use j2k_metal_support::MetalSupportError;
14#[cfg(any(test, target_os = "macos"))]
15use j2k_native::{DecodeError as NativeDecodeError, EncodeError as NativeEncodeError};
16
17pub use native_source::NativeBackendError;
18
19#[derive(Debug, thiserror::Error)]
20pub enum Error {
22 #[error(transparent)]
24 Decode(#[from] J2kError),
25 #[error("{context}: {source}")]
27 NativeDecode {
28 context: &'static str,
30 #[source]
32 source: NativeBackendError,
33 },
34 #[error("native J2K encode error during {operation}: {source}")]
36 NativeEncode {
37 operation: &'static str,
39 #[source]
41 source: NativeBackendError,
42 },
43 #[error(transparent)]
45 Buffer(#[from] BufferError),
46 #[error("{0}")]
48 BatchInfrastructure(
49 #[from]
50 #[source]
51 BatchInfrastructureError,
52 ),
53 #[error("backend request {request:?} is not supported by j2k-metal")]
55 UnsupportedBackend {
56 request: BackendRequest,
58 },
59 #[error("unsupported J2K Metal request: {reason}")]
61 UnsupportedMetalRequest {
62 reason: &'static str,
64 },
65 #[error("Metal is unavailable on this host")]
67 MetalUnavailable,
68 #[error("Metal runtime error: {message}")]
70 MetalRuntime {
71 message: String,
73 },
74 #[error("{message}")]
76 MetalSupport {
77 message: String,
79 #[source]
81 source: MetalSupportError,
82 },
83 #[error("Metal kernel error: {context}: allocation failed: {source}")]
85 PreparedPlanCacheAllocation {
86 context: &'static str,
88 #[source]
90 source: std::collections::TryReserveError,
91 },
92 #[error("Metal kernel error: {context}: cache invariant failed: {reason}")]
94 PreparedPlanCacheInvariant {
95 context: &'static str,
97 reason: &'static str,
99 },
100 #[error("Metal kernel error: {message}")]
102 MetalKernel {
103 message: String,
105 },
106 #[error("Metal kernel error: {message}")]
108 MetalKernelRetryable {
109 message: String,
111 retry_class: MetalKernelRetryClass,
113 },
114 #[error("Metal kernel error: {message}")]
116 MetalDirectFallback {
117 message: String,
119 reason: MetalDirectFallbackReason,
121 },
122 #[error("Metal state `{state}` is poisoned")]
124 MetalStatePoisoned {
125 state: &'static str,
127 },
128 #[error("Metal state `{state}` invariant failed: {reason}")]
130 MetalStateInvariant {
131 state: &'static str,
133 reason: &'static str,
135 },
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140#[non_exhaustive]
141pub enum MetalDirectFallbackReason {
142 UnsupportedPlan,
144 UnsupportedRuntimeInput,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150#[non_exhaustive]
151pub enum MetalKernelRetryClass {
152 ResidentClassicBatch,
154 ResidentHtBatch,
156 ResidentClassicOrHtBatch,
158}
159
160impl MetalKernelRetryClass {
161 #[cfg(target_os = "macos")]
162 fn applies_to(self, requested: Self) -> bool {
163 self == requested
164 || matches!(
165 (self, requested),
166 (
167 Self::ResidentClassicOrHtBatch,
168 Self::ResidentClassicBatch | Self::ResidentHtBatch
169 )
170 )
171 }
172}
173
174impl Error {
175 #[doc(hidden)]
178 #[must_use]
179 pub fn session_is_unusable(&self) -> bool {
180 matches!(
181 self,
182 Self::BatchInfrastructure(_)
183 | Self::PreparedPlanCacheAllocation { .. }
184 | Self::PreparedPlanCacheInvariant { .. }
185 | Self::MetalUnavailable
186 | Self::MetalRuntime { .. }
187 | Self::MetalStatePoisoned { .. }
188 | Self::MetalStateInvariant { .. }
189 )
190 }
191
192 #[cfg(target_os = "macos")]
193 pub(crate) fn is_conservative_retry_candidate(&self, requested: MetalKernelRetryClass) -> bool {
194 match self {
195 Self::MetalKernelRetryable { retry_class, .. } => retry_class.applies_to(requested),
196 _ => false,
197 }
198 }
199
200 #[cfg(target_os = "macos")]
201 pub(crate) fn is_direct_fallback(&self) -> bool {
202 matches!(self, Self::MetalDirectFallback { .. })
203 }
204}
205
206#[doc(hidden)]
207impl AdapterErrorParts for Error {
208 fn source_codec_error(&self) -> Option<&dyn CodecError> {
209 match self {
210 Self::Decode(inner) => Some(inner),
211 _ => None,
212 }
213 }
214
215 fn adapter_error_kind(&self) -> AdapterErrorKind {
216 match self {
217 Self::Buffer(_) => AdapterErrorKind::Buffer,
218 Self::UnsupportedBackend { .. }
219 | Self::UnsupportedMetalRequest { .. }
220 | Self::MetalUnavailable
221 | Self::MetalDirectFallback { .. } => AdapterErrorKind::Unsupported,
222 Self::Decode(_)
223 | Self::NativeDecode { .. }
224 | Self::NativeEncode { .. }
225 | Self::BatchInfrastructure(_)
226 | Self::MetalRuntime { .. }
227 | Self::MetalSupport { .. }
228 | Self::PreparedPlanCacheAllocation { .. }
229 | Self::PreparedPlanCacheInvariant { .. }
230 | Self::MetalKernel { .. }
231 | Self::MetalKernelRetryable { .. }
232 | Self::MetalStatePoisoned { .. }
233 | Self::MetalStateInvariant { .. } => AdapterErrorKind::Other,
234 }
235 }
236}
237
238#[doc(hidden)]
239impl CodecError for Error {
240 fn is_truncated(&self) -> bool {
241 matches!(
242 self,
243 Self::NativeDecode { source, .. } if source.is_decode_truncated()
244 ) || adapter_error_is_truncated(self)
245 }
246
247 fn is_not_implemented(&self) -> bool {
248 adapter_error_is_not_implemented(self)
249 }
250
251 fn is_unsupported(&self) -> bool {
252 matches!(
253 self,
254 Self::NativeDecode { source, .. } if source.is_unsupported()
255 ) || matches!(
256 self,
257 Self::NativeEncode { source, .. } if source.is_unsupported()
258 ) || adapter_error_is_unsupported(self)
259 }
260
261 fn is_buffer_error(&self) -> bool {
262 adapter_error_is_buffer_error(self)
263 }
264}
265
266#[cfg(any(test, target_os = "macos"))]
267pub(crate) fn native_decode_error(error: NativeDecodeError) -> Error {
268 Error::NativeDecode {
269 context: "native JPEG 2000 backend failed",
270 source: NativeBackendError::decode(error),
271 }
272}
273
274#[cfg(any(test, target_os = "macos"))]
275pub(crate) fn native_encode_error(operation: &'static str, source: NativeEncodeError) -> Error {
276 Error::NativeEncode {
277 operation,
278 source: NativeBackendError::encode(source),
279 }
280}
281
282#[cfg(any(test, target_os = "macos"))]
283pub(crate) fn metal_kernel_support_error(
284 message: impl Into<String>,
285 source: MetalSupportError,
286) -> Error {
287 Error::MetalSupport {
288 message: format!("Metal kernel error: {}", message.into()),
289 source,
290 }
291}
292
293#[cfg(any(test, target_os = "macos"))]
294pub(crate) fn metal_runtime_support_error(source: &MetalSupportError) -> Error {
295 if source.is_unavailable() {
296 Error::MetalUnavailable
297 } else {
298 Error::MetalSupport {
299 message: format!("Metal runtime error: {source}"),
300 source: source.clone(),
301 }
302 }
303}
304
305#[cfg(target_os = "macos")]
306pub(crate) fn adapter_backend_error(message: impl Into<String>) -> J2kError {
307 J2kError::Backend(BackendError::new(BackendErrorKind::Other, message))
308}
309
310#[cfg(test)]
311mod tests {
312 use j2k_core::CodecError;
313 use j2k_metal_support::MetalSupportError;
314 use j2k_native::{
315 DecodeError as NativeDecodeError, DecodingError as NativeDecodingError,
316 DirectPlanUnsupportedReason as NativeDirectPlanUnsupportedReason,
317 EncodeError as NativeEncodeError,
318 };
319
320 use super::{
321 metal_kernel_support_error, metal_runtime_support_error, native_decode_error,
322 native_encode_error, Error, NativeBackendError,
323 };
324
325 #[test]
326 fn native_decode_unsupported_error_keeps_codec_classification() {
327 let error = native_decode_error(NativeDecodeError::Decoding(
328 NativeDecodingError::UnsupportedFeature("test feature"),
329 ));
330
331 assert!(error.is_unsupported());
332 assert!(!error.is_truncated());
333 assert!(!error.is_not_implemented());
334 }
335
336 #[test]
337 fn native_encode_crossing_preserves_operation_and_concrete_source() {
338 let error = native_encode_error(
339 "classic Tier-1 token pack",
340 NativeEncodeError::ArithmeticOverflow {
341 what: "test token length",
342 },
343 );
344
345 assert!(error.to_string().contains("classic Tier-1 token pack"));
346 assert!(matches!(
347 &error,
348 Error::NativeEncode { operation, source }
349 if *operation == "classic Tier-1 token pack"
350 && source == &NativeBackendError::encode(
351 NativeEncodeError::ArithmeticOverflow {
352 what: "test token length",
353 }
354 )
355 ));
356 let opaque = std::error::Error::source(&error).expect("opaque adapter source");
357 assert!(opaque.downcast_ref::<NativeBackendError>().is_some());
358 let concrete = opaque.source().expect("concrete native encode source");
359 assert!(matches!(
360 concrete.downcast_ref::<NativeEncodeError>(),
361 Some(NativeEncodeError::ArithmeticOverflow {
362 what: "test token length"
363 })
364 ));
365 }
366
367 #[test]
368 fn native_decode_direct_plan_error_keeps_codec_classification() {
369 let error = native_decode_error(NativeDecodeError::Decoding(
370 NativeDecodingError::DirectPlanUnsupported(
371 NativeDirectPlanUnsupportedReason::ColorSingleTileCodestream,
372 ),
373 ));
374
375 assert!(error.is_unsupported());
376 assert!(!error.is_truncated());
377 assert!(!error.is_not_implemented());
378 }
379
380 #[test]
381 fn native_decode_unexpected_eof_keeps_codec_classification() {
382 let error = native_decode_error(NativeDecodeError::Decoding(
383 NativeDecodingError::UnexpectedEof,
384 ));
385
386 assert!(error.is_truncated());
387 assert!(!error.is_unsupported());
388 }
389
390 #[test]
391 fn native_decode_resource_errors_preserve_typed_sources() {
392 let sources = [
393 NativeDecodeError::AllocationTooLarge {
394 what: "Metal decode fixture",
395 requested: 9,
396 cap: 8,
397 },
398 NativeDecodeError::HostAllocationFailed {
399 what: "Metal decode fixture",
400 bytes: 7,
401 },
402 ];
403
404 for source in sources {
405 let error = native_decode_error(source);
406 assert!(matches!(
407 &error,
408 Error::NativeDecode {
409 context: "native JPEG 2000 backend failed",
410 source: stored,
411 } if stored == &NativeBackendError::decode(source)
412 ));
413 let opaque = core::error::Error::source(&error).expect("opaque adapter source");
414 assert!(opaque.downcast_ref::<NativeBackendError>().is_some());
415 let concrete = opaque.source().expect("concrete native decode source");
416 assert_eq!(concrete.downcast_ref::<NativeDecodeError>(), Some(&source));
417 assert!(!error.is_buffer_error());
418 assert!(!error.is_unsupported());
419 }
420 }
421
422 #[test]
423 fn metal_support_error_keeps_display_source_and_other_classification() {
424 let source = MetalSupportError::BufferAlignment {
425 offset_bytes: 1,
426 align: 4,
427 };
428 let error = metal_kernel_support_error(
429 format!("J2K Metal status readback buffer access invalid: {source}"),
430 source.clone(),
431 );
432
433 assert_eq!(
434 error.to_string(),
435 "Metal kernel error: J2K Metal status readback buffer access invalid: Metal buffer range offset 1 is not aligned to 4 bytes"
436 );
437 assert!(matches!(
438 &error,
439 Error::MetalSupport { source: stored, .. } if stored == &source
440 ));
441 let chained = std::error::Error::source(&error).expect("typed Metal support source");
442 assert!(chained.downcast_ref::<MetalSupportError>().is_some());
443 assert!(!error.is_unsupported());
444 assert!(!error.is_buffer_error());
445 }
446
447 #[test]
448 fn runtime_unavailability_keeps_existing_unsupported_route() {
449 let error = metal_runtime_support_error(&MetalSupportError::MetalUnavailable);
450
451 assert!(matches!(error, Error::MetalUnavailable));
452 assert!(error.is_unsupported());
453 }
454
455 #[test]
456 fn persistent_session_classification_is_owned_by_metal_error() {
457 assert!(Error::MetalUnavailable.session_is_unusable());
458 assert!(Error::MetalRuntime {
459 message: "test runtime failure".to_string(),
460 }
461 .session_is_unusable());
462 assert!(Error::MetalStateInvariant {
463 state: "test state",
464 reason: "test invariant",
465 }
466 .session_is_unusable());
467 assert!(!Error::UnsupportedMetalRequest {
468 reason: "test unsupported request",
469 }
470 .session_is_unusable());
471 assert!(!Error::MetalKernel {
472 message: "test group status".to_string(),
473 }
474 .session_is_unusable());
475 }
476}