Skip to main content

j2k_cuda/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3mod native_source;
4
5use j2k::J2kError;
6use j2k_core::{
7    adapter_error_is_buffer_error, adapter_error_is_not_implemented, adapter_error_is_truncated,
8    adapter_error_is_unsupported, AdapterErrorKind, AdapterErrorParts, BackendRequest, BufferError,
9    CodecError,
10};
11use j2k_native::DecodeError as NativeDecodeError;
12
13pub use native_source::NativeBackendError;
14
15/// Error returned by the CUDA JPEG 2000 adapter.
16#[derive(Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum Error {
19    /// CPU JPEG 2000 decode failed.
20    #[error(transparent)]
21    Decode(#[from] J2kError),
22    /// Native decoder failure produced while preparing CUDA-resident work.
23    #[error("{context}: {source}")]
24    NativeDecode {
25        /// Stable adapter operation context.
26        context: &'static str,
27        /// Concrete native decoder failure.
28        #[source]
29        source: NativeBackendError,
30    },
31    /// Caller-owned output buffers were invalid.
32    #[error(transparent)]
33    Buffer(#[from] BufferError),
34    /// A host-side allocation needed by the adapter could not be reserved.
35    #[error("host allocation failed for {what}: {bytes} bytes")]
36    HostAllocationFailed {
37        /// Requested allocation size in bytes.
38        bytes: usize,
39        /// Logical allocation purpose.
40        what: &'static str,
41    },
42    /// Allocator-reported host capacity exceeds the codec phase budget.
43    #[error(
44        "host allocation capacity for {what} is too large: requested {requested} bytes, cap {cap} bytes"
45    )]
46    HostAllocationTooLarge {
47        /// Aggregate allocator-reported byte capacity.
48        requested: usize,
49        /// Maximum permitted simultaneously live host bytes.
50        cap: usize,
51        /// Logical phase or owner graph.
52        what: &'static str,
53    },
54    /// Bounded HTJ2K GPU job planning rejected one source job.
55    #[error(transparent)]
56    HtJobChunkPlan(#[from] j2k_core::HtGpuJobChunkPlanError),
57    /// Backend request is unsupported by this adapter.
58    #[error("backend request {request:?} is not supported by j2k-cuda")]
59    UnsupportedBackend {
60        /// Requested backend.
61        request: BackendRequest,
62    },
63    /// CUDA request is unsupported by the strict CUDA adapter contract.
64    #[error("unsupported CUDA request: {reason}")]
65    UnsupportedCudaRequest {
66        /// Human-readable rejection reason.
67        reason: &'static str,
68    },
69    /// CUDA runtime or device is unavailable.
70    #[error("CUDA is unavailable on this host")]
71    CudaUnavailable,
72    #[cfg(feature = "cuda-runtime")]
73    /// CUDA runtime returned an error.
74    #[error("CUDA runtime error: {source}")]
75    CudaRuntime {
76        /// Typed runtime failure, including nested completion or resource-release errors.
77        #[source]
78        source: j2k_cuda_runtime::CudaError,
79    },
80    #[cfg(feature = "cuda-runtime")]
81    /// A classic JPEG 2000 or HTJ2K tier-1 descriptor failed during GPU execution.
82    #[error(
83        "CUDA tier-1 source {source_index} job {original_job_index} failed during device execution: {source}"
84    )]
85    CudaTier1JobFailed {
86        /// Original caller input index that owns the failed job.
87        source_index: usize,
88        /// Stable job index before pass-bucket ordering and chunk splitting.
89        original_job_index: usize,
90        /// Indexed CUDA kernel failure.
91        #[source]
92        source: j2k_cuda_runtime::CudaError,
93    },
94    #[cfg(feature = "cuda-runtime")]
95    /// A CUDA operation failed and the required resource cleanup also failed.
96    #[error("CUDA operation failed ({primary}); CUDA cleanup also failed ({cleanup})")]
97    CudaCleanupFailed {
98        /// Error returned by the original operation.
99        primary: Box<Error>,
100        /// Error returned while synchronizing or releasing queued resources.
101        cleanup: Box<Error>,
102    },
103}
104
105impl From<j2k_core::HostPhaseError> for Error {
106    fn from(error: j2k_core::HostPhaseError) -> Self {
107        match error {
108            j2k_core::HostPhaseError::AllocationFailed {
109                requested_bytes,
110                what,
111            } => Self::HostAllocationFailed {
112                bytes: requested_bytes,
113                what,
114            },
115            j2k_core::HostPhaseError::LimitExceeded {
116                requested_bytes,
117                cap_bytes,
118                what,
119            } => Self::HostAllocationTooLarge {
120                requested: requested_bytes,
121                cap: cap_bytes,
122                what,
123            },
124        }
125    }
126}
127
128impl Error {
129    pub(crate) const fn capability_rejected(rejection: j2k_core::CapabilityRejection) -> Self {
130        Self::UnsupportedCudaRequest {
131            reason: rejection.reason(),
132        }
133    }
134
135    /// Whether this error can leave submitted CUDA work referencing an
136    /// external allocation whose completion was not established.
137    #[cfg(feature = "cuda-runtime")]
138    #[doc(hidden)]
139    pub fn completion_is_uncertain(&self) -> bool {
140        match self {
141            Self::CudaRuntime { source } | Self::CudaTier1JobFailed { source, .. } => {
142                source.completion_is_uncertain()
143            }
144            Self::CudaCleanupFailed { primary, cleanup } => {
145                primary.completion_is_uncertain() || cleanup.completion_is_uncertain()
146            }
147            _ => false,
148        }
149    }
150
151    /// Whether the persistent CUDA session cannot safely execute later groups.
152    #[doc(hidden)]
153    #[must_use]
154    pub fn session_is_unusable(&self) -> bool {
155        match self {
156            Self::CudaUnavailable => true,
157            #[cfg(feature = "cuda-runtime")]
158            Self::CudaRuntime { source } | Self::CudaTier1JobFailed { source, .. } => {
159                source.session_is_unusable()
160            }
161            #[cfg(feature = "cuda-runtime")]
162            Self::CudaCleanupFailed { primary, cleanup } => {
163                primary.session_is_unusable() || cleanup.session_is_unusable()
164            }
165            _ => false,
166        }
167    }
168}
169
170#[cfg(feature = "cuda-runtime")]
171pub(crate) fn combine_cuda_cleanup_errors(primary_error: Error, cleanup_error: Error) -> Error {
172    Error::CudaCleanupFailed {
173        primary: Box::new(primary_error),
174        cleanup: Box::new(cleanup_error),
175    }
176}
177
178#[doc(hidden)]
179impl AdapterErrorParts for Error {
180    fn source_codec_error(&self) -> Option<&dyn CodecError> {
181        match self {
182            Self::Decode(inner) => Some(inner),
183            _ => None,
184        }
185    }
186
187    fn adapter_error_kind(&self) -> AdapterErrorKind {
188        match self {
189            Self::Buffer(_) => AdapterErrorKind::Buffer,
190            Self::UnsupportedBackend { .. }
191            | Self::UnsupportedCudaRequest { .. }
192            | Self::CudaUnavailable => AdapterErrorKind::Unsupported,
193            Self::Decode(_)
194            | Self::NativeDecode { .. }
195            | Self::HostAllocationFailed { .. }
196            | Self::HostAllocationTooLarge { .. }
197            | Self::HtJobChunkPlan(_) => AdapterErrorKind::Other,
198            #[cfg(feature = "cuda-runtime")]
199            Self::CudaRuntime { .. }
200            | Self::CudaTier1JobFailed { .. }
201            | Self::CudaCleanupFailed { .. } => AdapterErrorKind::Other,
202        }
203    }
204}
205
206#[doc(hidden)]
207impl CodecError for Error {
208    fn is_truncated(&self) -> bool {
209        matches!(
210            self,
211            Self::NativeDecode { source, .. } if source.is_decode_truncated()
212        ) || adapter_error_is_truncated(self)
213    }
214
215    fn is_not_implemented(&self) -> bool {
216        adapter_error_is_not_implemented(self)
217    }
218
219    fn is_unsupported(&self) -> bool {
220        matches!(
221            self,
222            Self::NativeDecode { source, .. } if source.is_unsupported()
223        ) || adapter_error_is_unsupported(self)
224    }
225
226    fn is_buffer_error(&self) -> bool {
227        adapter_error_is_buffer_error(self)
228    }
229}
230
231#[cfg_attr(
232    not(any(test, feature = "cuda-runtime")),
233    expect(
234        dead_code,
235        reason = "native decode translation is used by CUDA decode and its tests"
236    )
237)]
238pub(crate) fn native_decode_error(error: NativeDecodeError) -> Error {
239    Error::NativeDecode {
240        context: "native JPEG 2000 backend failed",
241        source: NativeBackendError::decode(error),
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use j2k_core::CodecError;
248    use j2k_native::{
249        DecodeError as NativeDecodeError, DecodingError as NativeDecodingError,
250        DirectPlanUnsupportedReason as NativeDirectPlanUnsupportedReason,
251    };
252
253    #[cfg(feature = "cuda-runtime")]
254    use super::combine_cuda_cleanup_errors;
255    use super::{native_decode_error, Error, NativeBackendError};
256    #[cfg(feature = "cuda-runtime")]
257    use j2k_cuda_runtime::CudaError;
258
259    #[cfg(feature = "cuda-runtime")]
260    fn runtime_error(message: &str) -> Error {
261        Error::CudaRuntime {
262            source: CudaError::StatePoisoned {
263                message: message.to_string(),
264            },
265        }
266    }
267
268    #[test]
269    fn host_allocation_failure_is_an_operational_error_not_buffer_misuse() {
270        let error = Error::HostAllocationFailed {
271            bytes: 4096,
272            what: "test staging",
273        };
274        assert!(!error.is_buffer_error());
275        assert!(!error.is_unsupported());
276        assert!(error.to_string().contains("4096"));
277    }
278
279    #[test]
280    fn host_capacity_failure_preserves_actual_phase_budget() {
281        let error = Error::HostAllocationTooLarge {
282            requested: 17,
283            cap: 16,
284            what: "test phase",
285        };
286        assert!(!error.is_buffer_error());
287        assert!(!error.is_unsupported());
288        assert!(error.to_string().contains("17"));
289        assert!(error.to_string().contains("16"));
290    }
291
292    #[test]
293    fn native_decode_unsupported_error_keeps_codec_classification() {
294        let error = native_decode_error(NativeDecodeError::Decoding(
295            NativeDecodingError::UnsupportedFeature("test feature"),
296        ));
297
298        assert!(error.is_unsupported());
299        assert!(!error.is_truncated());
300        assert!(!error.is_not_implemented());
301    }
302
303    #[test]
304    fn native_decode_direct_plan_error_keeps_codec_classification() {
305        let error = native_decode_error(NativeDecodeError::Decoding(
306            NativeDecodingError::DirectPlanUnsupported(
307                NativeDirectPlanUnsupportedReason::ColorSingleTileCodestream,
308            ),
309        ));
310
311        assert!(error.is_unsupported());
312        assert!(!error.is_truncated());
313        assert!(!error.is_not_implemented());
314    }
315
316    #[test]
317    fn native_decode_unexpected_eof_keeps_codec_classification() {
318        let error = native_decode_error(NativeDecodeError::Decoding(
319            NativeDecodingError::UnexpectedEof,
320        ));
321
322        assert!(error.is_truncated());
323        assert!(!error.is_unsupported());
324    }
325
326    #[test]
327    fn native_decode_resource_errors_preserve_typed_sources() {
328        let sources = [
329            NativeDecodeError::AllocationTooLarge {
330                what: "CUDA decode fixture",
331                requested: 9,
332                cap: 8,
333            },
334            NativeDecodeError::HostAllocationFailed {
335                what: "CUDA decode fixture",
336                bytes: 7,
337            },
338        ];
339
340        for source in sources {
341            let error = native_decode_error(source);
342            assert!(matches!(
343                &error,
344                Error::NativeDecode {
345                    context: "native JPEG 2000 backend failed",
346                    source: stored,
347                } if stored == &NativeBackendError::decode(source)
348            ));
349            let opaque = core::error::Error::source(&error).expect("opaque adapter source");
350            assert!(opaque.downcast_ref::<NativeBackendError>().is_some());
351            let concrete = opaque.source().expect("concrete native decode source");
352            assert_eq!(concrete.downcast_ref::<NativeDecodeError>(), Some(&source));
353            assert!(!error.is_buffer_error());
354            assert!(!error.is_unsupported());
355        }
356    }
357
358    #[cfg(feature = "cuda-runtime")]
359    #[test]
360    fn cuda_cleanup_failure_preserves_both_runtime_diagnostics() {
361        let combined = combine_cuda_cleanup_errors(
362            runtime_error("primary launch failure"),
363            runtime_error("follow-up synchronization failure"),
364        );
365
366        assert!(matches!(&combined, Error::CudaCleanupFailed { .. }));
367        let rendered = combined.to_string();
368        assert!(rendered.contains("primary launch failure"));
369        assert!(rendered.contains("follow-up synchronization failure"));
370        assert!(!combined.is_unsupported());
371    }
372
373    #[cfg(feature = "cuda-runtime")]
374    #[test]
375    fn cuda_cleanup_failure_preserves_non_runtime_primary_and_blocks_fallback() {
376        let combined = combine_cuda_cleanup_errors(
377            Error::UnsupportedCudaRequest {
378                reason: "invalid prepared store",
379            },
380            runtime_error("synchronization failure"),
381        );
382
383        assert!(matches!(&combined, Error::CudaCleanupFailed { .. }));
384        let rendered = combined.to_string();
385        assert!(rendered.contains("invalid prepared store"));
386        assert!(rendered.contains("synchronization failure"));
387        assert!(!combined.is_unsupported());
388    }
389}