Skip to main content

j2k_transcode_metal/
accelerator.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use j2k_transcode::{
4    DctGridToReversibleDwt53Job, DctToWaveletStageCounterEvent as CounterEvent,
5    DctToWaveletStageCounters, Dwt97BatchStageTimings, ReversibleDwt53FirstLevel,
6    TranscodeStageDispatchMode, TranscodeStageError,
7};
8
9#[cfg(target_os = "macos")]
10use crate::metal;
11#[cfg(target_os = "macos")]
12use crate::MetalTranscodeError;
13#[cfg(target_os = "macos")]
14use crate::MetalTranscodeSession;
15
16mod dispatch;
17
18const DEFAULT_AUTO_MIN_SAMPLES: usize = 224 * 224;
19// Metal single-job Auto dispatch is disabled for the transcode paths whose
20// current evidence is batch-shaped. Callers can opt in per stage with the
21// public threshold setters when they have host-local evidence.
22const DEFAULT_AUTO_DWT97_MIN_SAMPLES: usize = usize::MAX;
23const DEFAULT_AUTO_REVERSIBLE_MIN_SAMPLES: usize = usize::MAX;
24const DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_JOBS: usize = 32;
25const DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_SAMPLES: usize = 224 * 224 * 32;
26const DEFAULT_AUTO_DWT97_BATCH_MIN_JOBS: usize = 32;
27const DEFAULT_AUTO_DWT97_BATCH_MIN_SAMPLES: usize = 224 * 224 * 32;
28// Auto avoids the staged 9/7 Metal path for very large tile axes by default;
29// strict Metal and caller-lowered thresholds remain explicit policy decisions.
30const MAX_AUTO_DWT97_STAGED_BATCH_AXIS: usize = 1024;
31
32/// Optional Metal accelerator for `j2k-transcode` transform stages.
33#[derive(Debug, Clone)]
34pub struct MetalDctToWaveletStageAccelerator {
35    mode: TranscodeStageDispatchMode,
36    min_auto_samples: usize,
37    min_auto_dwt97_samples: usize,
38    min_auto_reversible_samples: usize,
39    min_auto_reversible_batch_jobs: usize,
40    min_auto_reversible_batch_samples: usize,
41    counters: DctToWaveletStageCounters,
42    last_dwt97_batch_stage_timings: Option<Dwt97BatchStageTimings>,
43    min_auto_dwt97_batch_jobs: usize,
44    min_auto_dwt97_batch_samples: usize,
45    #[cfg(target_os = "macos")]
46    session: Option<MetalTranscodeSession>,
47}
48
49impl MetalDctToWaveletStageAccelerator {
50    /// Create an accelerator that treats unsupported Metal dispatch as an error.
51    #[must_use]
52    pub const fn new_explicit() -> Self {
53        Self {
54            mode: TranscodeStageDispatchMode::Explicit,
55            min_auto_samples: 0,
56            min_auto_dwt97_samples: 0,
57            min_auto_reversible_samples: 0,
58            min_auto_reversible_batch_jobs: 0,
59            min_auto_reversible_batch_samples: 0,
60            counters: DctToWaveletStageCounters::new(),
61            last_dwt97_batch_stage_timings: None,
62            min_auto_dwt97_batch_jobs: 0,
63            min_auto_dwt97_batch_samples: 0,
64            #[cfg(target_os = "macos")]
65            session: None,
66        }
67    }
68
69    /// Create an accelerator that falls back to scalar CPU for small or unsupported jobs.
70    #[must_use]
71    pub const fn for_auto() -> Self {
72        Self {
73            mode: TranscodeStageDispatchMode::Auto,
74            min_auto_samples: DEFAULT_AUTO_MIN_SAMPLES,
75            min_auto_dwt97_samples: DEFAULT_AUTO_DWT97_MIN_SAMPLES,
76            min_auto_reversible_samples: DEFAULT_AUTO_REVERSIBLE_MIN_SAMPLES,
77            min_auto_reversible_batch_jobs: DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_JOBS,
78            min_auto_reversible_batch_samples: DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_SAMPLES,
79            counters: DctToWaveletStageCounters::new(),
80            last_dwt97_batch_stage_timings: None,
81            min_auto_dwt97_batch_jobs: DEFAULT_AUTO_DWT97_BATCH_MIN_JOBS,
82            min_auto_dwt97_batch_samples: DEFAULT_AUTO_DWT97_BATCH_MIN_SAMPLES,
83            #[cfg(target_os = "macos")]
84            session: None,
85        }
86    }
87
88    /// Create an explicit-dispatch accelerator bound to a caller-owned Metal session.
89    #[cfg(target_os = "macos")]
90    #[must_use]
91    pub fn new_explicit_with_session(session: MetalTranscodeSession) -> Self {
92        Self::new_explicit().with_session(session)
93    }
94
95    /// Create an Auto-mode accelerator bound to a caller-owned Metal session.
96    #[cfg(target_os = "macos")]
97    #[must_use]
98    pub fn for_auto_with_session(session: MetalTranscodeSession) -> Self {
99        Self::for_auto().with_session(session)
100    }
101
102    /// Create an explicit-dispatch accelerator bound to an existing Metal device.
103    #[cfg(target_os = "macos")]
104    #[must_use]
105    pub fn new_explicit_with_device(
106        device: objc2::rc::Retained<objc2::runtime::ProtocolObject<dyn objc2_metal::MTLDevice>>,
107    ) -> Self {
108        Self::new_explicit_with_session(MetalTranscodeSession::new(device))
109    }
110
111    /// Create an Auto-mode accelerator bound to an existing Metal device.
112    #[cfg(target_os = "macos")]
113    #[must_use]
114    pub fn for_auto_with_device(
115        device: objc2::rc::Retained<objc2::runtime::ProtocolObject<dyn objc2_metal::MTLDevice>>,
116    ) -> Self {
117        Self::for_auto_with_session(MetalTranscodeSession::new(device))
118    }
119
120    /// Bind this accelerator to a caller-owned Metal session.
121    #[cfg(target_os = "macos")]
122    #[must_use]
123    pub fn with_session(mut self, session: MetalTranscodeSession) -> Self {
124        self.session = Some(session);
125        self
126    }
127
128    /// Bind this accelerator to an existing Metal device.
129    #[cfg(target_os = "macos")]
130    #[must_use]
131    pub fn with_device(
132        self,
133        device: objc2::rc::Retained<objc2::runtime::ProtocolObject<dyn objc2_metal::MTLDevice>>,
134    ) -> Self {
135        self.with_session(MetalTranscodeSession::new(device))
136    }
137
138    #[cfg(target_os = "macos")]
139    fn metal_session(&mut self) -> &mut MetalTranscodeSession {
140        self.session
141            .get_or_insert_with(MetalTranscodeSession::default)
142    }
143
144    /// Override the minimum component sample count used before Auto mode dispatches non-reversible projection jobs to Metal.
145    #[must_use]
146    pub const fn with_auto_min_samples(mut self, min_samples: usize) -> Self {
147        self.min_auto_samples = min_samples;
148        self.min_auto_dwt97_samples = min_samples;
149        self
150    }
151
152    /// Override the minimum component sample count used before Auto mode dispatches 9/7 transform jobs to Metal.
153    #[must_use]
154    pub const fn with_auto_dwt97_min_samples(mut self, min_samples: usize) -> Self {
155        self.min_auto_dwt97_samples = min_samples;
156        self
157    }
158
159    /// Override the 9/7 batch thresholds used before Auto mode dispatches a same-geometry batch to Metal.
160    #[must_use]
161    pub const fn with_auto_dwt97_batch_thresholds(
162        mut self,
163        min_jobs: usize,
164        min_samples: usize,
165    ) -> Self {
166        self.min_auto_dwt97_batch_jobs = min_jobs;
167        self.min_auto_dwt97_batch_samples = min_samples;
168        self
169    }
170
171    /// Override the minimum component sample count used before Auto mode dispatches single reversible 5/3 jobs to Metal.
172    #[must_use]
173    pub const fn with_auto_reversible_min_samples(mut self, min_samples: usize) -> Self {
174        self.min_auto_reversible_samples = min_samples;
175        self
176    }
177
178    /// Override the reversible 5/3 batch thresholds used before Auto mode dispatches a same-geometry batch to Metal.
179    #[must_use]
180    pub const fn with_auto_reversible_batch_thresholds(
181        mut self,
182        min_jobs: usize,
183        min_samples: usize,
184    ) -> Self {
185        self.min_auto_reversible_batch_jobs = min_jobs;
186        self.min_auto_reversible_batch_samples = min_samples;
187        self
188    }
189
190    /// Number of reversible integer 5/3 jobs offered to this accelerator.
191    #[must_use]
192    pub const fn reversible_dwt53_attempts(&self) -> usize {
193        self.counters.reversible_dwt53_attempts()
194    }
195
196    /// Number of reversible integer 5/3 jobs handled by Metal.
197    #[must_use]
198    pub const fn reversible_dwt53_dispatches(&self) -> usize {
199        self.counters.reversible_dwt53_dispatches()
200    }
201
202    /// Number of reversible integer 5/3 batches offered to this accelerator.
203    #[must_use]
204    pub const fn reversible_dwt53_batch_attempts(&self) -> usize {
205        self.counters.reversible_dwt53_batch_attempts()
206    }
207
208    /// Number of reversible integer 5/3 batches handled by Metal.
209    #[must_use]
210    pub const fn reversible_dwt53_batch_dispatches(&self) -> usize {
211        self.counters.reversible_dwt53_batch_dispatches()
212    }
213
214    /// Number of 5/3 projection jobs offered to this accelerator.
215    #[must_use]
216    pub const fn dwt53_attempts(&self) -> usize {
217        self.counters.dwt53_attempts()
218    }
219
220    /// Number of 5/3 projection jobs handled by Metal.
221    #[must_use]
222    pub const fn dwt53_dispatches(&self) -> usize {
223        self.counters.dwt53_dispatches()
224    }
225
226    /// Number of 9/7 transform jobs offered to this accelerator.
227    #[must_use]
228    pub const fn dwt97_attempts(&self) -> usize {
229        self.counters.dwt97_attempts()
230    }
231
232    /// Number of 9/7 transform jobs handled by Metal.
233    #[must_use]
234    pub const fn dwt97_dispatches(&self) -> usize {
235        self.counters.dwt97_dispatches()
236    }
237
238    /// Number of 9/7 transform batches offered to this accelerator.
239    #[must_use]
240    pub const fn dwt97_batch_attempts(&self) -> usize {
241        self.counters.dwt97_batch_attempts()
242    }
243
244    /// Number of 9/7 transform batches handled by Metal.
245    #[must_use]
246    pub const fn dwt97_batch_dispatches(&self) -> usize {
247        self.counters.dwt97_batch_dispatches()
248    }
249
250    /// Number of 9/7 code-block-ready batches offered to this accelerator.
251    #[must_use]
252    pub const fn htj2k97_codeblock_batch_attempts(&self) -> usize {
253        self.counters.htj2k97_codeblock_batch_attempts()
254    }
255
256    /// Number of 9/7 code-block-ready batches handled by Metal.
257    #[must_use]
258    pub const fn htj2k97_codeblock_batch_dispatches(&self) -> usize {
259        self.counters.htj2k97_codeblock_batch_dispatches()
260    }
261
262    /// Backend stage timings for the most recent 9/7 batch dispatch.
263    #[must_use]
264    pub const fn last_dwt97_batch_stage_timings(&self) -> Option<Dwt97BatchStageTimings> {
265        self.last_dwt97_batch_stage_timings
266    }
267
268    #[cfg(target_os = "macos")]
269    fn recover<T>(&self, error: MetalTranscodeError) -> Result<Option<T>, TranscodeStageError> {
270        self.mode
271            .recover(error, MetalTranscodeError::is_recoverable)
272    }
273
274    /// Dispatch a same-geometry batch of reversible integer 5/3 DCT-grid projection jobs.
275    pub fn dct_grid_to_reversible_dwt53_batch(
276        &mut self,
277        jobs: &[DctGridToReversibleDwt53Job<'_>],
278    ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
279        self.dispatch_reversible_dwt53_batch(jobs)
280    }
281
282    fn dispatch_reversible_dwt53_batch(
283        &mut self,
284        jobs: &[DctGridToReversibleDwt53Job<'_>],
285    ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
286        self.counters
287            .record(CounterEvent::ReversibleDwt53BatchAttempt, 1);
288        if jobs.is_empty() {
289            return Ok(Some(Vec::new()));
290        }
291        let total_samples = jobs.iter().fold(0usize, |total, job| {
292            total.saturating_add(job.width.saturating_mul(job.height))
293        });
294        if self.mode.is_auto()
295            && (jobs.len() < self.min_auto_reversible_batch_jobs
296                || total_samples < self.min_auto_reversible_batch_samples)
297        {
298            return Ok(None);
299        }
300
301        #[cfg(not(target_os = "macos"))]
302        {
303            self.mode.unavailable()
304        }
305        #[cfg(target_os = "macos")]
306        {
307            match metal::dispatch_dct_grid_to_reversible_dwt53_batch(self.metal_session(), jobs) {
308                Ok(output) => {
309                    self.counters
310                        .record(CounterEvent::ReversibleDwt53BatchDispatch, 1);
311                    Ok(Some(output))
312                }
313                Err(error) => self.recover(error),
314            }
315        }
316    }
317}
318
319impl Default for MetalDctToWaveletStageAccelerator {
320    fn default() -> Self {
321        Self::for_auto()
322    }
323}