Skip to main content

j2k_transcode_metal/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Metal acceleration for coefficient-domain JPEG to HTJ2K transcode stages.
4//!
5//! The supported targets are direct DCT-grid to one-level 5/3 and 9/7 wavelet
6//! projections used by `j2k-transcode`'s HTJ2K paths. CPU scalar code
7//! remains the oracle and fallback.
8//!
9//! Auto routing is intentionally batch-first for the expensive Metal transcode
10//! paths: the default single-job reversible 5/3 and 9/7 thresholds are
11//! `usize::MAX`, so single-tile requests stay on the CPU unless callers opt in
12//! with `with_auto_reversible_min_samples` or `with_auto_dwt97_min_samples`.
13
14#[cfg(target_os = "macos")]
15mod metal;
16
17#[doc(hidden)]
18pub mod weights;
19
20mod accelerator;
21mod error;
22mod route;
23
24pub use accelerator::MetalDctToWaveletStageAccelerator;
25pub use error::{MetalRuntimeFailure, MetalTranscodeError};
26#[cfg(target_os = "macos")]
27pub use route::resident_codestream_buffer_from_metal_encoded_j2k;
28pub use route::{
29    jpeg_to_htj2k_batch_with_metal_route, jpeg_to_htj2k_with_metal_route, MetalEncodedTranscode,
30    MetalEncodedTranscodeBatch, MetalTranscodeFallbackReason, MetalTranscodeRouteReport,
31};
32
33#[cfg(target_os = "macos")]
34pub use metal::MetalTranscodeSession;
35
36/// Stable message returned when Metal is unavailable.
37pub const METAL_UNAVAILABLE: &str = "Metal is unavailable on this host";
38
39#[cfg(not(target_os = "macos"))]
40#[derive(Clone, Copy, Debug, Default)]
41/// Placeholder Metal transcode session for hosts without Metal support.
42pub struct MetalTranscodeSession {
43    _private: (),
44}
45
46#[cfg(not(target_os = "macos"))]
47impl MetalTranscodeSession {
48    /// Return `MetalUnavailable` on hosts without Metal support.
49    pub const fn system_default() -> Result<Self, MetalTranscodeError> {
50        Err(MetalTranscodeError::MetalUnavailable)
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use crate::route::{
58        ensure_strict_metal_batch_dispatched, ensure_strict_metal_dispatched, route_report,
59    };
60    use j2k_core::{BackendKind, BackendRequest};
61    use j2k_transcode::{BatchTranscodeReport, JpegToHtj2kCoefficientPath, TranscodeTimingReport};
62
63    #[cfg(target_os = "macos")]
64    #[derive(Debug)]
65    struct TestRuntimeError;
66
67    #[cfg(target_os = "macos")]
68    impl core::fmt::Display for TestRuntimeError {
69        fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
70            formatter.write_str("driver rejected execution")
71        }
72    }
73
74    #[cfg(target_os = "macos")]
75    impl std::error::Error for TestRuntimeError {}
76
77    #[cfg(target_os = "macos")]
78    #[test]
79    fn runtime_failure_retains_operation_detail_and_source() {
80        let error = MetalTranscodeError::runtime("Metal test command buffer", TestRuntimeError);
81
82        assert_eq!(
83            error.to_string(),
84            "Metal test command buffer: driver rejected execution"
85        );
86        let runtime = std::error::Error::source(&error)
87            .and_then(|source| source.downcast_ref::<MetalRuntimeFailure>())
88            .expect("runtime failure wrapper");
89        let source = std::error::Error::source(runtime).expect("concrete runtime source");
90        assert!(source.downcast_ref::<TestRuntimeError>().is_some());
91        assert!(!error.is_recoverable());
92    }
93
94    #[test]
95    fn allocation_failures_are_hard_in_auto_mode() {
96        let error = MetalTranscodeError::HostAllocationTooLarge {
97            requested: usize::MAX,
98            cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
99            what: "test output",
100        };
101        assert!(!error.is_recoverable());
102        assert!(MetalTranscodeError::UnsupportedJob("test decline").is_recoverable());
103    }
104
105    #[test]
106    fn route_report_uses_shared_accelerator_work_classifier() {
107        let timings = TranscodeTimingReport {
108            dwt97_batch_readback_bytes: 128,
109            ..TranscodeTimingReport::default()
110        };
111        let route = route_report(BackendRequest::Auto, &timings);
112        assert_eq!(route.selected_transform_backend, BackendKind::Metal);
113        assert_eq!(route.fallback_reason, None);
114    }
115
116    #[test]
117    fn strict_metal_accepts_shared_accelerator_work_evidence() {
118        let timings = TranscodeTimingReport {
119            dwt97_batch_pack_upload_transfers: 1,
120            ..TranscodeTimingReport::default()
121        };
122        let batch_report = BatchTranscodeReport {
123            tile_count: 1,
124            successful_tiles: 1,
125            failed_tiles: 0,
126            transformed_components: 1,
127            reversible_dwt53_batches: 0,
128            reversible_dwt53_batch_jobs: 0,
129            extract_us: 0,
130            transform_us: 0,
131            encode_us: 0,
132            timings,
133            coefficient_path: JpegToHtj2kCoefficientPath::FloatDirectLinear53,
134        };
135
136        ensure_strict_metal_dispatched(&timings).expect("shared classifier marks Metal work");
137        ensure_strict_metal_batch_dispatched(&batch_report)
138            .expect("batch strict route uses shared classifier");
139    }
140}