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//! Device-accepting expert constructors use retained `objc2-metal`
15//! `ProtocolObject<dyn MTLDevice>` owners as of 0.9. There is no `metal-rs`
16//! constructor compatibility layer.
17
18#[cfg(target_os = "macos")]
19mod metal;
20
21#[doc(hidden)]
22pub mod weights;
23
24mod accelerator;
25mod error;
26mod route;
27
28pub use accelerator::MetalDctToWaveletStageAccelerator;
29pub use error::{MetalRuntimeFailure, MetalTranscodeError};
30#[cfg(target_os = "macos")]
31pub use route::resident_codestream_buffer_from_metal_encoded_j2k;
32pub use route::{
33    jpeg_to_htj2k_batch_with_metal_route, jpeg_to_htj2k_with_metal_route, MetalEncodedTranscode,
34    MetalEncodedTranscodeBatch, MetalTranscodeFallbackReason, MetalTranscodeRouteReport,
35};
36
37#[cfg(target_os = "macos")]
38pub use metal::MetalTranscodeSession;
39
40/// Stable message returned when Metal is unavailable.
41pub const METAL_UNAVAILABLE: &str = "Metal is unavailable on this host";
42
43#[cfg(not(target_os = "macos"))]
44#[derive(Clone, Copy, Debug, Default)]
45/// Placeholder Metal transcode session for hosts without Metal support.
46pub struct MetalTranscodeSession {
47    _private: (),
48}
49
50#[cfg(not(target_os = "macos"))]
51impl MetalTranscodeSession {
52    /// Return `MetalUnavailable` on hosts without Metal support.
53    pub const fn system_default() -> Result<Self, MetalTranscodeError> {
54        Err(MetalTranscodeError::MetalUnavailable)
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::route::{
62        ensure_strict_metal_batch_dispatched, ensure_strict_metal_dispatched, route_report,
63    };
64    use j2k_core::{BackendKind, BackendRequest};
65    use j2k_transcode::{BatchTranscodeReport, JpegToHtj2kCoefficientPath, TranscodeTimingReport};
66
67    #[cfg(target_os = "macos")]
68    #[derive(Debug)]
69    struct TestRuntimeError;
70
71    #[cfg(target_os = "macos")]
72    impl core::fmt::Display for TestRuntimeError {
73        fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
74            formatter.write_str("driver rejected execution")
75        }
76    }
77
78    #[cfg(target_os = "macos")]
79    impl std::error::Error for TestRuntimeError {}
80
81    #[cfg(target_os = "macos")]
82    #[test]
83    fn runtime_failure_retains_operation_detail_and_source() {
84        let error = MetalTranscodeError::runtime("Metal test command buffer", TestRuntimeError);
85
86        assert_eq!(
87            error.to_string(),
88            "Metal test command buffer: driver rejected execution"
89        );
90        let runtime = std::error::Error::source(&error)
91            .and_then(|source| source.downcast_ref::<MetalRuntimeFailure>())
92            .expect("runtime failure wrapper");
93        let source = std::error::Error::source(runtime).expect("concrete runtime source");
94        assert!(source.downcast_ref::<TestRuntimeError>().is_some());
95        assert!(!error.is_recoverable());
96    }
97
98    #[test]
99    fn allocation_failures_are_hard_in_auto_mode() {
100        let error = MetalTranscodeError::HostAllocationTooLarge {
101            requested: usize::MAX,
102            cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
103            what: "test output",
104        };
105        assert!(!error.is_recoverable());
106        assert!(MetalTranscodeError::UnsupportedJob("test decline").is_recoverable());
107    }
108
109    #[test]
110    fn route_report_uses_shared_accelerator_work_classifier() {
111        let timings = TranscodeTimingReport {
112            dwt97_batch_readback_bytes: 128,
113            ..TranscodeTimingReport::default()
114        };
115        let route = route_report(BackendRequest::Auto, &timings);
116        assert_eq!(route.selected_transform_backend, BackendKind::Metal);
117        assert_eq!(route.fallback_reason, None);
118    }
119
120    #[test]
121    fn strict_metal_accepts_shared_accelerator_work_evidence() {
122        let timings = TranscodeTimingReport {
123            dwt97_batch_pack_upload_transfers: 1,
124            ..TranscodeTimingReport::default()
125        };
126        let batch_report = BatchTranscodeReport {
127            tile_count: 1,
128            successful_tiles: 1,
129            failed_tiles: 0,
130            transformed_components: 1,
131            reversible_dwt53_batches: 0,
132            reversible_dwt53_batch_jobs: 0,
133            extract_us: 0,
134            transform_us: 0,
135            encode_us: 0,
136            timings,
137            coefficient_path: JpegToHtj2kCoefficientPath::FloatDirectLinear53,
138        };
139
140        ensure_strict_metal_dispatched(&timings).expect("shared classifier marks Metal work");
141        ensure_strict_metal_batch_dispatched(&batch_report)
142            .expect("batch strict route uses shared classifier");
143    }
144}