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(device: ::metal::Device) -> Self {
106        Self::new_explicit_with_session(MetalTranscodeSession::new(device))
107    }
108
109    /// Create an Auto-mode accelerator bound to an existing Metal device.
110    #[cfg(target_os = "macos")]
111    #[must_use]
112    pub fn for_auto_with_device(device: ::metal::Device) -> Self {
113        Self::for_auto_with_session(MetalTranscodeSession::new(device))
114    }
115
116    /// Bind this accelerator to a caller-owned Metal session.
117    #[cfg(target_os = "macos")]
118    #[must_use]
119    pub fn with_session(mut self, session: MetalTranscodeSession) -> Self {
120        self.session = Some(session);
121        self
122    }
123
124    /// Bind this accelerator to an existing Metal device.
125    #[cfg(target_os = "macos")]
126    #[must_use]
127    pub fn with_device(self, device: ::metal::Device) -> Self {
128        self.with_session(MetalTranscodeSession::new(device))
129    }
130
131    #[cfg(target_os = "macos")]
132    fn metal_session(&mut self) -> &mut MetalTranscodeSession {
133        self.session
134            .get_or_insert_with(MetalTranscodeSession::default)
135    }
136
137    /// Override the minimum component sample count used before Auto mode dispatches non-reversible projection jobs to Metal.
138    #[must_use]
139    pub const fn with_auto_min_samples(mut self, min_samples: usize) -> Self {
140        self.min_auto_samples = min_samples;
141        self.min_auto_dwt97_samples = min_samples;
142        self
143    }
144
145    /// Override the minimum component sample count used before Auto mode dispatches 9/7 transform jobs to Metal.
146    #[must_use]
147    pub const fn with_auto_dwt97_min_samples(mut self, min_samples: usize) -> Self {
148        self.min_auto_dwt97_samples = min_samples;
149        self
150    }
151
152    /// Override the 9/7 batch thresholds used before Auto mode dispatches a same-geometry batch to Metal.
153    #[must_use]
154    pub const fn with_auto_dwt97_batch_thresholds(
155        mut self,
156        min_jobs: usize,
157        min_samples: usize,
158    ) -> Self {
159        self.min_auto_dwt97_batch_jobs = min_jobs;
160        self.min_auto_dwt97_batch_samples = min_samples;
161        self
162    }
163
164    /// Override the minimum component sample count used before Auto mode dispatches single reversible 5/3 jobs to Metal.
165    #[must_use]
166    pub const fn with_auto_reversible_min_samples(mut self, min_samples: usize) -> Self {
167        self.min_auto_reversible_samples = min_samples;
168        self
169    }
170
171    /// Override the reversible 5/3 batch thresholds used before Auto mode dispatches a same-geometry batch to Metal.
172    #[must_use]
173    pub const fn with_auto_reversible_batch_thresholds(
174        mut self,
175        min_jobs: usize,
176        min_samples: usize,
177    ) -> Self {
178        self.min_auto_reversible_batch_jobs = min_jobs;
179        self.min_auto_reversible_batch_samples = min_samples;
180        self
181    }
182
183    /// Number of reversible integer 5/3 jobs offered to this accelerator.
184    #[must_use]
185    pub const fn reversible_dwt53_attempts(&self) -> usize {
186        self.counters.reversible_dwt53_attempts()
187    }
188
189    /// Number of reversible integer 5/3 jobs handled by Metal.
190    #[must_use]
191    pub const fn reversible_dwt53_dispatches(&self) -> usize {
192        self.counters.reversible_dwt53_dispatches()
193    }
194
195    /// Number of reversible integer 5/3 batches offered to this accelerator.
196    #[must_use]
197    pub const fn reversible_dwt53_batch_attempts(&self) -> usize {
198        self.counters.reversible_dwt53_batch_attempts()
199    }
200
201    /// Number of reversible integer 5/3 batches handled by Metal.
202    #[must_use]
203    pub const fn reversible_dwt53_batch_dispatches(&self) -> usize {
204        self.counters.reversible_dwt53_batch_dispatches()
205    }
206
207    /// Number of 5/3 projection jobs offered to this accelerator.
208    #[must_use]
209    pub const fn dwt53_attempts(&self) -> usize {
210        self.counters.dwt53_attempts()
211    }
212
213    /// Number of 5/3 projection jobs handled by Metal.
214    #[must_use]
215    pub const fn dwt53_dispatches(&self) -> usize {
216        self.counters.dwt53_dispatches()
217    }
218
219    /// Number of 9/7 transform jobs offered to this accelerator.
220    #[must_use]
221    pub const fn dwt97_attempts(&self) -> usize {
222        self.counters.dwt97_attempts()
223    }
224
225    /// Number of 9/7 transform jobs handled by Metal.
226    #[must_use]
227    pub const fn dwt97_dispatches(&self) -> usize {
228        self.counters.dwt97_dispatches()
229    }
230
231    /// Number of 9/7 transform batches offered to this accelerator.
232    #[must_use]
233    pub const fn dwt97_batch_attempts(&self) -> usize {
234        self.counters.dwt97_batch_attempts()
235    }
236
237    /// Number of 9/7 transform batches handled by Metal.
238    #[must_use]
239    pub const fn dwt97_batch_dispatches(&self) -> usize {
240        self.counters.dwt97_batch_dispatches()
241    }
242
243    /// Number of 9/7 code-block-ready batches offered to this accelerator.
244    #[must_use]
245    pub const fn htj2k97_codeblock_batch_attempts(&self) -> usize {
246        self.counters.htj2k97_codeblock_batch_attempts()
247    }
248
249    /// Number of 9/7 code-block-ready batches handled by Metal.
250    #[must_use]
251    pub const fn htj2k97_codeblock_batch_dispatches(&self) -> usize {
252        self.counters.htj2k97_codeblock_batch_dispatches()
253    }
254
255    /// Backend stage timings for the most recent 9/7 batch dispatch.
256    #[must_use]
257    pub const fn last_dwt97_batch_stage_timings(&self) -> Option<Dwt97BatchStageTimings> {
258        self.last_dwt97_batch_stage_timings
259    }
260
261    #[cfg(target_os = "macos")]
262    fn recover<T>(&self, error: MetalTranscodeError) -> Result<Option<T>, TranscodeStageError> {
263        self.mode
264            .recover(error, MetalTranscodeError::is_recoverable)
265    }
266
267    /// Dispatch a same-geometry batch of reversible integer 5/3 DCT-grid projection jobs.
268    pub fn dct_grid_to_reversible_dwt53_batch(
269        &mut self,
270        jobs: &[DctGridToReversibleDwt53Job<'_>],
271    ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
272        self.dispatch_reversible_dwt53_batch(jobs)
273    }
274
275    fn dispatch_reversible_dwt53_batch(
276        &mut self,
277        jobs: &[DctGridToReversibleDwt53Job<'_>],
278    ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
279        self.counters
280            .record(CounterEvent::ReversibleDwt53BatchAttempt, 1);
281        if jobs.is_empty() {
282            return Ok(Some(Vec::new()));
283        }
284        let total_samples = jobs.iter().fold(0usize, |total, job| {
285            total.saturating_add(job.width.saturating_mul(job.height))
286        });
287        if self.mode.is_auto()
288            && (jobs.len() < self.min_auto_reversible_batch_jobs
289                || total_samples < self.min_auto_reversible_batch_samples)
290        {
291            return Ok(None);
292        }
293
294        #[cfg(not(target_os = "macos"))]
295        {
296            self.mode.unavailable()
297        }
298        #[cfg(target_os = "macos")]
299        {
300            match metal::dispatch_dct_grid_to_reversible_dwt53_batch(self.metal_session(), jobs) {
301                Ok(output) => {
302                    self.counters
303                        .record(CounterEvent::ReversibleDwt53BatchDispatch, 1);
304                    Ok(Some(output))
305                }
306                Err(error) => self.recover(error),
307            }
308        }
309    }
310}
311
312impl Default for MetalDctToWaveletStageAccelerator {
313    fn default() -> Self {
314        Self::for_auto()
315    }
316}