1#[cfg(target_os = "macos")]
10mod metal;
11
12#[doc(hidden)]
13pub mod weights;
14
15#[cfg(target_os = "macos")]
16pub use metal::MetalTranscodeSession;
17
18use core::fmt;
19
20use j2k_transcode::accelerator::{
21 DctGridToDwt53Job, DctGridToDwt97Job, DctGridToHtj2k97CodeBlockJob,
22 DctGridToReversibleDwt53Job, DctToWaveletStageAccelerator, Dwt97BatchStageTimings,
23 Htj2k97CodeBlockOptions, PrequantizedHtj2k97Component, ReversibleDwt53FirstLevel,
24 TranscodeStageError,
25};
26use j2k_transcode::dct53_2d::Dwt53TwoDimensional;
27use j2k_transcode::dct97_2d::Dwt97TwoDimensional;
28
29pub const METAL_UNAVAILABLE: &str = "Metal is unavailable on this host";
31
32const DEFAULT_AUTO_MIN_SAMPLES: usize = 224 * 224;
33const DEFAULT_AUTO_DWT97_MIN_SAMPLES: usize = usize::MAX;
34const DEFAULT_AUTO_REVERSIBLE_MIN_SAMPLES: usize = usize::MAX;
35const DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_JOBS: usize = 32;
36const DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_SAMPLES: usize = 224 * 224 * 32;
37const DEFAULT_AUTO_DWT97_BATCH_MIN_JOBS: usize = 32;
38const DEFAULT_AUTO_DWT97_BATCH_MIN_SAMPLES: usize = 224 * 224 * 32;
39const MAX_AUTO_DWT97_STAGED_BATCH_AXIS: usize = 1024;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum MetalTranscodeError {
44 MetalUnavailable,
46 UnsupportedJob(&'static str),
48 Runtime(&'static str),
50 Kernel(&'static str),
52}
53
54impl fmt::Display for MetalTranscodeError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::MetalUnavailable => f.write_str(METAL_UNAVAILABLE),
58 Self::UnsupportedJob(reason) | Self::Runtime(reason) | Self::Kernel(reason) => {
59 f.write_str(reason)
60 }
61 }
62 }
63}
64
65impl From<MetalTranscodeError> for TranscodeStageError {
66 fn from(error: MetalTranscodeError) -> Self {
67 match error {
68 MetalTranscodeError::MetalUnavailable => Self::DeviceUnavailable,
69 MetalTranscodeError::UnsupportedJob(reason) => Self::Unsupported(reason),
70 MetalTranscodeError::Runtime(reason) | MetalTranscodeError::Kernel(reason) => {
71 Self::Backend(reason.to_string())
72 }
73 }
74 }
75}
76
77impl std::error::Error for MetalTranscodeError {}
78
79#[cfg(not(target_os = "macos"))]
80#[derive(Clone, Copy, Debug, Default)]
81pub struct MetalTranscodeSession {
83 _private: (),
84}
85
86#[cfg(not(target_os = "macos"))]
87impl MetalTranscodeSession {
88 pub const fn system_default() -> Result<Self, MetalTranscodeError> {
90 Err(MetalTranscodeError::MetalUnavailable)
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct MetalDctToWaveletStageAccelerator {
97 mode: MetalDispatchMode,
98 min_auto_samples: usize,
99 min_auto_dwt97_samples: usize,
100 min_auto_reversible_samples: usize,
101 min_auto_reversible_batch_jobs: usize,
102 min_auto_reversible_batch_samples: usize,
103 reversible_dwt53_attempts: usize,
104 reversible_dwt53_dispatches: usize,
105 reversible_dwt53_batch_attempts: usize,
106 reversible_dwt53_batch_dispatches: usize,
107 dwt53_attempts: usize,
108 dwt53_dispatches: usize,
109 dwt97_attempts: usize,
110 dwt97_dispatches: usize,
111 dwt97_batch_attempts: usize,
112 dwt97_batch_dispatches: usize,
113 htj2k97_codeblock_batch_attempts: usize,
114 htj2k97_codeblock_batch_dispatches: usize,
115 last_dwt97_batch_stage_timings: Option<Dwt97BatchStageTimings>,
116 min_auto_dwt97_batch_jobs: usize,
117 min_auto_dwt97_batch_samples: usize,
118 #[cfg(target_os = "macos")]
119 session: Option<MetalTranscodeSession>,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123enum MetalDispatchMode {
124 Explicit,
125 Auto,
126}
127
128impl MetalDctToWaveletStageAccelerator {
129 #[must_use]
132 pub const fn new_explicit() -> Self {
133 Self {
134 mode: MetalDispatchMode::Explicit,
135 min_auto_samples: 0,
136 min_auto_dwt97_samples: 0,
137 min_auto_reversible_samples: 0,
138 min_auto_reversible_batch_jobs: 0,
139 min_auto_reversible_batch_samples: 0,
140 reversible_dwt53_attempts: 0,
141 reversible_dwt53_dispatches: 0,
142 reversible_dwt53_batch_attempts: 0,
143 reversible_dwt53_batch_dispatches: 0,
144 dwt53_attempts: 0,
145 dwt53_dispatches: 0,
146 dwt97_attempts: 0,
147 dwt97_dispatches: 0,
148 dwt97_batch_attempts: 0,
149 dwt97_batch_dispatches: 0,
150 htj2k97_codeblock_batch_attempts: 0,
151 htj2k97_codeblock_batch_dispatches: 0,
152 last_dwt97_batch_stage_timings: None,
153 min_auto_dwt97_batch_jobs: 0,
154 min_auto_dwt97_batch_samples: 0,
155 #[cfg(target_os = "macos")]
156 session: None,
157 }
158 }
159
160 #[must_use]
163 pub const fn for_auto() -> Self {
164 Self {
165 mode: MetalDispatchMode::Auto,
166 min_auto_samples: DEFAULT_AUTO_MIN_SAMPLES,
167 min_auto_dwt97_samples: DEFAULT_AUTO_DWT97_MIN_SAMPLES,
168 min_auto_reversible_samples: DEFAULT_AUTO_REVERSIBLE_MIN_SAMPLES,
169 min_auto_reversible_batch_jobs: DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_JOBS,
170 min_auto_reversible_batch_samples: DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_SAMPLES,
171 reversible_dwt53_attempts: 0,
172 reversible_dwt53_dispatches: 0,
173 reversible_dwt53_batch_attempts: 0,
174 reversible_dwt53_batch_dispatches: 0,
175 dwt53_attempts: 0,
176 dwt53_dispatches: 0,
177 dwt97_attempts: 0,
178 dwt97_dispatches: 0,
179 dwt97_batch_attempts: 0,
180 dwt97_batch_dispatches: 0,
181 htj2k97_codeblock_batch_attempts: 0,
182 htj2k97_codeblock_batch_dispatches: 0,
183 last_dwt97_batch_stage_timings: None,
184 min_auto_dwt97_batch_jobs: DEFAULT_AUTO_DWT97_BATCH_MIN_JOBS,
185 min_auto_dwt97_batch_samples: DEFAULT_AUTO_DWT97_BATCH_MIN_SAMPLES,
186 #[cfg(target_os = "macos")]
187 session: None,
188 }
189 }
190
191 #[cfg(target_os = "macos")]
193 #[must_use]
194 pub fn new_explicit_with_session(session: MetalTranscodeSession) -> Self {
195 Self::new_explicit().with_session(session)
196 }
197
198 #[cfg(target_os = "macos")]
200 #[must_use]
201 pub fn for_auto_with_session(session: MetalTranscodeSession) -> Self {
202 Self::for_auto().with_session(session)
203 }
204
205 #[cfg(target_os = "macos")]
207 #[must_use]
208 pub fn new_explicit_with_device(device: ::metal::Device) -> Self {
209 Self::new_explicit_with_session(MetalTranscodeSession::new(device))
210 }
211
212 #[cfg(target_os = "macos")]
214 #[must_use]
215 pub fn for_auto_with_device(device: ::metal::Device) -> Self {
216 Self::for_auto_with_session(MetalTranscodeSession::new(device))
217 }
218
219 #[cfg(target_os = "macos")]
221 #[must_use]
222 pub fn with_session(mut self, session: MetalTranscodeSession) -> Self {
223 self.session = Some(session);
224 self
225 }
226
227 #[cfg(target_os = "macos")]
229 #[must_use]
230 pub fn with_device(self, device: ::metal::Device) -> Self {
231 self.with_session(MetalTranscodeSession::new(device))
232 }
233
234 #[cfg(target_os = "macos")]
235 fn metal_session(&mut self) -> &mut MetalTranscodeSession {
236 self.session
237 .get_or_insert_with(MetalTranscodeSession::default)
238 }
239
240 #[must_use]
243 pub const fn with_auto_min_samples(mut self, min_samples: usize) -> Self {
244 self.min_auto_samples = min_samples;
245 self.min_auto_dwt97_samples = min_samples;
246 self
247 }
248
249 #[must_use]
252 pub const fn with_auto_dwt97_min_samples(mut self, min_samples: usize) -> Self {
253 self.min_auto_dwt97_samples = min_samples;
254 self
255 }
256
257 #[must_use]
260 pub const fn with_auto_dwt97_batch_thresholds(
261 mut self,
262 min_jobs: usize,
263 min_samples: usize,
264 ) -> Self {
265 self.min_auto_dwt97_batch_jobs = min_jobs;
266 self.min_auto_dwt97_batch_samples = min_samples;
267 self
268 }
269
270 #[must_use]
273 pub const fn with_auto_reversible_min_samples(mut self, min_samples: usize) -> Self {
274 self.min_auto_reversible_samples = min_samples;
275 self
276 }
277
278 #[must_use]
281 pub const fn with_auto_reversible_batch_thresholds(
282 mut self,
283 min_jobs: usize,
284 min_samples: usize,
285 ) -> Self {
286 self.min_auto_reversible_batch_jobs = min_jobs;
287 self.min_auto_reversible_batch_samples = min_samples;
288 self
289 }
290
291 #[must_use]
293 pub const fn reversible_dwt53_attempts(&self) -> usize {
294 self.reversible_dwt53_attempts
295 }
296
297 #[must_use]
299 pub const fn reversible_dwt53_dispatches(&self) -> usize {
300 self.reversible_dwt53_dispatches
301 }
302
303 #[must_use]
305 pub const fn reversible_dwt53_batch_attempts(&self) -> usize {
306 self.reversible_dwt53_batch_attempts
307 }
308
309 #[must_use]
311 pub const fn reversible_dwt53_batch_dispatches(&self) -> usize {
312 self.reversible_dwt53_batch_dispatches
313 }
314
315 #[must_use]
317 pub const fn dwt53_attempts(&self) -> usize {
318 self.dwt53_attempts
319 }
320
321 #[must_use]
323 pub const fn dwt53_dispatches(&self) -> usize {
324 self.dwt53_dispatches
325 }
326
327 #[must_use]
329 pub const fn dwt97_attempts(&self) -> usize {
330 self.dwt97_attempts
331 }
332
333 #[must_use]
335 pub const fn dwt97_dispatches(&self) -> usize {
336 self.dwt97_dispatches
337 }
338
339 #[must_use]
341 pub const fn dwt97_batch_attempts(&self) -> usize {
342 self.dwt97_batch_attempts
343 }
344
345 #[must_use]
347 pub const fn dwt97_batch_dispatches(&self) -> usize {
348 self.dwt97_batch_dispatches
349 }
350
351 #[must_use]
353 pub const fn htj2k97_codeblock_batch_attempts(&self) -> usize {
354 self.htj2k97_codeblock_batch_attempts
355 }
356
357 #[must_use]
359 pub const fn htj2k97_codeblock_batch_dispatches(&self) -> usize {
360 self.htj2k97_codeblock_batch_dispatches
361 }
362
363 #[must_use]
365 pub const fn last_dwt97_batch_stage_timings(&self) -> Option<Dwt97BatchStageTimings> {
366 self.last_dwt97_batch_stage_timings
367 }
368
369 pub fn dct_grid_to_reversible_dwt53_batch(
373 &mut self,
374 jobs: &[DctGridToReversibleDwt53Job<'_>],
375 ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
376 self.dispatch_reversible_dwt53_batch(jobs)
377 }
378
379 fn dispatch_reversible_dwt53_batch(
380 &mut self,
381 jobs: &[DctGridToReversibleDwt53Job<'_>],
382 ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
383 self.reversible_dwt53_batch_attempts =
384 self.reversible_dwt53_batch_attempts.saturating_add(1);
385
386 if jobs.is_empty() {
387 return Ok(Some(Vec::new()));
388 }
389
390 let total_samples = jobs.iter().fold(0usize, |total, job| {
391 total.saturating_add(job.width.saturating_mul(job.height))
392 });
393 if self.mode == MetalDispatchMode::Auto
398 && (jobs.len() < self.min_auto_reversible_batch_jobs
399 || total_samples < self.min_auto_reversible_batch_samples)
400 {
401 return Ok(None);
402 }
403
404 #[cfg(not(target_os = "macos"))]
405 {
406 match self.mode {
407 MetalDispatchMode::Explicit => Err(TranscodeStageError::DeviceUnavailable),
408 MetalDispatchMode::Auto => Ok(None),
409 }
410 }
411
412 #[cfg(target_os = "macos")]
413 {
414 match metal::dispatch_dct_grid_to_reversible_dwt53_batch(self.metal_session(), jobs) {
415 Ok(output) => {
416 self.reversible_dwt53_batch_dispatches =
417 self.reversible_dwt53_batch_dispatches.saturating_add(1);
418 Ok(Some(output))
419 }
420 Err(
421 MetalTranscodeError::MetalUnavailable | MetalTranscodeError::UnsupportedJob(_),
422 ) if self.mode == MetalDispatchMode::Auto => Ok(None),
423 Err(error) => Err(error.into()),
424 }
425 }
426 }
427}
428
429impl Default for MetalDctToWaveletStageAccelerator {
430 fn default() -> Self {
431 Self::for_auto()
432 }
433}
434
435impl DctToWaveletStageAccelerator for MetalDctToWaveletStageAccelerator {
436 fn supports_dwt97_batch(&self) -> bool {
437 true
438 }
439
440 fn supports_htj2k97_codeblock_batch(&self) -> bool {
441 true
442 }
443
444 fn dct_grid_to_reversible_dwt53(
445 &mut self,
446 job: DctGridToReversibleDwt53Job<'_>,
447 ) -> Result<Option<ReversibleDwt53FirstLevel>, TranscodeStageError> {
448 self.reversible_dwt53_attempts = self.reversible_dwt53_attempts.saturating_add(1);
449
450 if self.mode == MetalDispatchMode::Auto
452 && job.width.saturating_mul(job.height) < self.min_auto_reversible_samples
453 {
454 return Ok(None);
455 }
456
457 #[cfg(not(target_os = "macos"))]
458 {
459 match self.mode {
460 MetalDispatchMode::Explicit => Err(TranscodeStageError::DeviceUnavailable),
461 MetalDispatchMode::Auto => Ok(None),
462 }
463 }
464
465 #[cfg(target_os = "macos")]
466 {
467 match metal::dispatch_dct_grid_to_reversible_dwt53(self.metal_session(), job) {
468 Ok(output) => {
469 self.reversible_dwt53_dispatches =
470 self.reversible_dwt53_dispatches.saturating_add(1);
471 Ok(Some(output))
472 }
473 Err(
474 MetalTranscodeError::MetalUnavailable | MetalTranscodeError::UnsupportedJob(_),
475 ) if self.mode == MetalDispatchMode::Auto => Ok(None),
476 Err(error) => Err(error.into()),
477 }
478 }
479 }
480
481 fn dct_grid_to_reversible_dwt53_batch(
482 &mut self,
483 jobs: &[DctGridToReversibleDwt53Job<'_>],
484 ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
485 self.dispatch_reversible_dwt53_batch(jobs)
486 }
487
488 fn dct_grid_to_dwt53(
489 &mut self,
490 job: DctGridToDwt53Job<'_>,
491 ) -> Result<Option<Dwt53TwoDimensional<f64>>, TranscodeStageError> {
492 self.dwt53_attempts = self.dwt53_attempts.saturating_add(1);
493
494 if self.mode == MetalDispatchMode::Auto
495 && job.width.saturating_mul(job.height) < self.min_auto_samples
496 {
497 return Ok(None);
498 }
499
500 #[cfg(not(target_os = "macos"))]
501 {
502 let _ = job;
503 match self.mode {
504 MetalDispatchMode::Explicit => Err(TranscodeStageError::DeviceUnavailable),
505 MetalDispatchMode::Auto => Ok(None),
506 }
507 }
508
509 #[cfg(target_os = "macos")]
510 {
511 match metal::dispatch_dct_grid_to_dwt53(self.metal_session(), job) {
512 Ok(output) => {
513 self.dwt53_dispatches = self.dwt53_dispatches.saturating_add(1);
514 Ok(Some(output))
515 }
516 Err(
517 MetalTranscodeError::MetalUnavailable | MetalTranscodeError::UnsupportedJob(_),
518 ) if self.mode == MetalDispatchMode::Auto => Ok(None),
519 Err(error) => Err(error.into()),
520 }
521 }
522 }
523
524 fn dct_grid_to_dwt97(
525 &mut self,
526 job: DctGridToDwt97Job<'_>,
527 ) -> Result<Option<Dwt97TwoDimensional<f64>>, TranscodeStageError> {
528 self.dwt97_attempts = self.dwt97_attempts.saturating_add(1);
529
530 if self.mode == MetalDispatchMode::Auto
531 && job.width.saturating_mul(job.height) < self.min_auto_dwt97_samples
532 {
533 return Ok(None);
534 }
535
536 #[cfg(not(target_os = "macos"))]
537 {
538 let _ = job;
539 match self.mode {
540 MetalDispatchMode::Explicit => Err(TranscodeStageError::DeviceUnavailable),
541 MetalDispatchMode::Auto => Ok(None),
542 }
543 }
544
545 #[cfg(target_os = "macos")]
546 {
547 match metal::dispatch_dct_grid_to_dwt97(self.metal_session(), job) {
548 Ok(output) => {
549 self.dwt97_dispatches = self.dwt97_dispatches.saturating_add(1);
550 Ok(Some(output))
551 }
552 Err(
553 MetalTranscodeError::MetalUnavailable | MetalTranscodeError::UnsupportedJob(_),
554 ) if self.mode == MetalDispatchMode::Auto => Ok(None),
555 Err(error) => Err(error.into()),
556 }
557 }
558 }
559
560 fn dct_grid_to_dwt97_batch(
561 &mut self,
562 jobs: &[DctGridToDwt97Job<'_>],
563 ) -> Result<Option<Vec<Dwt97TwoDimensional<f64>>>, TranscodeStageError> {
564 self.dwt97_batch_attempts = self.dwt97_batch_attempts.saturating_add(1);
565 self.last_dwt97_batch_stage_timings = None;
566
567 if jobs.is_empty() {
568 return Ok(Some(Vec::new()));
569 }
570
571 let total_samples = jobs.iter().fold(0usize, |total, job| {
572 total.saturating_add(job.width.saturating_mul(job.height))
573 });
574 if self.mode == MetalDispatchMode::Auto
575 && (jobs.len() < self.min_auto_dwt97_batch_jobs
576 || total_samples < self.min_auto_dwt97_batch_samples)
577 {
578 return Ok(None);
579 }
580 if self.mode == MetalDispatchMode::Auto
581 && jobs.iter().any(|job| {
582 job.width > MAX_AUTO_DWT97_STAGED_BATCH_AXIS
583 || job.height > MAX_AUTO_DWT97_STAGED_BATCH_AXIS
584 })
585 {
586 return Ok(None);
587 }
588
589 #[cfg(not(target_os = "macos"))]
590 {
591 let _ = jobs;
592 match self.mode {
593 MetalDispatchMode::Explicit => Err(TranscodeStageError::DeviceUnavailable),
594 MetalDispatchMode::Auto => Ok(None),
595 }
596 }
597
598 #[cfg(target_os = "macos")]
599 {
600 match metal::dispatch_dct_grid_to_dwt97_batch(self.metal_session(), jobs) {
601 Ok((output, timings)) => {
602 self.dwt97_batch_dispatches = self.dwt97_batch_dispatches.saturating_add(1);
603 self.last_dwt97_batch_stage_timings = Some(timings);
604 Ok(Some(output))
605 }
606 Err(
607 MetalTranscodeError::MetalUnavailable | MetalTranscodeError::UnsupportedJob(_),
608 ) if self.mode == MetalDispatchMode::Auto => Ok(None),
609 Err(error) => Err(error.into()),
610 }
611 }
612 }
613
614 fn dct_grid_to_htj2k97_codeblock_batch(
615 &mut self,
616 jobs: &[DctGridToHtj2k97CodeBlockJob<'_>],
617 options: Htj2k97CodeBlockOptions,
618 ) -> Result<Option<Vec<PrequantizedHtj2k97Component>>, TranscodeStageError> {
619 self.dwt97_batch_attempts = self.dwt97_batch_attempts.saturating_add(1);
620 self.htj2k97_codeblock_batch_attempts =
621 self.htj2k97_codeblock_batch_attempts.saturating_add(1);
622 self.last_dwt97_batch_stage_timings = None;
623
624 if jobs.is_empty() {
625 return Ok(Some(Vec::new()));
626 }
627
628 let total_samples = jobs.iter().fold(0usize, |total, job| {
629 total.saturating_add(job.width.saturating_mul(job.height))
630 });
631 if self.mode == MetalDispatchMode::Auto
632 && (jobs.len() < self.min_auto_dwt97_batch_jobs
633 || total_samples < self.min_auto_dwt97_batch_samples)
634 {
635 return Ok(None);
636 }
637 if self.mode == MetalDispatchMode::Auto
638 && jobs.iter().any(|job| {
639 job.width > MAX_AUTO_DWT97_STAGED_BATCH_AXIS
640 || job.height > MAX_AUTO_DWT97_STAGED_BATCH_AXIS
641 })
642 {
643 return Ok(None);
644 }
645
646 #[cfg(not(target_os = "macos"))]
647 {
648 let _ = (jobs, options);
649 match self.mode {
650 MetalDispatchMode::Explicit => Err(TranscodeStageError::DeviceUnavailable),
651 MetalDispatchMode::Auto => Ok(None),
652 }
653 }
654
655 #[cfg(target_os = "macos")]
656 {
657 match metal::dispatch_dct_grid_to_htj2k97_codeblock_batch(
658 self.metal_session(),
659 jobs,
660 options,
661 ) {
662 Ok((output, timings)) => {
663 self.dwt97_batch_dispatches = self.dwt97_batch_dispatches.saturating_add(1);
664 self.htj2k97_codeblock_batch_dispatches =
665 self.htj2k97_codeblock_batch_dispatches.saturating_add(1);
666 self.last_dwt97_batch_stage_timings = Some(timings);
667 Ok(Some(output))
668 }
669 Err(
670 MetalTranscodeError::MetalUnavailable | MetalTranscodeError::UnsupportedJob(_),
671 ) if self.mode == MetalDispatchMode::Auto => Ok(None),
672 Err(error) => Err(error.into()),
673 }
674 }
675 }
676
677 fn last_dwt97_batch_stage_timings(&self) -> Option<Dwt97BatchStageTimings> {
678 self.last_dwt97_batch_stage_timings
679 }
680}