Skip to main content

j2k_transcode_metal/
route.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use core::fmt;
4
5use j2k_core::{BackendKind, BackendRequest};
6use j2k_transcode::{
7    BatchTranscodeReport, EncodedTranscode, EncodedTranscodeBatch, JpegTileBatchInput,
8    JpegToHtj2kError, JpegToHtj2kOptions, JpegToHtj2kTranscoder, TranscodePipelineMap,
9    TranscodeStageError, TranscodeTimingReport,
10};
11#[cfg(target_os = "macos")]
12use j2k_transcode::{ResidentBufferRef, ResidentCodestreamBuffer, ResidentHandoffError};
13
14use crate::MetalDctToWaveletStageAccelerator;
15
16const CUDA_REQUESTED_THROUGH_METAL_ADAPTER: &str = "CUDA transcode requested through Metal adapter";
17const STRICT_METAL_TRANSCODE_NO_DISPATCH: &str =
18    "strict Metal transcode produced no Metal dispatch";
19
20/// Structured CPU fallback reason for the Metal JPEG-to-HTJ2K route facade.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum MetalTranscodeFallbackReason {
23    /// The caller requested CPU explicitly.
24    CpuRequested,
25    /// Auto found no transform stage eligible for Metal.
26    AutoNoEligibleTransformStage,
27    /// Auto offered transform jobs to Metal, but all jobs used CPU fallback.
28    AutoAllTransformJobsFellBackToCpu,
29    /// Auto used Metal for some transform jobs and CPU fallback for others.
30    AutoPartialTransformFallback,
31}
32
33impl MetalTranscodeFallbackReason {
34    /// Stable reason label for logs, examples, and benchmark output.
35    #[must_use]
36    pub const fn as_str(self) -> &'static str {
37        match self {
38            Self::CpuRequested => "cpu_requested",
39            Self::AutoNoEligibleTransformStage => "auto_no_eligible_transform_stage",
40            Self::AutoAllTransformJobsFellBackToCpu => "auto_all_transform_jobs_fell_back_to_cpu",
41            Self::AutoPartialTransformFallback => "auto_partial_transform_fallback",
42        }
43    }
44}
45
46impl fmt::Display for MetalTranscodeFallbackReason {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(self.as_str())
49    }
50}
51
52/// Public route report for Metal-adapted JPEG-to-HTJ2K transcode calls.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct MetalTranscodeRouteReport {
55    /// Caller backend request.
56    pub request: BackendRequest,
57    /// Backend that handled transform stages.
58    pub selected_transform_backend: BackendKind,
59    /// Backend that produced the public codestream byte vector.
60    pub output_backend: BackendKind,
61    /// Structured CPU fallback reason when the route did not fully use Metal.
62    pub fallback_reason: Option<MetalTranscodeFallbackReason>,
63    /// Stage residency map derived from the existing transcode timing counters.
64    pub pipeline_map: TranscodePipelineMap,
65}
66
67/// JPEG-to-HTJ2K transcode output plus Metal route report.
68pub struct MetalEncodedTranscode {
69    /// Encoded HTJ2K codestream and native transcode report.
70    pub encoded: EncodedTranscode,
71    /// Route and residency report for this call.
72    pub route: MetalTranscodeRouteReport,
73}
74
75/// Batch JPEG-to-HTJ2K transcode output plus Metal route report.
76pub struct MetalEncodedTranscodeBatch {
77    /// Per-tile outputs and aggregate native transcode report.
78    pub batch: EncodedTranscodeBatch,
79    /// Route and residency report for this batch call.
80    pub route: MetalTranscodeRouteReport,
81}
82
83/// Build a backend-neutral resident codestream descriptor from a Metal encode output.
84#[cfg(target_os = "macos")]
85pub fn resident_codestream_buffer_from_metal_encoded_j2k(
86    encoded: &j2k_metal::MetalEncodedJ2k,
87) -> Result<ResidentCodestreamBuffer<'_>, ResidentHandoffError> {
88    let memory = encoded
89        .codestream_memory_range()
90        .ok_or(ResidentHandoffError::OffsetOverflow)?;
91    let allocation_len = encoded
92        .codestream_allocation_len()
93        .ok_or(ResidentHandoffError::RangeExceedsAllocation)?;
94    let buffer = ResidentBufferRef::with_allocation_len(memory, allocation_len)?;
95    ResidentCodestreamBuffer::new(buffer, encoded.byte_len(), encoded.capacity())?
96        .require_backend(BackendKind::Metal)
97}
98
99/// Transcode JPEG to HTJ2K using CPU, Auto Metal, or strict Metal routing.
100///
101/// `BackendRequest::Metal` uses the explicit Metal accelerator and returns an
102/// error if Metal is unavailable or the required transform stage is unsupported.
103/// `BackendRequest::Auto` may return CPU output with a structured fallback reason.
104pub fn jpeg_to_htj2k_with_metal_route(
105    bytes: &[u8],
106    options: &JpegToHtj2kOptions,
107    request: BackendRequest,
108) -> Result<MetalEncodedTranscode, JpegToHtj2kError> {
109    let mut transcoder = JpegToHtj2kTranscoder::default();
110    let encoded = match request {
111        BackendRequest::Cpu => transcoder.transcode(bytes, options)?,
112        BackendRequest::Auto => {
113            let mut accelerator = MetalDctToWaveletStageAccelerator::for_auto();
114            transcoder.transcode_with_accelerator(bytes, options, &mut accelerator)?
115        }
116        BackendRequest::Metal => {
117            let mut accelerator = MetalDctToWaveletStageAccelerator::new_explicit();
118            let encoded =
119                transcoder.transcode_with_accelerator(bytes, options, &mut accelerator)?;
120            ensure_strict_metal_dispatched(&encoded.report.timings)?;
121            encoded
122        }
123        BackendRequest::Cuda => {
124            return Err(JpegToHtj2kError::Unsupported(
125                CUDA_REQUESTED_THROUGH_METAL_ADAPTER,
126            ));
127        }
128    };
129    let route = route_report(request, &encoded.report.timings);
130    Ok(MetalEncodedTranscode { encoded, route })
131}
132
133/// Batch transcode JPEG tiles to HTJ2K using CPU, Auto Metal, or strict Metal routing.
134pub fn jpeg_to_htj2k_batch_with_metal_route(
135    tiles: &[JpegTileBatchInput<'_>],
136    options: &JpegToHtj2kOptions,
137    request: BackendRequest,
138) -> Result<MetalEncodedTranscodeBatch, JpegToHtj2kError> {
139    let mut transcoder = JpegToHtj2kTranscoder::default();
140    let batch = match request {
141        BackendRequest::Cpu => transcoder.transcode_batch(tiles, options)?,
142        BackendRequest::Auto => {
143            let mut accelerator = MetalDctToWaveletStageAccelerator::for_auto();
144            transcoder.transcode_batch_with_accelerator(tiles, options, &mut accelerator)?
145        }
146        BackendRequest::Metal => {
147            let mut accelerator = MetalDctToWaveletStageAccelerator::new_explicit();
148            let batch =
149                transcoder.transcode_batch_with_accelerator(tiles, options, &mut accelerator)?;
150            ensure_strict_metal_batch_dispatched(&batch.report)?;
151            batch
152        }
153        BackendRequest::Cuda => {
154            return Err(JpegToHtj2kError::Unsupported(
155                CUDA_REQUESTED_THROUGH_METAL_ADAPTER,
156            ));
157        }
158    };
159    let route = route_report(request, &batch.report.timings);
160    Ok(MetalEncodedTranscodeBatch { batch, route })
161}
162
163pub(crate) fn route_report(
164    request: BackendRequest,
165    timings: &TranscodeTimingReport,
166) -> MetalTranscodeRouteReport {
167    let selected_transform_backend = selected_transform_backend(timings);
168    MetalTranscodeRouteReport {
169        request,
170        selected_transform_backend,
171        output_backend: BackendKind::Cpu,
172        fallback_reason: fallback_reason(request, selected_transform_backend, timings),
173        pipeline_map: timings.pipeline_map(),
174    }
175}
176
177fn selected_transform_backend(timings: &TranscodeTimingReport) -> BackendKind {
178    if timings.accelerator_work_observed() {
179        BackendKind::Metal
180    } else {
181        BackendKind::Cpu
182    }
183}
184
185fn fallback_reason(
186    request: BackendRequest,
187    selected_transform_backend: BackendKind,
188    timings: &TranscodeTimingReport,
189) -> Option<MetalTranscodeFallbackReason> {
190    match request {
191        BackendRequest::Cpu => Some(MetalTranscodeFallbackReason::CpuRequested),
192        BackendRequest::Auto
193            if selected_transform_backend == BackendKind::Cpu
194                && timings.accelerator_attempts == 0 =>
195        {
196            Some(MetalTranscodeFallbackReason::AutoNoEligibleTransformStage)
197        }
198        BackendRequest::Auto if selected_transform_backend == BackendKind::Cpu => {
199            Some(MetalTranscodeFallbackReason::AutoAllTransformJobsFellBackToCpu)
200        }
201        BackendRequest::Auto if timings.cpu_fallback_jobs > 0 => {
202            Some(MetalTranscodeFallbackReason::AutoPartialTransformFallback)
203        }
204        BackendRequest::Metal | BackendRequest::Cuda | BackendRequest::Auto => None,
205    }
206}
207
208pub(crate) fn ensure_strict_metal_dispatched(
209    timings: &TranscodeTimingReport,
210) -> Result<(), JpegToHtj2kError> {
211    if timings.accelerator_work_observed() {
212        Ok(())
213    } else {
214        Err(JpegToHtj2kError::Accelerator(
215            TranscodeStageError::Unsupported(STRICT_METAL_TRANSCODE_NO_DISPATCH),
216        ))
217    }
218}
219
220pub(crate) fn ensure_strict_metal_batch_dispatched(
221    report: &BatchTranscodeReport,
222) -> Result<(), JpegToHtj2kError> {
223    if report.successful_tiles == 0 || report.timings.accelerator_work_observed() {
224        Ok(())
225    } else {
226        Err(JpegToHtj2kError::Accelerator(
227            TranscodeStageError::Unsupported(STRICT_METAL_TRANSCODE_NO_DISPATCH),
228        ))
229    }
230}