1use core::fmt;
4use j2k_metal_support::MetalSupportError;
5use j2k_transcode::TranscodeStageError;
6
7use crate::{weights::SparseWeightRowsError, METAL_UNAVAILABLE};
8
9#[derive(Debug)]
11pub enum MetalTranscodeError {
12 MetalUnavailable,
14 UnsupportedJob(&'static str),
16 Runtime(MetalRuntimeFailure),
18 MetalSupport {
20 operation: &'static str,
22 source: MetalSupportError,
24 },
25 Kernel(&'static str),
27 HostAllocationTooLarge {
29 requested: usize,
31 cap: usize,
33 what: &'static str,
35 },
36 HostAllocationFailed {
38 requested: usize,
40 what: &'static str,
42 },
43 DeviceAllocationTooLarge {
45 requested: usize,
47 cap: usize,
49 what: &'static str,
51 },
52 DeviceAllocationFailed {
54 requested: usize,
56 what: &'static str,
58 },
59}
60
61impl MetalTranscodeError {
62 #[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#[derive(Debug)]
87pub struct MetalRuntimeFailure {
88 operation: &'static str,
89 source: Box<dyn std::error::Error + Send + Sync + 'static>,
90}
91
92impl MetalRuntimeFailure {
93 #[must_use]
95 pub const fn operation(&self) -> &'static str {
96 self.operation
97 }
98
99 #[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}