Skip to main content

j2k_transcode_metal/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use core::fmt;
4use j2k_metal_support::MetalSupportError;
5use j2k_transcode::TranscodeStageError;
6
7use crate::{weights::SparseWeightRowsError, METAL_UNAVAILABLE};
8
9/// Error returned by the Metal transcode accelerator.
10#[derive(Debug)]
11pub enum MetalTranscodeError {
12    /// Metal is unavailable on this host or target.
13    MetalUnavailable,
14    /// The request is outside the current Metal implementation.
15    UnsupportedJob(&'static str),
16    /// Metal runtime creation or device execution failed with its diagnostic retained.
17    Runtime(MetalRuntimeFailure),
18    /// A shared Metal resource operation failed with its typed source retained.
19    MetalSupport {
20        /// Logical Metal operation that failed.
21        operation: &'static str,
22        /// Typed shared-support failure.
23        source: MetalSupportError,
24    },
25    /// A validated Metal result violated an internal kernel contract.
26    Kernel(&'static str),
27    /// A codec-owned host allocation exceeds the transcode safety limit.
28    HostAllocationTooLarge {
29        /// Requested byte count, saturated on overflow.
30        requested: usize,
31        /// Maximum permitted byte count.
32        cap: usize,
33        /// Logical allocation purpose.
34        what: &'static str,
35    },
36    /// A bounded codec-owned host allocation could not be reserved.
37    HostAllocationFailed {
38        /// Requested byte count.
39        requested: usize,
40        /// Logical allocation purpose.
41        what: &'static str,
42    },
43    /// A codec-owned Metal allocation exceeds the shared transcode safety limit.
44    DeviceAllocationTooLarge {
45        /// Requested byte count, saturated on overflow.
46        requested: usize,
47        /// Maximum permitted byte count.
48        cap: usize,
49        /// Logical allocation purpose.
50        what: &'static str,
51    },
52    /// Metal returned no buffer for a bounded device allocation.
53    DeviceAllocationFailed {
54        /// Requested byte count.
55        requested: usize,
56        /// Logical allocation purpose.
57        what: &'static str,
58    },
59}
60
61impl MetalTranscodeError {
62    /// Whether Auto mode may recover from this error by using scalar fallback.
63    #[cfg(any(test, target_os = "macos"))]
64    pub(crate) const fn is_recoverable(&self) -> bool {
65        matches!(self, Self::MetalUnavailable | Self::UnsupportedJob(_))
66    }
67
68    #[cfg(target_os = "macos")]
69    pub(crate) fn runtime(
70        operation: &'static str,
71        source: impl std::error::Error + Send + Sync + 'static,
72    ) -> Self {
73        Self::Runtime(MetalRuntimeFailure {
74            operation,
75            source: Box::new(source),
76        })
77    }
78
79    #[cfg(any(test, target_os = "macos"))]
80    pub(crate) const fn support(operation: &'static str, source: MetalSupportError) -> Self {
81        Self::MetalSupport { operation, source }
82    }
83}
84
85/// Diagnostic retained when the Metal runtime rejects a transcode operation.
86#[derive(Debug)]
87pub struct MetalRuntimeFailure {
88    operation: &'static str,
89    source: Box<dyn std::error::Error + Send + Sync + 'static>,
90}
91
92impl MetalRuntimeFailure {
93    /// Logical Metal operation that failed.
94    #[must_use]
95    pub const fn operation(&self) -> &'static str {
96        self.operation
97    }
98
99    /// Concrete runtime source retained by this adapter boundary.
100    #[must_use]
101    pub fn source_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
102        self.source.as_ref()
103    }
104}
105
106impl fmt::Display for MetalRuntimeFailure {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "{}: {}", self.operation, self.source)
109    }
110}
111
112impl fmt::Display for MetalTranscodeError {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            Self::MetalUnavailable => f.write_str(METAL_UNAVAILABLE),
116            Self::UnsupportedJob(reason) | Self::Kernel(reason) => f.write_str(reason),
117            Self::Runtime(failure) => failure.fmt(f),
118            Self::MetalSupport { operation, source } => {
119                write!(f, "{operation}: {source}")
120            }
121            Self::HostAllocationTooLarge {
122                requested,
123                cap,
124                what,
125            } => write!(
126                f,
127                "Metal transcode host allocation for {what} is too large: requested {requested} bytes, cap {cap} bytes"
128            ),
129            Self::HostAllocationFailed { requested, what } => write!(
130                f,
131                "Metal transcode host allocation failed for {what}: {requested} bytes"
132            ),
133            Self::DeviceAllocationTooLarge {
134                requested,
135                cap,
136                what,
137            } => write!(
138                f,
139                "Metal transcode device allocation for {what} is too large: requested {requested} bytes, cap {cap} bytes"
140            ),
141            Self::DeviceAllocationFailed { requested, what } => write!(
142                f,
143                "Metal transcode device allocation failed for {what}: {requested} bytes"
144            ),
145        }
146    }
147}
148
149impl From<MetalTranscodeError> for TranscodeStageError {
150    fn from(error: MetalTranscodeError) -> Self {
151        match error {
152            MetalTranscodeError::MetalUnavailable => Self::DeviceUnavailable,
153            MetalTranscodeError::UnsupportedJob(reason) => Self::Unsupported(reason),
154            MetalTranscodeError::Runtime(failure) => {
155                let operation = failure.operation();
156                Self::backend("metal", operation, failure)
157            }
158            MetalTranscodeError::MetalSupport { operation, source } => {
159                Self::backend("metal", operation, source)
160            }
161            MetalTranscodeError::Kernel(reason) => Self::backend(
162                "metal",
163                "kernel execution",
164                MetalTranscodeError::Kernel(reason),
165            ),
166            MetalTranscodeError::HostAllocationTooLarge { requested, cap, .. } => {
167                Self::MemoryCapExceeded { requested, cap }
168            }
169            MetalTranscodeError::HostAllocationFailed { requested, .. } => {
170                Self::HostAllocationFailed { bytes: requested }
171            }
172            MetalTranscodeError::DeviceAllocationTooLarge {
173                requested,
174                cap,
175                what,
176            } => Self::DeviceMemoryCapExceeded {
177                backend: "metal",
178                what,
179                requested,
180                cap,
181            },
182            MetalTranscodeError::DeviceAllocationFailed { requested, what } => {
183                Self::DeviceAllocationFailed {
184                    backend: "metal",
185                    what,
186                    requested,
187                }
188            }
189        }
190    }
191}
192
193impl From<SparseWeightRowsError> for MetalTranscodeError {
194    fn from(error: SparseWeightRowsError) -> Self {
195        match error {
196            SparseWeightRowsError::SizeOverflow => Self::HostAllocationTooLarge {
197                requested: usize::MAX,
198                cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
199                what: "projection weights",
200            },
201            SparseWeightRowsError::AllocationTooLarge { requested, cap } => {
202                Self::HostAllocationTooLarge {
203                    requested,
204                    cap,
205                    what: "projection weights",
206                }
207            }
208            SparseWeightRowsError::HostAllocationFailed { requested } => {
209                Self::HostAllocationFailed {
210                    requested,
211                    what: "projection weights",
212                }
213            }
214        }
215    }
216}
217
218impl std::error::Error for MetalRuntimeFailure {
219    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
220        Some(self.source.as_ref())
221    }
222}
223
224impl std::error::Error for MetalTranscodeError {
225    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
226        match self {
227            Self::Runtime(failure) => Some(failure),
228            Self::MetalSupport { source, .. } => Some(source),
229            Self::MetalUnavailable
230            | Self::UnsupportedJob(_)
231            | Self::Kernel(_)
232            | Self::HostAllocationTooLarge { .. }
233            | Self::HostAllocationFailed { .. }
234            | Self::DeviceAllocationTooLarge { .. }
235            | Self::DeviceAllocationFailed { .. } => None,
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::{MetalTranscodeError, TranscodeStageError};
243    use j2k_metal_support::MetalSupportError;
244
245    #[test]
246    fn host_cap_failure_preserves_typed_stage_error() {
247        let stage = TranscodeStageError::from(MetalTranscodeError::HostAllocationTooLarge {
248            requested: 65,
249            cap: 64,
250            what: "test host output",
251        });
252        assert!(matches!(
253            stage,
254            TranscodeStageError::MemoryCapExceeded {
255                requested: 65,
256                cap: 64,
257            }
258        ));
259    }
260
261    #[test]
262    fn host_allocator_failure_preserves_typed_stage_error() {
263        let stage = TranscodeStageError::from(MetalTranscodeError::HostAllocationFailed {
264            requested: 4096,
265            what: "test host output",
266        });
267        assert!(matches!(
268            stage,
269            TranscodeStageError::HostAllocationFailed { bytes: 4096 }
270        ));
271    }
272
273    #[test]
274    fn device_allocation_failures_preserve_stage_resource_categories() {
275        let cap = TranscodeStageError::from(MetalTranscodeError::DeviceAllocationTooLarge {
276            requested: 65,
277            cap: 64,
278            what: "test device output",
279        });
280        assert!(matches!(
281            cap,
282            TranscodeStageError::DeviceMemoryCapExceeded {
283                backend: "metal",
284                what: "test device output",
285                requested: 65,
286                cap: 64,
287            }
288        ));
289
290        let allocation = TranscodeStageError::from(MetalTranscodeError::DeviceAllocationFailed {
291            requested: 4096,
292            what: "test device output",
293        });
294        assert!(matches!(
295            allocation,
296            TranscodeStageError::DeviceAllocationFailed {
297                backend: "metal",
298                what: "test device output",
299                requested: 4096,
300            }
301        ));
302    }
303
304    #[test]
305    fn metal_support_failure_keeps_typed_source_and_is_not_recoverable() {
306        let source = MetalSupportError::CommandBufferUnavailable;
307        let error = MetalTranscodeError::support("test command buffer", source.clone());
308
309        assert!(!error.is_recoverable());
310        assert!(matches!(
311            &error,
312            MetalTranscodeError::MetalSupport {
313                operation: "test command buffer",
314                source: stored,
315            } if stored == &source
316        ));
317        let chained = std::error::Error::source(&error).expect("typed Metal support source");
318        assert!(chained.downcast_ref::<MetalSupportError>().is_some());
319
320        let stage = TranscodeStageError::from(error);
321        assert!(matches!(
322            &stage,
323            TranscodeStageError::Backend {
324                backend: "metal",
325                operation: "test command buffer",
326                ..
327            }
328        ));
329        let chained = std::error::Error::source(&stage).expect("typed stage backend source");
330        assert_eq!(chained.downcast_ref::<MetalSupportError>(), Some(&source));
331    }
332}