1#![deny(unsafe_op_in_unsafe_fn)]
11#![warn(unreachable_pub)]
12
13mod batch;
14#[cfg(target_os = "macos")]
15mod buffer_pool;
16#[cfg(any(test, target_os = "macos"))]
17mod classic;
18#[cfg(target_os = "macos")]
19mod compute;
20#[cfg(target_os = "macos")]
21mod direct;
22mod encode;
23#[cfg(any(test, target_os = "macos"))]
24mod ht;
25#[cfg(target_os = "macos")]
26mod hybrid;
27#[cfg(any(test, target_os = "macos"))]
28mod idwt;
29#[cfg(any(test, target_os = "macos"))]
30mod mct;
31mod profile;
32#[cfg(target_os = "macos")]
33mod profile_env;
34mod routing;
35#[cfg(any(test, target_os = "macos"))]
36mod store;
37
38use core::convert::Infallible;
39#[cfg(target_os = "macos")]
40use std::{
41 collections::{hash_map::DefaultHasher, HashMap},
42 hash::{Hash, Hasher},
43 sync::{Mutex, OnceLock},
44};
45use std::{ops::Range, sync::Arc};
46
47use j2k::{
48 adapter::device_plan::{DeviceDecodePlan, DeviceDecodeRequest},
49 J2kContext as CpuJ2kContext, J2kDecoder as CpuDecoder, J2kError,
50 J2kScratchPool as CpuJ2kScratchPool, J2kView,
51};
52use j2k_core::{
53 copy_tight_pixels_to_strided_output, BackendKind, BackendRequest, BufferError, CodecError,
54 DecodeOutcome, DeviceMemoryRange, DeviceSubmission, DeviceSurface, Downscale, ImageCodec,
55 ImageDecode, ImageDecodeDevice, ImageDecodeSubmit, PixelFormat, ReadySubmission, Rect,
56 TileBatchDecodeDevice, TileBatchDecodeManyDevice, TileBatchDecodeSubmit,
57};
58#[cfg(target_os = "macos")]
59use j2k_native::{
60 DecodeSettings as NativeDecodeSettings, DecoderContext as NativeDecoderContext,
61 Image as NativeImage, J2kDirectColorPlan, J2kDirectGrayscalePlan,
62};
63
64#[cfg(target_os = "macos")]
65use j2k_metal_support::{system_default_device, MetalSupportError};
66#[cfg(target_os = "macos")]
67use metal::foreign_types::ForeignType;
68#[cfg(target_os = "macos")]
69use metal::{Buffer, Device, MTLResourceOptions};
70
71#[doc(hidden)]
72pub use batch::{benchmark_group_region_scaled_requests, BenchmarkGroupedRequests};
73pub use encode::{
74 encode_lossless_batch_with_report, submit_lossless_batch, submit_lossless_batch_to_metal,
75 validate_lossless_roundtrip_on_metal, validate_lossless_roundtrip_on_metal_with_session,
76 MetalEncodeInputStaging, MetalEncodeStageAccelerator, MetalEncodedJ2k,
77 MetalLosslessBufferEncodeBatchOutcome, MetalLosslessBufferEncodeOutcome,
78 MetalLosslessEncodeBatchRequest, MetalLosslessEncodeBatchStats, MetalLosslessEncodeConfig,
79 MetalLosslessEncodeOutcome, MetalLosslessEncodeResidency, MetalLosslessEncodeStageStats,
80 MetalLosslessEncodeTile, SubmittedJ2kLosslessMetalBufferEncodeBatch,
81 SubmittedJ2kLosslessMetalEncodeBatch,
82};
83
84#[cfg(target_os = "macos")]
85#[doc(hidden)]
86pub fn benchmark_region_scaled_direct_plan_prepare(
87 input: &[u8],
88 fmt: PixelFormat,
89 roi: Rect,
90 scale: Downscale,
91) -> Result<(), Error> {
92 hybrid::benchmark_region_scaled_direct_plan_prepare(input, fmt, roi, scale)
93}
94
95#[cfg(not(target_os = "macos"))]
96#[doc(hidden)]
97pub fn benchmark_region_scaled_direct_plan_prepare(
98 _input: &[u8],
99 _fmt: PixelFormat,
100 _roi: Rect,
101 _scale: Downscale,
102) -> Result<(), Error> {
103 Err(Error::MetalUnavailable)
104}
105
106#[derive(Debug, thiserror::Error)]
107pub enum Error {
109 #[error(transparent)]
111 Decode(#[from] J2kError),
112 #[error(transparent)]
114 Buffer(#[from] BufferError),
115 #[error("backend request {request:?} is not supported by j2k-metal")]
117 UnsupportedBackend {
118 request: BackendRequest,
120 },
121 #[error("unsupported J2K Metal request: {reason}")]
123 UnsupportedMetalRequest {
124 reason: &'static str,
126 },
127 #[error("Metal is unavailable on this host")]
129 MetalUnavailable,
130 #[error("Metal runtime error: {message}")]
132 MetalRuntime {
133 message: String,
135 },
136 #[error("Metal kernel error: {message}")]
138 MetalKernel {
139 message: String,
141 },
142 #[error("Metal state `{state}` is poisoned")]
144 MetalStatePoisoned {
145 state: &'static str,
147 },
148}
149
150impl CodecError for Error {
151 fn is_truncated(&self) -> bool {
152 matches!(self, Self::Decode(inner) if inner.is_truncated())
153 }
154
155 fn is_not_implemented(&self) -> bool {
156 matches!(self, Self::Decode(inner) if inner.is_not_implemented())
157 }
158
159 fn is_unsupported(&self) -> bool {
160 matches!(
161 self,
162 Self::UnsupportedBackend { .. }
163 | Self::UnsupportedMetalRequest { .. }
164 | Self::MetalUnavailable
165 ) || matches!(self, Self::Decode(inner) if inner.is_unsupported())
166 }
167
168 fn is_buffer_error(&self) -> bool {
169 matches!(self, Self::Buffer(_))
170 || matches!(self, Self::Decode(inner) if inner.is_buffer_error())
171 }
172}
173
174#[derive(Clone)]
175pub(crate) enum Storage {
176 Host(Vec<u8>),
177 #[cfg(target_os = "macos")]
178 Metal(Buffer),
179}
180
181#[cfg(target_os = "macos")]
182#[derive(Clone)]
183struct DirectGrayPlanCacheEntry {
184 plan: J2kDirectGrayscalePlan,
185 prepared: Arc<crate::compute::PreparedDirectGrayscalePlan>,
186}
187
188#[cfg(target_os = "macos")]
189#[derive(Clone)]
190struct DirectColorPlanCacheEntry {
191 plan: J2kDirectColorPlan,
192 prepared: Arc<crate::compute::PreparedDirectColorPlan>,
193}
194
195#[cfg(target_os = "macos")]
196const DIRECT_PLAN_CACHE_CAP: usize = 128;
197#[cfg(target_os = "macos")]
198const AUTO_REPEATED_GRAYSCALE_MIN_DIM: u32 = 512;
199#[cfg(target_os = "macos")]
200const AUTO_REPEATED_GRAYSCALE_MIN_COUNT: usize = 16;
201
202#[derive(Clone)]
203pub struct Surface {
205 backend: BackendKind,
206 residency: SurfaceResidency,
207 dimensions: (u32, u32),
208 fmt: PixelFormat,
209 pitch_bytes: usize,
210 byte_offset: usize,
211 storage: Storage,
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum SurfaceResidency {
217 Host,
219 MetalResidentDecode,
221 CpuStagedMetalUpload,
223}
224
225#[cfg(target_os = "macos")]
226const CPU_STAGED_METAL_REQUIRES_EXPLICIT_API: &str =
227 "CPU-staged Metal upload requires the explicit CPU-staged API; BackendRequest::Metal only accepts resident Metal decode";
228impl Surface {
229 pub fn residency(&self) -> SurfaceResidency {
231 self.residency
232 }
233
234 pub fn pitch_bytes(&self) -> usize {
236 self.pitch_bytes
237 }
238
239 fn checked_storage_range(&self, storage_len: usize) -> Result<Range<usize>, Error> {
240 let len = self.byte_len();
241 let end = self
242 .byte_offset
243 .checked_add(len)
244 .ok_or_else(|| Error::MetalKernel {
245 message: "J2K Metal surface byte range overflows usize".to_string(),
246 })?;
247 if end > storage_len {
248 return Err(Error::MetalKernel {
249 message: format!(
250 "J2K Metal surface byte range {start}..{end} exceeds storage length {storage_len}",
251 start = self.byte_offset
252 ),
253 });
254 }
255 Ok(self.byte_offset..end)
256 }
257
258 fn storage_bytes(&self) -> Result<&[u8], Error> {
259 match &self.storage {
260 Storage::Host(bytes) => {
261 let range = self.checked_storage_range(bytes.len())?;
262 Ok(&bytes[range])
263 }
264 #[cfg(target_os = "macos")]
265 Storage::Metal(buffer) => {
266 let storage_len =
267 usize::try_from(buffer.length()).map_err(|_| Error::MetalKernel {
268 message: "J2K Metal buffer length does not fit usize".to_string(),
269 })?;
270 let range = self.checked_storage_range(storage_len)?;
271 let contents = buffer.contents();
272 if contents.is_null() {
273 return Err(Error::MetalKernel {
274 message: "J2K Metal surface buffer is not host-addressable".to_string(),
275 });
276 }
277 unsafe {
279 Ok(core::slice::from_raw_parts(
280 contents.cast::<u8>().add(range.start),
281 range.len(),
282 ))
283 }
284 }
285 }
286 }
287
288 pub fn as_bytes(&self) -> &[u8] {
294 self.storage_bytes()
295 .expect("validated J2K Metal surface byte range")
296 }
297
298 pub fn download_into(&self, out: &mut [u8], stride: usize) -> Result<(), Error> {
300 copy_tight_pixels_to_strided_output(
301 self.storage_bytes()?,
302 self.dimensions,
303 self.fmt,
304 out,
305 stride,
306 )
307 .map_err(Error::from)
308 }
309
310 #[cfg(target_os = "macos")]
311 pub fn metal_buffer(&self) -> Option<(&Buffer, usize)> {
313 match &self.storage {
314 Storage::Metal(buffer) => Some((buffer, self.byte_offset)),
315 Storage::Host(_) => None,
316 }
317 }
318
319 #[cfg(target_os = "macos")]
320 pub(crate) fn from_metal_buffer(
321 buffer: Buffer,
322 dimensions: (u32, u32),
323 fmt: PixelFormat,
324 ) -> Self {
325 Self {
326 backend: BackendKind::Metal,
327 residency: SurfaceResidency::MetalResidentDecode,
328 dimensions,
329 fmt,
330 pitch_bytes: dimensions.0 as usize * fmt.bytes_per_pixel(),
331 byte_offset: 0,
332 storage: Storage::Metal(buffer),
333 }
334 }
335
336 #[cfg(target_os = "macos")]
337 pub(crate) fn from_metal_buffer_with_offset(
338 buffer: Buffer,
339 dimensions: (u32, u32),
340 fmt: PixelFormat,
341 byte_offset: usize,
342 ) -> Self {
343 Self {
344 backend: BackendKind::Metal,
345 residency: SurfaceResidency::MetalResidentDecode,
346 dimensions,
347 fmt,
348 pitch_bytes: dimensions.0 as usize * fmt.bytes_per_pixel(),
349 byte_offset,
350 storage: Storage::Metal(buffer),
351 }
352 }
353}
354
355impl DeviceSurface for Surface {
356 fn backend_kind(&self) -> BackendKind {
357 self.backend
358 }
359
360 fn residency(&self) -> j2k_core::SurfaceResidency {
361 match self.residency {
362 SurfaceResidency::Host => j2k_core::SurfaceResidency::Host,
363 SurfaceResidency::MetalResidentDecode => {
364 j2k_core::SurfaceResidency::MetalResidentDecode
365 }
366 SurfaceResidency::CpuStagedMetalUpload => {
367 j2k_core::SurfaceResidency::CpuStagedMetalUpload
368 }
369 }
370 }
371
372 fn dimensions(&self) -> (u32, u32) {
373 self.dimensions
374 }
375
376 fn pixel_format(&self) -> PixelFormat {
377 self.fmt
378 }
379
380 fn byte_len(&self) -> usize {
381 self.pitch_bytes * self.dimensions.1 as usize
382 }
383
384 fn memory_range(&self) -> Option<DeviceMemoryRange> {
385 match &self.storage {
386 Storage::Host(_) => None,
387 #[cfg(target_os = "macos")]
388 Storage::Metal(buffer) => Some(DeviceMemoryRange::new(
389 BackendKind::Metal,
390 u64::try_from(buffer.as_ptr() as usize).ok()?,
391 self.byte_offset,
392 self.byte_len(),
393 )),
394 }
395 }
396}
397
398#[cfg(target_os = "macos")]
399#[derive(Clone)]
400pub struct MetalBackendSession {
402 device: Device,
403 runtime: Arc<OnceLock<Result<Arc<crate::compute::MetalRuntime>, MetalSupportError>>>,
404 direct_gray_plan_cache: Arc<Mutex<HashMap<u64, DirectGrayPlanCacheEntry>>>,
405 direct_color_plan_cache: Arc<Mutex<HashMap<u64, DirectColorPlanCacheEntry>>>,
406 region_scaled_color_plan_cache:
407 Arc<Mutex<HashMap<u64, Arc<crate::compute::PreparedDirectColorPlan>>>>,
408}
409
410#[cfg(target_os = "macos")]
411impl MetalBackendSession {
412 pub fn new(device: Device) -> Self {
414 Self {
415 device,
416 runtime: Arc::new(OnceLock::new()),
417 direct_gray_plan_cache: Arc::new(Mutex::new(HashMap::new())),
418 direct_color_plan_cache: Arc::new(Mutex::new(HashMap::new())),
419 region_scaled_color_plan_cache: Arc::new(Mutex::new(HashMap::new())),
420 }
421 }
422
423 pub fn system_default() -> Result<Self, Error> {
425 system_default_device()
426 .map(Self::new)
427 .map_err(|error| crate::compute::runtime_initialization_error(&error))
428 }
429
430 pub fn device(&self) -> &metal::DeviceRef {
432 self.device.as_ref()
433 }
434
435 pub(crate) fn runtime(&self) -> Result<Arc<crate::compute::MetalRuntime>, Error> {
436 match self.runtime.get_or_init(|| {
437 crate::compute::MetalRuntime::new_with_device(&self.device).map(Arc::new)
438 }) {
439 Ok(runtime) => Ok(runtime.clone()),
440 Err(error) => Err(crate::compute::runtime_initialization_error(error)),
441 }
442 }
443
444 #[cfg(test)]
445 pub(crate) fn direct_cache_ids_for_test(&self) -> (usize, usize, usize) {
446 (
447 Arc::as_ptr(&self.direct_gray_plan_cache) as usize,
448 Arc::as_ptr(&self.direct_color_plan_cache) as usize,
449 Arc::as_ptr(&self.region_scaled_color_plan_cache) as usize,
450 )
451 }
452}
453
454#[cfg(target_os = "macos")]
455impl j2k_core::AcceleratorSession for MetalBackendSession {
456 fn backend_kind(&self) -> BackendKind {
457 BackendKind::Metal
458 }
459}
460
461#[cfg(target_os = "macos")]
462impl core::fmt::Debug for MetalBackendSession {
463 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
464 f.debug_struct("MetalBackendSession")
465 .field("device", &self.device.name())
466 .finish_non_exhaustive()
467 }
468}
469
470#[cfg(not(target_os = "macos"))]
471#[derive(Clone, Copy, Debug, Default)]
472pub struct MetalBackendSession {
474 _private: (),
475}
476
477#[cfg(not(target_os = "macos"))]
478impl MetalBackendSession {
479 pub fn system_default() -> Result<Self, Error> {
481 Err(Error::MetalUnavailable)
482 }
483}
484
485#[derive(Clone, Default)]
486pub struct MetalSession {
488 shared: batch::SharedSession,
489 #[cfg(target_os = "macos")]
490 backend: Option<MetalBackendSession>,
491}
492
493impl MetalSession {
494 #[cfg(target_os = "macos")]
496 pub fn with_backend_session(backend: MetalBackendSession) -> Self {
497 Self {
498 shared: batch::SharedSession::default(),
499 backend: Some(backend),
500 }
501 }
502
503 #[cfg(target_os = "macos")]
505 pub fn backend_session(&self) -> Option<&MetalBackendSession> {
506 self.backend.as_ref()
507 }
508
509 pub fn submissions(&self) -> Result<u64, Error> {
511 Ok(self.shared.lock()?.submissions)
512 }
513
514 fn record_submit(&mut self) -> Result<(), Error> {
515 let mut session = self.shared.lock()?;
516 session.submissions = session.submissions.saturating_add(1);
517 Ok(())
518 }
519}
520
521impl core::fmt::Debug for MetalSession {
522 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
523 f.debug_struct("MetalSession")
524 .field("submissions", &self.submissions())
525 .finish()
526 }
527}
528
529#[derive(Default)]
536pub struct MetalTileBatch {
537 session: MetalSession,
538 submissions: Vec<batch::MetalSubmission>,
539}
540
541impl MetalTileBatch {
542 pub fn new() -> Self {
544 Self::default()
545 }
546
547 pub fn with_capacity(capacity: usize) -> Self {
549 Self {
550 submissions: Vec::with_capacity(capacity),
551 ..Self::default()
552 }
553 }
554
555 pub fn len(&self) -> usize {
557 self.submissions.len()
558 }
559
560 pub fn is_empty(&self) -> bool {
562 self.submissions.is_empty()
563 }
564
565 pub fn submissions(&self) -> Result<u64, Error> {
570 self.session.submissions()
571 }
572
573 pub fn push_tile(
576 &mut self,
577 input: &[u8],
578 fmt: PixelFormat,
579 backend: BackendRequest,
580 ) -> Result<usize, Error> {
581 self.push_shared_tile(Arc::<[u8]>::from(input), fmt, backend)
582 }
583
584 pub fn push_shared_tile(
586 &mut self,
587 input: Arc<[u8]>,
588 fmt: PixelFormat,
589 backend: BackendRequest,
590 ) -> Result<usize, Error> {
591 let slot = self.submissions.len();
592 let submission = batch::queue_tile_request_shared(
593 &mut self.session,
594 input,
595 fmt,
596 backend,
597 batch::BatchOp::Full,
598 )?;
599 self.submissions.push(submission);
600 Ok(slot)
601 }
602
603 pub fn push_tile_region(
606 &mut self,
607 input: &[u8],
608 fmt: PixelFormat,
609 roi: Rect,
610 backend: BackendRequest,
611 ) -> Result<usize, Error> {
612 self.push_shared_tile_region(Arc::<[u8]>::from(input), fmt, roi, backend)
613 }
614
615 pub fn push_shared_tile_region(
617 &mut self,
618 input: Arc<[u8]>,
619 fmt: PixelFormat,
620 roi: Rect,
621 backend: BackendRequest,
622 ) -> Result<usize, Error> {
623 let slot = self.submissions.len();
624 let submission = batch::queue_tile_request_shared(
625 &mut self.session,
626 input,
627 fmt,
628 backend,
629 batch::BatchOp::Region(roi),
630 )?;
631 self.submissions.push(submission);
632 Ok(slot)
633 }
634
635 pub fn push_tile_scaled(
638 &mut self,
639 input: &[u8],
640 fmt: PixelFormat,
641 scale: Downscale,
642 backend: BackendRequest,
643 ) -> Result<usize, Error> {
644 self.push_shared_tile_scaled(Arc::<[u8]>::from(input), fmt, scale, backend)
645 }
646
647 pub fn push_shared_tile_scaled(
649 &mut self,
650 input: Arc<[u8]>,
651 fmt: PixelFormat,
652 scale: Downscale,
653 backend: BackendRequest,
654 ) -> Result<usize, Error> {
655 let slot = self.submissions.len();
656 let submission = batch::queue_tile_request_shared(
657 &mut self.session,
658 input,
659 fmt,
660 backend,
661 batch::BatchOp::Scaled(scale),
662 )?;
663 self.submissions.push(submission);
664 Ok(slot)
665 }
666
667 pub fn push_tile_region_scaled(
669 &mut self,
670 input: &[u8],
671 fmt: PixelFormat,
672 roi: Rect,
673 scale: Downscale,
674 backend: BackendRequest,
675 ) -> Result<usize, Error> {
676 self.push_shared_tile_region_scaled(Arc::<[u8]>::from(input), fmt, roi, scale, backend)
677 }
678
679 pub fn push_shared_tile_region_scaled(
681 &mut self,
682 input: Arc<[u8]>,
683 fmt: PixelFormat,
684 roi: Rect,
685 scale: Downscale,
686 backend: BackendRequest,
687 ) -> Result<usize, Error> {
688 let slot = self.submissions.len();
689 let submission = batch::queue_tile_request_shared(
690 &mut self.session,
691 input,
692 fmt,
693 backend,
694 batch::BatchOp::RegionScaled { roi, scale },
695 )?;
696 self.submissions.push(submission);
697 Ok(slot)
698 }
699
700 pub fn decode_all(self) -> Result<Vec<Surface>, Error> {
702 let mut surfaces = Vec::with_capacity(self.submissions.len());
703 for submission in self.submissions {
704 surfaces.push(submission.wait()?);
705 }
706 Ok(surfaces)
707 }
708}
709
710pub struct J2kDecoder<'a> {
712 inner: CpuDecoder<'a>,
713 pool: CpuJ2kScratchPool,
714 #[cfg(target_os = "macos")]
715 native_image: Option<NativeImage<'a>>,
716 #[cfg(target_os = "macos")]
717 native_context: NativeDecoderContext<'a>,
718 #[cfg(target_os = "macos")]
719 native_direct_gray_plan: Option<J2kDirectGrayscalePlan>,
720 #[cfg(target_os = "macos")]
721 native_prepared_direct_gray_plan: Option<Arc<crate::compute::PreparedDirectGrayscalePlan>>,
722 #[cfg(target_os = "macos")]
723 native_direct_color_plan: Option<J2kDirectColorPlan>,
724 #[cfg(target_os = "macos")]
725 native_prepared_direct_color_plan: Option<Arc<crate::compute::PreparedDirectColorPlan>>,
726}
727
728impl<'a> J2kDecoder<'a> {
729 pub fn new(input: &'a [u8]) -> Result<Self, Error> {
731 Ok(Self {
732 inner: CpuDecoder::new(input)?,
733 pool: CpuJ2kScratchPool::new(),
734 #[cfg(target_os = "macos")]
735 native_image: None,
736 #[cfg(target_os = "macos")]
737 native_context: NativeDecoderContext::default(),
738 #[cfg(target_os = "macos")]
739 native_direct_gray_plan: None,
740 #[cfg(target_os = "macos")]
741 native_prepared_direct_gray_plan: None,
742 #[cfg(target_os = "macos")]
743 native_direct_color_plan: None,
744 #[cfg(target_os = "macos")]
745 native_prepared_direct_color_plan: None,
746 })
747 }
748
749 pub fn from_view(view: J2kView<'a>) -> Result<Self, Error> {
751 Ok(Self {
752 inner: CpuDecoder::from_view(view)?,
753 pool: CpuJ2kScratchPool::new(),
754 #[cfg(target_os = "macos")]
755 native_image: None,
756 #[cfg(target_os = "macos")]
757 native_context: NativeDecoderContext::default(),
758 #[cfg(target_os = "macos")]
759 native_direct_gray_plan: None,
760 #[cfg(target_os = "macos")]
761 native_prepared_direct_gray_plan: None,
762 #[cfg(target_os = "macos")]
763 native_direct_color_plan: None,
764 #[cfg(target_os = "macos")]
765 native_prepared_direct_color_plan: None,
766 })
767 }
768
769 pub fn inner(&self) -> &CpuDecoder<'a> {
771 &self.inner
772 }
773
774 pub fn decode_to_device_with_session(
776 &mut self,
777 fmt: PixelFormat,
778 session: &MetalBackendSession,
779 ) -> Result<Surface, Error> {
780 if let Some(error) =
781 routing::decision_error(routing::decide_route(BackendRequest::Metal, fmt))
782 {
783 return Err(error);
784 }
785
786 #[cfg(target_os = "macos")]
787 {
788 crate::compute::with_runtime_for_session(session, |_| {
789 if let Some(surface) = self.decode_direct_to_surface_with_session(fmt, session)? {
790 Ok(surface)
791 } else {
792 self.decode_full_to_metal_surface_with_device(fmt, &session.device)
793 }
794 })
795 }
796 #[cfg(not(target_os = "macos"))]
797 {
798 let _ = session;
799 Err(Error::MetalUnavailable)
800 }
801 }
802
803 pub fn decode_to_host_surface(&mut self, fmt: PixelFormat) -> Result<Surface, Error> {
805 self.decode_to_cpu_surface(fmt)
806 }
807
808 pub fn decode_region_to_host_surface(
810 &mut self,
811 fmt: PixelFormat,
812 roi: Rect,
813 ) -> Result<Surface, Error> {
814 let plan = DeviceDecodePlan::for_image(
815 self.inner.info().dimensions,
816 DeviceDecodeRequest::Region { roi },
817 )?;
818 self.decode_region_to_cpu_surface(fmt, plan)
819 }
820
821 pub fn decode_scaled_to_host_surface(
823 &mut self,
824 fmt: PixelFormat,
825 scale: Downscale,
826 ) -> Result<Surface, Error> {
827 let plan = DeviceDecodePlan::for_image(
828 self.inner.info().dimensions,
829 DeviceDecodeRequest::Scaled { scale },
830 )?;
831 self.decode_scaled_to_cpu_surface(fmt, scale, plan)
832 }
833
834 pub fn decode_region_scaled_to_host_surface(
836 &mut self,
837 fmt: PixelFormat,
838 roi: Rect,
839 scale: Downscale,
840 ) -> Result<Surface, Error> {
841 let plan = DeviceDecodePlan::for_image(
842 self.inner.info().dimensions,
843 DeviceDecodeRequest::RegionScaled { roi, scale },
844 )?;
845 self.decode_region_scaled_to_cpu_surface(fmt, roi, scale, plan)
846 }
847
848 pub fn decode_to_cpu_staged_metal_surface_with_session(
850 &mut self,
851 fmt: PixelFormat,
852 session: &MetalBackendSession,
853 ) -> Result<Surface, Error> {
854 #[cfg(target_os = "macos")]
855 {
856 let dims = self.inner.info().dimensions;
857 let stride = dims.0 as usize * fmt.bytes_per_pixel();
858 let mut out = vec![0u8; stride * dims.1 as usize];
859 self.inner
860 .decode_into_with_scratch(&mut self.pool, &mut out, stride, fmt)?;
861 Ok(upload_surface_to_metal_with_device(
862 &out,
863 dims,
864 fmt,
865 session.device(),
866 ))
867 }
868 #[cfg(not(target_os = "macos"))]
869 {
870 let _ = (fmt, session);
871 Err(Error::MetalUnavailable)
872 }
873 }
874
875 pub fn decode_region_to_cpu_staged_metal_surface_with_session(
877 &mut self,
878 fmt: PixelFormat,
879 roi: Rect,
880 session: &MetalBackendSession,
881 ) -> Result<Surface, Error> {
882 #[cfg(target_os = "macos")]
883 {
884 let plan = DeviceDecodePlan::for_image(
885 self.inner.info().dimensions,
886 DeviceDecodeRequest::Region { roi },
887 )?;
888 let dims = plan.output_dims();
889 let stride = dims.0 as usize * fmt.bytes_per_pixel();
890 let mut out = vec![0u8; stride * dims.1 as usize];
891 self.inner.decode_region_into(
892 &mut self.pool,
893 &mut out,
894 stride,
895 fmt,
896 plan.source_rect(),
897 )?;
898 Ok(upload_surface_to_metal_with_device(
899 &out,
900 dims,
901 fmt,
902 session.device(),
903 ))
904 }
905 #[cfg(not(target_os = "macos"))]
906 {
907 let _ = (fmt, roi, session);
908 Err(Error::MetalUnavailable)
909 }
910 }
911
912 pub fn decode_scaled_to_cpu_staged_metal_surface_with_session(
914 &mut self,
915 fmt: PixelFormat,
916 scale: Downscale,
917 session: &MetalBackendSession,
918 ) -> Result<Surface, Error> {
919 #[cfg(target_os = "macos")]
920 {
921 let plan = DeviceDecodePlan::for_image(
922 self.inner.info().dimensions,
923 DeviceDecodeRequest::Scaled { scale },
924 )?;
925 let dims = plan.output_dims();
926 let stride = dims.0 as usize * fmt.bytes_per_pixel();
927 let mut out = vec![0u8; stride * dims.1 as usize];
928 self.inner
929 .decode_scaled_into(&mut self.pool, &mut out, stride, fmt, scale)?;
930 Ok(upload_surface_to_metal_with_device(
931 &out,
932 dims,
933 fmt,
934 session.device(),
935 ))
936 }
937 #[cfg(not(target_os = "macos"))]
938 {
939 let _ = (fmt, scale, session);
940 Err(Error::MetalUnavailable)
941 }
942 }
943
944 pub fn decode_region_scaled_to_cpu_staged_metal_surface_with_session(
946 &mut self,
947 fmt: PixelFormat,
948 roi: Rect,
949 scale: Downscale,
950 session: &MetalBackendSession,
951 ) -> Result<Surface, Error> {
952 #[cfg(target_os = "macos")]
953 {
954 let plan = DeviceDecodePlan::for_image(
955 self.inner.info().dimensions,
956 DeviceDecodeRequest::RegionScaled { roi, scale },
957 )?;
958 let dims = plan.output_dims();
959 let stride = dims.0 as usize * fmt.bytes_per_pixel();
960 let mut out = vec![0u8; stride * dims.1 as usize];
961 self.inner.decode_region_scaled_into(
962 &mut self.pool,
963 &mut out,
964 stride,
965 fmt,
966 roi,
967 scale,
968 )?;
969 Ok(upload_surface_to_metal_with_device(
970 &out,
971 dims,
972 fmt,
973 session.device(),
974 ))
975 }
976 #[cfg(not(target_os = "macos"))]
977 {
978 let _ = (fmt, roi, scale, session);
979 Err(Error::MetalUnavailable)
980 }
981 }
982
983 #[cfg(target_os = "macos")]
984 fn ensure_native_image(&mut self) -> Result<(), Error> {
985 if self.native_image.is_none() {
986 self.native_image = Some(
987 NativeImage::new(self.inner.bytes(), &NativeDecodeSettings::default())
988 .map_err(|error| J2kError::Backend(error.to_string()))?,
989 );
990 }
991 Ok(())
992 }
993
994 #[cfg(target_os = "macos")]
995 fn ensure_prepared_direct_gray_plan_with_session(
996 &mut self,
997 session: &MetalBackendSession,
998 ) -> Result<Option<Arc<crate::compute::PreparedDirectGrayscalePlan>>, Error> {
999 let cache_key = direct_gray_plan_cache_key(self.inner.bytes());
1000 if self.native_prepared_direct_gray_plan.is_none() {
1001 if let Some((plan, prepared)) = cached_session_direct_gray_plan(session, cache_key) {
1002 self.native_direct_gray_plan = Some(plan);
1003 self.native_prepared_direct_gray_plan = Some(prepared);
1004 }
1005 }
1006 if self.native_prepared_direct_gray_plan.is_none() {
1007 self.ensure_native_image()?;
1008 let (Some(image), native_context) =
1009 (self.native_image.as_ref(), &mut self.native_context)
1010 else {
1011 return Err(Error::Decode(J2kError::Backend(
1012 "native image cache missing".to_string(),
1013 )));
1014 };
1015
1016 let plan = match image.build_direct_grayscale_plan_with_context(native_context) {
1017 Ok(plan) => plan,
1018 Err(error) if direct::is_unsupported_direct_plan_error(&error.to_string()) => {
1019 return Ok(None);
1020 }
1021 Err(error) => {
1022 return Err(Error::Decode(J2kError::Backend(format!(
1023 "failed to build J2K MetalDirect grayscale plan: {error}"
1024 ))));
1025 }
1026 };
1027 let prepared = Arc::new(crate::compute::prepare_direct_grayscale_plan(&plan)?);
1028 store_session_direct_gray_plan(session, cache_key, &plan, prepared.clone());
1029 self.native_direct_gray_plan = Some(plan);
1030 self.native_prepared_direct_gray_plan = Some(prepared);
1031 }
1032
1033 Ok(self.native_prepared_direct_gray_plan.clone())
1034 }
1035
1036 #[cfg(target_os = "macos")]
1037 fn ensure_prepared_direct_color_plan_with_session(
1038 &mut self,
1039 session: &MetalBackendSession,
1040 ) -> Result<Option<Arc<crate::compute::PreparedDirectColorPlan>>, Error> {
1041 let cache_key = direct_plan_cache_key(self.inner.bytes());
1042 if self.native_prepared_direct_color_plan.is_none() {
1043 if let Some((plan, prepared)) = cached_session_direct_color_plan(session, cache_key) {
1044 self.native_direct_color_plan = Some(plan);
1045 self.native_prepared_direct_color_plan = Some(prepared);
1046 }
1047 }
1048 if self.native_prepared_direct_color_plan.is_none() {
1049 self.ensure_native_image()?;
1050 let (Some(image), native_context) =
1051 (self.native_image.as_ref(), &mut self.native_context)
1052 else {
1053 return Err(Error::Decode(J2kError::Backend(
1054 "native image cache missing".to_string(),
1055 )));
1056 };
1057
1058 let plan = match image.build_direct_color_plan_with_context(native_context) {
1059 Ok(plan) => plan,
1060 Err(error) if direct::is_unsupported_direct_plan_error(&error.to_string()) => {
1061 return Ok(None);
1062 }
1063 Err(error) => {
1064 return Err(Error::Decode(J2kError::Backend(format!(
1065 "failed to build J2K MetalDirect color plan: {error}"
1066 ))));
1067 }
1068 };
1069 let prepared = Arc::new(crate::compute::prepare_direct_color_plan(&plan)?);
1070 store_session_direct_color_plan(session, cache_key, &plan, prepared.clone());
1071 self.native_direct_color_plan = Some(plan);
1072 self.native_prepared_direct_color_plan = Some(prepared);
1073 }
1074
1075 Ok(self.native_prepared_direct_color_plan.clone())
1076 }
1077
1078 #[cfg(target_os = "macos")]
1079 fn ensure_prepared_direct_gray_plan(
1080 &mut self,
1081 ) -> Result<Option<Arc<crate::compute::PreparedDirectGrayscalePlan>>, Error> {
1082 if self.native_prepared_direct_gray_plan.is_none() {
1083 self.ensure_native_image()?;
1084 let (Some(image), native_context) =
1085 (self.native_image.as_ref(), &mut self.native_context)
1086 else {
1087 return Err(Error::Decode(J2kError::Backend(
1088 "native image cache missing".to_string(),
1089 )));
1090 };
1091 let plan = match image.build_direct_grayscale_plan_with_context(native_context) {
1092 Ok(plan) => plan,
1093 Err(error) if direct::is_unsupported_direct_plan_error(&error.to_string()) => {
1094 return Ok(None);
1095 }
1096 Err(error) => {
1097 return Err(Error::Decode(J2kError::Backend(format!(
1098 "failed to build J2K MetalDirect grayscale plan: {error}"
1099 ))));
1100 }
1101 };
1102 let prepared = Arc::new(crate::compute::prepare_direct_grayscale_plan(&plan)?);
1103 self.native_direct_gray_plan = Some(plan);
1104 self.native_prepared_direct_gray_plan = Some(prepared);
1105 }
1106 Ok(self.native_prepared_direct_gray_plan.clone())
1107 }
1108
1109 #[cfg(target_os = "macos")]
1110 fn ensure_prepared_direct_color_plan(
1111 &mut self,
1112 ) -> Result<Option<Arc<crate::compute::PreparedDirectColorPlan>>, Error> {
1113 if self.native_prepared_direct_color_plan.is_none() {
1114 self.ensure_native_image()?;
1115 let (Some(image), native_context) =
1116 (self.native_image.as_ref(), &mut self.native_context)
1117 else {
1118 return Err(Error::Decode(J2kError::Backend(
1119 "native image cache missing".to_string(),
1120 )));
1121 };
1122 let plan = match image.build_direct_color_plan_with_context(native_context) {
1123 Ok(plan) => plan,
1124 Err(error) if direct::is_unsupported_direct_plan_error(&error.to_string()) => {
1125 return Ok(None);
1126 }
1127 Err(error) => {
1128 return Err(Error::Decode(J2kError::Backend(format!(
1129 "failed to build J2K MetalDirect color plan: {error}"
1130 ))));
1131 }
1132 };
1133 let prepared = Arc::new(crate::compute::prepare_direct_color_plan(&plan)?);
1134 self.native_direct_color_plan = Some(plan);
1135 self.native_prepared_direct_color_plan = Some(prepared);
1136 }
1137 Ok(self.native_prepared_direct_color_plan.clone())
1138 }
1139
1140 #[cfg(target_os = "macos")]
1141 fn decode_direct_to_surface(&mut self, fmt: PixelFormat) -> Result<Option<Surface>, Error> {
1142 if matches!(fmt, PixelFormat::Gray8 | PixelFormat::Gray16) {
1143 let Some(plan) = self.ensure_prepared_direct_gray_plan()? else {
1144 return Ok(None);
1145 };
1146 return Ok(Some(
1147 crate::compute::execute_prepared_direct_grayscale_plan(&plan, fmt)?,
1148 ));
1149 }
1150
1151 if matches!(
1152 fmt,
1153 PixelFormat::Rgb8 | PixelFormat::Rgba8 | PixelFormat::Rgb16
1154 ) {
1155 let Some(plan) = self.ensure_prepared_direct_color_plan()? else {
1156 return Ok(None);
1157 };
1158 return match crate::compute::execute_prepared_direct_color_plan(&plan, fmt) {
1159 Ok(surface) => Ok(Some(surface)),
1160 Err(error) if is_direct_color_runtime_fallback_error(&error) => Ok(None),
1161 Err(error) => Err(error),
1162 };
1163 }
1164
1165 Ok(None)
1166 }
1167
1168 #[cfg(target_os = "macos")]
1169 fn decode_direct_to_surface_with_session(
1170 &mut self,
1171 fmt: PixelFormat,
1172 session: &MetalBackendSession,
1173 ) -> Result<Option<Surface>, Error> {
1174 if matches!(fmt, PixelFormat::Gray8 | PixelFormat::Gray16) {
1175 let Some(plan) = self.ensure_prepared_direct_gray_plan_with_session(session)? else {
1176 return Ok(None);
1177 };
1178 return Ok(Some(
1179 crate::compute::execute_prepared_direct_grayscale_plan_with_device(
1180 &plan,
1181 fmt,
1182 &session.device,
1183 )?,
1184 ));
1185 }
1186
1187 if matches!(
1188 fmt,
1189 PixelFormat::Rgb8 | PixelFormat::Rgba8 | PixelFormat::Rgb16
1190 ) {
1191 let Some(plan) = self.ensure_prepared_direct_color_plan_with_session(session)? else {
1192 return Ok(None);
1193 };
1194 return match crate::compute::execute_prepared_direct_color_plan_with_device(
1195 &plan,
1196 fmt,
1197 &session.device,
1198 ) {
1199 Ok(surface) => Ok(Some(surface)),
1200 Err(error) if is_direct_color_runtime_fallback_error(&error) => Ok(None),
1201 Err(error) => Err(error),
1202 };
1203 }
1204
1205 Ok(None)
1206 }
1207
1208 #[cfg(target_os = "macos")]
1209 fn decode_region_scaled_direct_to_surface(
1210 &mut self,
1211 fmt: PixelFormat,
1212 roi: Rect,
1213 scale: Downscale,
1214 ) -> Result<Option<Surface>, Error> {
1215 crate::hybrid::decode_region_scaled_direct_to_surface(self.inner.bytes(), fmt, roi, scale)
1216 }
1217
1218 #[cfg(target_os = "macos")]
1219 fn decode_region_scaled_direct_to_surface_with_session(
1220 &mut self,
1221 fmt: PixelFormat,
1222 roi: Rect,
1223 scale: Downscale,
1224 session: &MetalBackendSession,
1225 ) -> Result<Option<Surface>, Error> {
1226 crate::hybrid::decode_region_scaled_direct_to_surface_with_session(
1227 self.inner.bytes(),
1228 fmt,
1229 roi,
1230 scale,
1231 session,
1232 )
1233 }
1234 #[cfg(target_os = "macos")]
1235 fn decode_full_to_metal_surface(&mut self, fmt: PixelFormat) -> Result<Surface, Error> {
1236 self.ensure_native_image()?;
1237 let (Some(image), native_context) = (self.native_image.as_ref(), &mut self.native_context)
1238 else {
1239 return Err(Error::Decode(J2kError::Backend(
1240 "native image cache missing".to_string(),
1241 )));
1242 };
1243 crate::compute::decode_image_to_surface(image, native_context, fmt)
1244 }
1245
1246 #[cfg(target_os = "macos")]
1247 fn decode_full_to_metal_surface_with_device(
1248 &mut self,
1249 fmt: PixelFormat,
1250 device: &Device,
1251 ) -> Result<Surface, Error> {
1252 self.ensure_native_image()?;
1253 let (Some(image), native_context) = (self.native_image.as_ref(), &mut self.native_context)
1254 else {
1255 return Err(Error::Decode(J2kError::Backend(
1256 "native image cache missing".to_string(),
1257 )));
1258 };
1259 crate::compute::decode_image_to_surface_with_device(image, native_context, fmt, device)
1260 }
1261
1262 #[cfg(target_os = "macos")]
1263 fn decode_repeated_grayscale_cpu_to_surfaces(
1264 &mut self,
1265 fmt: PixelFormat,
1266 count: usize,
1267 ) -> Result<Vec<Surface>, Error> {
1268 let mut surfaces = Vec::with_capacity(count);
1269 for _ in 0..count {
1270 surfaces.push(self.decode_to_cpu_surface(fmt)?);
1271 }
1272 Ok(surfaces)
1273 }
1274
1275 #[cfg(target_os = "macos")]
1276 fn should_auto_use_direct_for_repeated(
1277 plan: &J2kDirectGrayscalePlan,
1278 fmt: PixelFormat,
1279 count: usize,
1280 ) -> bool {
1281 if !matches!(fmt, PixelFormat::Gray8 | PixelFormat::Gray16) || count == 0 {
1282 return false;
1283 }
1284
1285 let max_dim = plan.dimensions.0.max(plan.dimensions.1);
1286 max_dim >= AUTO_REPEATED_GRAYSCALE_MIN_DIM && count >= AUTO_REPEATED_GRAYSCALE_MIN_COUNT
1287 }
1288
1289 #[cfg(target_os = "macos")]
1290 #[doc(hidden)]
1291 pub fn decode_repeated_grayscale_direct_to_device(
1292 &mut self,
1293 fmt: PixelFormat,
1294 count: usize,
1295 ) -> Result<Vec<Surface>, Error> {
1296 if count == 0 {
1297 return Ok(Vec::new());
1298 }
1299 if self.native_direct_gray_plan.is_none() {
1300 self.ensure_native_image()?;
1301 let (Some(image), native_context) =
1302 (self.native_image.as_ref(), &mut self.native_context)
1303 else {
1304 return Err(Error::Decode(J2kError::Backend(
1305 "native image cache missing".to_string(),
1306 )));
1307 };
1308 let plan = image
1309 .build_direct_grayscale_plan_with_context(native_context)
1310 .map_err(|error| J2kError::Backend(error.to_string()))?;
1311 let prepared = Arc::new(crate::compute::prepare_direct_grayscale_plan(&plan)?);
1312 self.native_direct_gray_plan = Some(plan);
1313 self.native_prepared_direct_gray_plan = Some(prepared);
1314 }
1315 let Some(plan) = self.native_prepared_direct_gray_plan.as_ref() else {
1316 return Ok(Vec::new());
1317 };
1318 crate::compute::execute_repeated_prepared_direct_grayscale_plan(plan, fmt, count)
1319 }
1320
1321 #[cfg(target_os = "macos")]
1322 #[doc(hidden)]
1323 pub fn decode_repeated_color_direct_to_device(
1324 &mut self,
1325 fmt: PixelFormat,
1326 count: usize,
1327 ) -> Result<Vec<Surface>, Error> {
1328 if count == 0 {
1329 return Ok(Vec::new());
1330 }
1331 let surface = self.decode_to_surface_impl(fmt, BackendRequest::Metal)?;
1332 Ok(vec![surface; count])
1333 }
1334
1335 #[cfg(target_os = "macos")]
1336 #[doc(hidden)]
1337 pub fn decode_repeated_grayscale_auto_to_device(
1338 &mut self,
1339 fmt: PixelFormat,
1340 count: usize,
1341 ) -> Result<Vec<Surface>, Error> {
1342 if count == 0 {
1343 return Ok(Vec::new());
1344 }
1345 if !matches!(fmt, PixelFormat::Gray8 | PixelFormat::Gray16) {
1346 return self.decode_repeated_grayscale_cpu_to_surfaces(fmt, count);
1347 }
1348 let dims = self.inner.info().dimensions;
1349 if dims.0.max(dims.1) < AUTO_REPEATED_GRAYSCALE_MIN_DIM
1350 || count < AUTO_REPEATED_GRAYSCALE_MIN_COUNT
1351 {
1352 return self.decode_repeated_grayscale_cpu_to_surfaces(fmt, count);
1353 }
1354 if self.native_direct_gray_plan.is_none() {
1355 self.ensure_native_image()?;
1356 let (Some(image), native_context) =
1357 (self.native_image.as_ref(), &mut self.native_context)
1358 else {
1359 return Err(Error::Decode(J2kError::Backend(
1360 "native image cache missing".to_string(),
1361 )));
1362 };
1363 let Ok(plan) = image.build_direct_grayscale_plan_with_context(native_context) else {
1364 return self.decode_repeated_grayscale_cpu_to_surfaces(fmt, count);
1365 };
1366 let prepared = Arc::new(crate::compute::prepare_direct_grayscale_plan(&plan)?);
1367 self.native_direct_gray_plan = Some(plan);
1368 self.native_prepared_direct_gray_plan = Some(prepared);
1369 }
1370 let Some(plan) = self.native_direct_gray_plan.as_ref() else {
1371 return self.decode_repeated_grayscale_cpu_to_surfaces(fmt, count);
1372 };
1373 if Self::should_auto_use_direct_for_repeated(plan, fmt, count) {
1374 let Some(prepared) = self.native_prepared_direct_gray_plan.as_ref() else {
1375 return self.decode_repeated_grayscale_cpu_to_surfaces(fmt, count);
1376 };
1377 crate::compute::execute_repeated_prepared_direct_grayscale_plan(prepared, fmt, count)
1378 } else {
1379 self.decode_repeated_grayscale_cpu_to_surfaces(fmt, count)
1380 }
1381 }
1382
1383 fn decode_to_cpu_surface(&mut self, fmt: PixelFormat) -> Result<Surface, Error> {
1384 let dims = self.inner.info().dimensions;
1385 let stride = dims.0 as usize * fmt.bytes_per_pixel();
1386 let mut out = vec![0u8; stride * dims.1 as usize];
1387 self.inner
1388 .decode_into_with_scratch(&mut self.pool, &mut out, stride, fmt)?;
1389 upload_surface(out, dims, fmt, BackendRequest::Cpu)
1390 }
1391
1392 fn decode_region_to_cpu_surface(
1393 &mut self,
1394 fmt: PixelFormat,
1395 plan: DeviceDecodePlan,
1396 ) -> Result<Surface, Error> {
1397 let dims = plan.output_dims();
1398 let stride = dims.0 as usize * fmt.bytes_per_pixel();
1399 let mut out = vec![0u8; stride * dims.1 as usize];
1400 self.inner
1401 .decode_region_into(&mut self.pool, &mut out, stride, fmt, plan.source_rect())?;
1402 upload_surface(out, dims, fmt, BackendRequest::Cpu)
1403 }
1404
1405 fn decode_scaled_to_cpu_surface(
1406 &mut self,
1407 fmt: PixelFormat,
1408 scale: Downscale,
1409 plan: DeviceDecodePlan,
1410 ) -> Result<Surface, Error> {
1411 let dims = plan.output_dims();
1412 let stride = dims.0 as usize * fmt.bytes_per_pixel();
1413 let mut out = vec![0u8; stride * dims.1 as usize];
1414 self.inner
1415 .decode_scaled_into(&mut self.pool, &mut out, stride, fmt, scale)?;
1416 upload_surface(out, dims, fmt, BackendRequest::Cpu)
1417 }
1418
1419 fn decode_region_scaled_to_cpu_surface(
1420 &mut self,
1421 fmt: PixelFormat,
1422 roi: Rect,
1423 scale: Downscale,
1424 plan: DeviceDecodePlan,
1425 ) -> Result<Surface, Error> {
1426 let dims = plan.output_dims();
1427 let stride = dims.0 as usize * fmt.bytes_per_pixel();
1428 let mut out = vec![0u8; stride * dims.1 as usize];
1429 self.inner
1430 .decode_region_scaled_into(&mut self.pool, &mut out, stride, fmt, roi, scale)?;
1431 upload_surface(out, dims, fmt, BackendRequest::Cpu)
1432 }
1433
1434 #[cfg(target_os = "macos")]
1435 fn decode_region_to_metal_surface(
1436 &mut self,
1437 fmt: PixelFormat,
1438 plan: DeviceDecodePlan,
1439 ) -> Result<Surface, Error> {
1440 self.ensure_native_image()?;
1441 let (Some(image), native_context) = (self.native_image.as_ref(), &mut self.native_context)
1442 else {
1443 return Err(Error::Decode(J2kError::Backend(
1444 "native image cache missing".to_string(),
1445 )));
1446 };
1447 crate::compute::decode_image_region_to_surface(
1448 image,
1449 native_context,
1450 fmt,
1451 plan.source_rect(),
1452 )
1453 }
1454
1455 #[cfg(target_os = "macos")]
1456 fn decode_scaled_to_metal_surface(
1457 &mut self,
1458 fmt: PixelFormat,
1459 scale: Downscale,
1460 plan: DeviceDecodePlan,
1461 ) -> Result<Surface, Error> {
1462 crate::compute::decode_scaled_to_surface(self.inner.bytes(), plan.source_dims(), fmt, scale)
1463 }
1464
1465 #[cfg(target_os = "macos")]
1466 fn decode_region_scaled_to_metal_surface(
1467 &mut self,
1468 fmt: PixelFormat,
1469 roi: Rect,
1470 scale: Downscale,
1471 plan: DeviceDecodePlan,
1472 ) -> Result<Surface, Error> {
1473 if let Some(surface) = self.decode_region_scaled_direct_to_surface(fmt, roi, scale)? {
1474 return Ok(surface);
1475 }
1476 crate::compute::decode_region_scaled_to_surface(
1477 self.inner.bytes(),
1478 plan.source_dims(),
1479 fmt,
1480 roi,
1481 scale,
1482 )
1483 }
1484
1485 #[cfg(target_os = "macos")]
1486 fn decode_region_to_metal_surface_with_device(
1487 &mut self,
1488 fmt: PixelFormat,
1489 plan: DeviceDecodePlan,
1490 device: &Device,
1491 ) -> Result<Surface, Error> {
1492 self.ensure_native_image()?;
1493 let (Some(image), native_context) = (self.native_image.as_ref(), &mut self.native_context)
1494 else {
1495 return Err(Error::Decode(J2kError::Backend(
1496 "native image cache missing".to_string(),
1497 )));
1498 };
1499 crate::compute::decode_image_region_to_surface_with_device(
1500 image,
1501 native_context,
1502 fmt,
1503 plan.source_rect(),
1504 device,
1505 )
1506 }
1507
1508 #[cfg(target_os = "macos")]
1509 fn decode_scaled_to_metal_surface_with_device(
1510 &mut self,
1511 fmt: PixelFormat,
1512 scale: Downscale,
1513 plan: DeviceDecodePlan,
1514 device: &Device,
1515 ) -> Result<Surface, Error> {
1516 crate::compute::decode_scaled_to_surface_with_device(
1517 self.inner.bytes(),
1518 plan.source_dims(),
1519 fmt,
1520 scale,
1521 device,
1522 )
1523 }
1524 #[cfg(target_os = "macos")]
1525 fn decode_region_scaled_to_metal_surface_with_session(
1526 &mut self,
1527 fmt: PixelFormat,
1528 roi: Rect,
1529 scale: Downscale,
1530 plan: DeviceDecodePlan,
1531 session: &MetalBackendSession,
1532 ) -> Result<Surface, Error> {
1533 if let Some(surface) =
1534 self.decode_region_scaled_direct_to_surface_with_session(fmt, roi, scale, session)?
1535 {
1536 return Ok(surface);
1537 }
1538 crate::compute::with_runtime_for_session(session, |_| {
1539 crate::compute::decode_region_scaled_to_surface_with_device(
1540 self.inner.bytes(),
1541 plan.source_dims(),
1542 fmt,
1543 roi,
1544 scale,
1545 &session.device,
1546 )
1547 })
1548 }
1549
1550 pub(crate) fn decode_to_surface_impl(
1551 &mut self,
1552 fmt: PixelFormat,
1553 backend: BackendRequest,
1554 ) -> Result<Surface, Error> {
1555 let route = routing::decide_route(backend, fmt);
1556 if let Some(error) = routing::decision_error(route) {
1557 return Err(error);
1558 }
1559
1560 match route {
1561 routing::RouteDecision::CpuHost => self.decode_to_cpu_surface(fmt),
1562 #[cfg(target_os = "macos")]
1563 routing::RouteDecision::MetalKernel => {
1564 if let Some(surface) = self.decode_direct_to_surface(fmt)? {
1565 Ok(surface)
1566 } else {
1567 self.decode_full_to_metal_surface(fmt)
1568 }
1569 }
1570 routing::RouteDecision::RejectExplicitMetal { .. }
1571 | routing::RouteDecision::RejectUnsupportedBackend { .. } => {
1572 unreachable!("handled by decision_error")
1573 }
1574 #[cfg(not(target_os = "macos"))]
1575 routing::RouteDecision::MetalUnavailable => unreachable!("handled by decision_error"),
1576 }
1577 }
1578
1579 pub(crate) fn decode_region_to_surface_impl(
1580 &mut self,
1581 fmt: PixelFormat,
1582 roi: Rect,
1583 backend: BackendRequest,
1584 ) -> Result<Surface, Error> {
1585 let route = routing::decide_route(backend, fmt);
1586 if let Some(error) = routing::decision_error(route) {
1587 return Err(error);
1588 }
1589
1590 let plan = DeviceDecodePlan::for_image(
1591 self.inner.info().dimensions,
1592 DeviceDecodeRequest::Region { roi },
1593 )?;
1594 match route {
1595 routing::RouteDecision::CpuHost => self.decode_region_to_cpu_surface(fmt, plan),
1596 #[cfg(target_os = "macos")]
1597 routing::RouteDecision::MetalKernel => self.decode_region_to_metal_surface(fmt, plan),
1598 routing::RouteDecision::RejectExplicitMetal { .. }
1599 | routing::RouteDecision::RejectUnsupportedBackend { .. } => {
1600 unreachable!("handled by decision_error")
1601 }
1602 #[cfg(not(target_os = "macos"))]
1603 routing::RouteDecision::MetalUnavailable => unreachable!("handled by decision_error"),
1604 }
1605 }
1606
1607 pub fn decode_region_to_device_with_session(
1609 &mut self,
1610 fmt: PixelFormat,
1611 roi: Rect,
1612 session: &MetalBackendSession,
1613 ) -> Result<Surface, Error> {
1614 if let Some(error) =
1615 routing::decision_error(routing::decide_route(BackendRequest::Metal, fmt))
1616 {
1617 return Err(error);
1618 }
1619
1620 #[cfg(target_os = "macos")]
1621 {
1622 let plan = DeviceDecodePlan::for_image(
1623 self.inner.info().dimensions,
1624 DeviceDecodeRequest::Region { roi },
1625 )?;
1626 crate::compute::with_runtime_for_session(session, |_| {
1627 self.decode_region_to_metal_surface_with_device(fmt, plan, &session.device)
1628 })
1629 }
1630 #[cfg(not(target_os = "macos"))]
1631 {
1632 let _ = (roi, session);
1633 Err(Error::MetalUnavailable)
1634 }
1635 }
1636
1637 pub(crate) fn decode_scaled_to_surface_impl(
1638 &mut self,
1639 fmt: PixelFormat,
1640 scale: Downscale,
1641 backend: BackendRequest,
1642 ) -> Result<Surface, Error> {
1643 let route = routing::decide_route(backend, fmt);
1644 if let Some(error) = routing::decision_error(route) {
1645 return Err(error);
1646 }
1647
1648 let plan = DeviceDecodePlan::for_image(
1649 self.inner.info().dimensions,
1650 DeviceDecodeRequest::Scaled { scale },
1651 )?;
1652 match route {
1653 routing::RouteDecision::CpuHost => self.decode_scaled_to_cpu_surface(fmt, scale, plan),
1654 #[cfg(target_os = "macos")]
1655 routing::RouteDecision::MetalKernel => {
1656 self.decode_scaled_to_metal_surface(fmt, scale, plan)
1657 }
1658 routing::RouteDecision::RejectExplicitMetal { .. }
1659 | routing::RouteDecision::RejectUnsupportedBackend { .. } => {
1660 unreachable!("handled by decision_error")
1661 }
1662 #[cfg(not(target_os = "macos"))]
1663 routing::RouteDecision::MetalUnavailable => unreachable!("handled by decision_error"),
1664 }
1665 }
1666
1667 pub(crate) fn decode_region_scaled_to_surface_impl(
1668 &mut self,
1669 fmt: PixelFormat,
1670 roi: Rect,
1671 scale: Downscale,
1672 backend: BackendRequest,
1673 ) -> Result<Surface, Error> {
1674 let route = routing::decide_route(backend, fmt);
1675 if let Some(error) = routing::decision_error(route) {
1676 return Err(error);
1677 }
1678 let plan = DeviceDecodePlan::for_image(
1679 self.inner.info().dimensions,
1680 DeviceDecodeRequest::RegionScaled { roi, scale },
1681 )?;
1682 match route {
1683 routing::RouteDecision::CpuHost => {
1684 self.decode_region_scaled_to_cpu_surface(fmt, roi, scale, plan)
1685 }
1686 #[cfg(target_os = "macos")]
1687 routing::RouteDecision::MetalKernel => {
1688 self.decode_region_scaled_to_metal_surface(fmt, roi, scale, plan)
1689 }
1690 routing::RouteDecision::RejectExplicitMetal { .. }
1691 | routing::RouteDecision::RejectUnsupportedBackend { .. } => {
1692 unreachable!("handled by decision_error")
1693 }
1694 #[cfg(not(target_os = "macos"))]
1695 routing::RouteDecision::MetalUnavailable => unreachable!("handled by decision_error"),
1696 }
1697 }
1698
1699 pub fn decode_scaled_to_device_with_session(
1701 &mut self,
1702 fmt: PixelFormat,
1703 scale: Downscale,
1704 session: &MetalBackendSession,
1705 ) -> Result<Surface, Error> {
1706 if let Some(error) =
1707 routing::decision_error(routing::decide_route(BackendRequest::Metal, fmt))
1708 {
1709 return Err(error);
1710 }
1711 if !matches!(fmt, PixelFormat::Gray8 | PixelFormat::Gray16) {
1712 return Err(Error::UnsupportedMetalRequest {
1713 reason: "J2K Metal session scaled decode currently supports Gray8/Gray16 only",
1714 });
1715 }
1716
1717 #[cfg(target_os = "macos")]
1718 {
1719 let plan = DeviceDecodePlan::for_image(
1720 self.inner.info().dimensions,
1721 DeviceDecodeRequest::Scaled { scale },
1722 )?;
1723 crate::compute::with_runtime_for_session(session, |_| {
1724 self.decode_scaled_to_metal_surface_with_device(fmt, scale, plan, &session.device)
1725 })
1726 }
1727 #[cfg(not(target_os = "macos"))]
1728 {
1729 let _ = (scale, session);
1730 Err(Error::MetalUnavailable)
1731 }
1732 }
1733
1734 pub fn decode_region_scaled_to_device_with_session(
1736 &mut self,
1737 fmt: PixelFormat,
1738 roi: Rect,
1739 scale: Downscale,
1740 session: &MetalBackendSession,
1741 ) -> Result<Surface, Error> {
1742 if let Some(error) =
1743 routing::decision_error(routing::decide_route(BackendRequest::Metal, fmt))
1744 {
1745 return Err(error);
1746 }
1747
1748 #[cfg(target_os = "macos")]
1749 {
1750 let plan = DeviceDecodePlan::for_image(
1751 self.inner.info().dimensions,
1752 DeviceDecodeRequest::RegionScaled { roi, scale },
1753 )?;
1754 self.decode_region_scaled_to_metal_surface_with_session(fmt, roi, scale, plan, session)
1755 }
1756 #[cfg(not(target_os = "macos"))]
1757 {
1758 let _ = (roi, scale, session);
1759 Err(Error::MetalUnavailable)
1760 }
1761 }
1762}
1763
1764#[cfg(target_os = "macos")]
1765fn direct_plan_cache_key(bytes: &[u8]) -> u64 {
1766 let mut hasher = DefaultHasher::new();
1767 bytes.hash(&mut hasher);
1768 hasher.finish()
1769}
1770
1771#[cfg(target_os = "macos")]
1772fn direct_gray_plan_cache_key(bytes: &[u8]) -> u64 {
1773 direct_plan_cache_key(bytes)
1774}
1775
1776#[cfg(target_os = "macos")]
1777fn cached_session_direct_gray_plan(
1778 session: &MetalBackendSession,
1779 key: u64,
1780) -> Option<(
1781 J2kDirectGrayscalePlan,
1782 Arc<crate::compute::PreparedDirectGrayscalePlan>,
1783)> {
1784 let guard = session.direct_gray_plan_cache.lock().ok()?;
1785 guard
1786 .get(&key)
1787 .map(|entry| (entry.plan.clone(), entry.prepared.clone()))
1788}
1789
1790#[cfg(target_os = "macos")]
1791fn store_session_direct_gray_plan(
1792 session: &MetalBackendSession,
1793 key: u64,
1794 plan: &J2kDirectGrayscalePlan,
1795 prepared: Arc<crate::compute::PreparedDirectGrayscalePlan>,
1796) {
1797 if let Ok(mut guard) = session.direct_gray_plan_cache.lock() {
1798 evict_one_direct_plan_if_needed(&mut guard);
1799 guard.insert(
1800 key,
1801 DirectGrayPlanCacheEntry {
1802 plan: plan.clone(),
1803 prepared,
1804 },
1805 );
1806 }
1807}
1808
1809#[cfg(target_os = "macos")]
1810fn cached_session_direct_color_plan(
1811 session: &MetalBackendSession,
1812 key: u64,
1813) -> Option<(
1814 J2kDirectColorPlan,
1815 Arc<crate::compute::PreparedDirectColorPlan>,
1816)> {
1817 let guard = session.direct_color_plan_cache.lock().ok()?;
1818 guard
1819 .get(&key)
1820 .map(|entry| (entry.plan.clone(), entry.prepared.clone()))
1821}
1822
1823#[cfg(target_os = "macos")]
1824fn store_session_direct_color_plan(
1825 session: &MetalBackendSession,
1826 key: u64,
1827 plan: &J2kDirectColorPlan,
1828 prepared: Arc<crate::compute::PreparedDirectColorPlan>,
1829) {
1830 if let Ok(mut guard) = session.direct_color_plan_cache.lock() {
1831 evict_one_direct_plan_if_needed(&mut guard);
1832 guard.insert(
1833 key,
1834 DirectColorPlanCacheEntry {
1835 plan: plan.clone(),
1836 prepared,
1837 },
1838 );
1839 }
1840}
1841
1842#[cfg(target_os = "macos")]
1843fn evict_one_direct_plan_if_needed<T>(cache: &mut HashMap<u64, T>) {
1844 if cache.len() < DIRECT_PLAN_CACHE_CAP {
1845 return;
1846 }
1847 if let Some(key) = cache.keys().next().copied() {
1848 cache.remove(&key);
1849 }
1850}
1851
1852#[cfg(target_os = "macos")]
1853fn is_direct_color_runtime_fallback_error(error: &Error) -> bool {
1854 is_direct_runtime_fallback_error(error)
1855}
1856
1857#[cfg(target_os = "macos")]
1858fn is_direct_runtime_fallback_error(error: &Error) -> bool {
1859 matches!(
1860 error,
1861 Error::MetalKernel { message }
1862 if message.contains("unsupported classic kernel input")
1863 || message.contains("unsupported HT kernel input")
1864 || message.contains("direct component plan")
1865 || message.contains("currently supports grayscale direct plans only")
1866 || message.contains("currently supports color direct plans only")
1867 )
1868}
1869
1870#[cfg(target_os = "macos")]
1871pub(crate) fn decode_full_grayscale_batch_direct_to_device(
1872 inputs: &[Arc<[u8]>],
1873 fmt: PixelFormat,
1874) -> Result<Vec<Surface>, Error> {
1875 if inputs.is_empty() {
1876 return Ok(Vec::new());
1877 }
1878 if !matches!(fmt, PixelFormat::Gray8 | PixelFormat::Gray16) {
1879 return Err(Error::MetalKernel {
1880 message: format!("J2K MetalDirect full grayscale batch does not support {fmt:?}"),
1881 });
1882 }
1883
1884 let mut plans = Vec::with_capacity(inputs.len());
1885 for input in inputs {
1886 let mut decoder = J2kDecoder::new(input.as_ref())?;
1887 let Some(plan) = decoder.ensure_prepared_direct_gray_plan()? else {
1888 return Err(Error::MetalKernel {
1889 message: format!(
1890 "explicit J2K MetalDirect batch currently supports full grayscale Gray8/Gray16 only; fmt={fmt:?}"
1891 ),
1892 });
1893 };
1894 plans.push(plan);
1895 }
1896 crate::compute::execute_prepared_direct_grayscale_plan_batch(&plans, fmt)
1897}
1898
1899#[cfg(target_os = "macos")]
1900pub(crate) fn decode_full_color_batch_direct_to_device(
1901 inputs: &[Arc<[u8]>],
1902 fmt: PixelFormat,
1903) -> Result<Vec<Surface>, Error> {
1904 if inputs.is_empty() {
1905 return Ok(Vec::new());
1906 }
1907 if !matches!(
1908 fmt,
1909 PixelFormat::Rgb8 | PixelFormat::Rgba8 | PixelFormat::Rgb16
1910 ) {
1911 return Err(Error::MetalKernel {
1912 message: format!("J2K MetalDirect full color batch does not support {fmt:?}"),
1913 });
1914 }
1915
1916 let mut plans = Vec::with_capacity(inputs.len());
1917 for input in inputs {
1918 let mut decoder = J2kDecoder::new(input.as_ref())?;
1919 let Some(plan) = decoder.ensure_prepared_direct_color_plan()? else {
1920 return Err(Error::MetalKernel {
1921 message: format!(
1922 "explicit J2K MetalDirect batch currently supports full RGB color only; fmt={fmt:?}"
1923 ),
1924 });
1925 };
1926 plans.push(plan);
1927 }
1928 match crate::compute::execute_prepared_direct_color_plan_batch(&plans, fmt) {
1929 Ok(surfaces) => Ok(surfaces),
1930 Err(error) if is_direct_color_runtime_fallback_error(&error) => {
1931 Err(Error::UnsupportedMetalRequest {
1932 reason: CPU_STAGED_METAL_REQUIRES_EXPLICIT_API,
1933 })
1934 }
1935 Err(error) => Err(error),
1936 }
1937}
1938
1939impl ImageCodec for J2kDecoder<'_> {
1940 type Error = Error;
1941 type Warning = Infallible;
1942 type Pool = CpuJ2kScratchPool;
1943}
1944
1945impl<'a> ImageDecode<'a> for J2kDecoder<'a> {
1946 type View = J2kView<'a>;
1947
1948 fn inspect(input: &'a [u8]) -> Result<j2k_core::Info, Self::Error> {
1949 Ok(CpuDecoder::inspect(input)?)
1950 }
1951
1952 fn parse(input: &'a [u8]) -> Result<Self::View, Self::Error> {
1953 Ok(J2kView::parse(input)?)
1954 }
1955
1956 fn from_view(view: Self::View) -> Result<Self, Self::Error> {
1957 Self::from_view(view)
1958 }
1959
1960 fn decode_into(
1961 &mut self,
1962 out: &mut [u8],
1963 stride: usize,
1964 fmt: PixelFormat,
1965 ) -> Result<DecodeOutcome<Self::Warning>, Self::Error> {
1966 Ok(self.inner.decode_into(out, stride, fmt)?)
1967 }
1968
1969 fn decode_into_with_scratch(
1970 &mut self,
1971 pool: &mut Self::Pool,
1972 out: &mut [u8],
1973 stride: usize,
1974 fmt: PixelFormat,
1975 ) -> Result<DecodeOutcome<Self::Warning>, Self::Error> {
1976 Ok(self
1977 .inner
1978 .decode_into_with_scratch(pool, out, stride, fmt)?)
1979 }
1980
1981 fn decode_region_into(
1982 &mut self,
1983 pool: &mut Self::Pool,
1984 out: &mut [u8],
1985 stride: usize,
1986 fmt: PixelFormat,
1987 roi: Rect,
1988 ) -> Result<DecodeOutcome<Self::Warning>, Self::Error> {
1989 Ok(self.inner.decode_region_into(pool, out, stride, fmt, roi)?)
1990 }
1991
1992 fn decode_scaled_into(
1993 &mut self,
1994 pool: &mut Self::Pool,
1995 out: &mut [u8],
1996 stride: usize,
1997 fmt: PixelFormat,
1998 scale: Downscale,
1999 ) -> Result<DecodeOutcome<Self::Warning>, Self::Error> {
2000 Ok(self
2001 .inner
2002 .decode_scaled_into(pool, out, stride, fmt, scale)?)
2003 }
2004
2005 fn decode_region_scaled_into(
2006 &mut self,
2007 pool: &mut Self::Pool,
2008 out: &mut [u8],
2009 stride: usize,
2010 fmt: PixelFormat,
2011 roi: Rect,
2012 scale: Downscale,
2013 ) -> Result<DecodeOutcome<Self::Warning>, Self::Error> {
2014 Ok(self
2015 .inner
2016 .decode_region_scaled_into(pool, out, stride, fmt, roi, scale)?)
2017 }
2018}
2019
2020impl<'a> ImageDecodeDevice<'a> for J2kDecoder<'a> {
2021 type DeviceSurface = Surface;
2022}
2023
2024#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2025pub struct Codec;
2027
2028impl ImageCodec for Codec {
2029 type Error = Error;
2030 type Warning = Infallible;
2031 type Pool = CpuJ2kScratchPool;
2032}
2033
2034impl<'a> ImageDecodeSubmit<'a> for J2kDecoder<'a> {
2035 type Session = MetalSession;
2036 type DeviceSurface = Surface;
2037 type SubmittedSurface = ReadySubmission<Surface, Error>;
2038
2039 fn submit_to_device(
2040 &mut self,
2041 session: &mut Self::Session,
2042 fmt: PixelFormat,
2043 backend: BackendRequest,
2044 ) -> Result<Self::SubmittedSurface, Self::Error> {
2045 session.record_submit()?;
2046 Ok(ReadySubmission::from_result(
2047 self.decode_to_surface_impl(fmt, backend),
2048 ))
2049 }
2050
2051 fn submit_region_to_device(
2052 &mut self,
2053 session: &mut Self::Session,
2054 fmt: PixelFormat,
2055 roi: Rect,
2056 backend: BackendRequest,
2057 ) -> Result<Self::SubmittedSurface, Self::Error> {
2058 session.record_submit()?;
2059 Ok(ReadySubmission::from_result(
2060 self.decode_region_to_surface_impl(fmt, roi, backend),
2061 ))
2062 }
2063
2064 fn submit_scaled_to_device(
2065 &mut self,
2066 session: &mut Self::Session,
2067 fmt: PixelFormat,
2068 scale: Downscale,
2069 backend: BackendRequest,
2070 ) -> Result<Self::SubmittedSurface, Self::Error> {
2071 session.record_submit()?;
2072 Ok(ReadySubmission::from_result(
2073 self.decode_scaled_to_surface_impl(fmt, scale, backend),
2074 ))
2075 }
2076
2077 fn submit_region_scaled_to_device(
2078 &mut self,
2079 session: &mut Self::Session,
2080 fmt: PixelFormat,
2081 roi: Rect,
2082 scale: Downscale,
2083 backend: BackendRequest,
2084 ) -> Result<Self::SubmittedSurface, Self::Error> {
2085 session.record_submit()?;
2086 Ok(ReadySubmission::from_result(
2087 self.decode_region_scaled_to_surface_impl(fmt, roi, scale, backend),
2088 ))
2089 }
2090}
2091
2092impl TileBatchDecodeSubmit for Codec {
2093 type Context = CpuJ2kContext;
2094 type Session = MetalSession;
2095 type DeviceSurface = Surface;
2096 type SubmittedSurface = batch::MetalSubmission;
2097
2098 fn submit_tile_to_device(
2099 ctx: &mut j2k_core::DecoderContext<Self::Context>,
2100 session: &mut Self::Session,
2101 pool: &mut Self::Pool,
2102 input: &[u8],
2103 fmt: PixelFormat,
2104 backend: BackendRequest,
2105 ) -> Result<Self::SubmittedSurface, Self::Error> {
2106 let _ = (ctx, pool);
2107 batch::queue_tile_request(session, input, fmt, backend, batch::BatchOp::Full)
2108 }
2109
2110 fn submit_tile_region_to_device(
2111 ctx: &mut j2k_core::DecoderContext<Self::Context>,
2112 session: &mut Self::Session,
2113 pool: &mut Self::Pool,
2114 input: &[u8],
2115 fmt: PixelFormat,
2116 roi: Rect,
2117 backend: BackendRequest,
2118 ) -> Result<Self::SubmittedSurface, Self::Error> {
2119 let _ = (ctx, pool);
2120 batch::queue_tile_request(session, input, fmt, backend, batch::BatchOp::Region(roi))
2121 }
2122
2123 fn submit_tile_scaled_to_device(
2124 ctx: &mut j2k_core::DecoderContext<Self::Context>,
2125 session: &mut Self::Session,
2126 pool: &mut Self::Pool,
2127 input: &[u8],
2128 fmt: PixelFormat,
2129 scale: Downscale,
2130 backend: BackendRequest,
2131 ) -> Result<Self::SubmittedSurface, Self::Error> {
2132 let _ = (ctx, pool);
2133 batch::queue_tile_request(session, input, fmt, backend, batch::BatchOp::Scaled(scale))
2134 }
2135
2136 fn submit_tile_region_scaled_to_device(
2137 ctx: &mut j2k_core::DecoderContext<Self::Context>,
2138 session: &mut Self::Session,
2139 pool: &mut Self::Pool,
2140 input: &[u8],
2141 fmt: PixelFormat,
2142 roi: Rect,
2143 scale: Downscale,
2144 backend: BackendRequest,
2145 ) -> Result<Self::SubmittedSurface, Self::Error> {
2146 let _ = (ctx, pool);
2147 batch::queue_tile_request(
2148 session,
2149 input,
2150 fmt,
2151 backend,
2152 batch::BatchOp::RegionScaled { roi, scale },
2153 )
2154 }
2155}
2156
2157impl TileBatchDecodeManyDevice for Codec {
2158 type Context = CpuJ2kContext;
2159 type DeviceSurface = Surface;
2160
2161 fn decode_tiles_to_device(
2162 ctx: &mut j2k_core::DecoderContext<Self::Context>,
2163 pool: &mut Self::Pool,
2164 inputs: &[&[u8]],
2165 fmt: PixelFormat,
2166 backend: BackendRequest,
2167 ) -> Result<Vec<Self::DeviceSurface>, Self::Error> {
2168 if inputs.is_empty() {
2169 return Ok(Vec::new());
2170 }
2171
2172 let mut session = MetalSession::default();
2173 let submissions = inputs
2174 .iter()
2175 .map(|input| {
2176 <Self as TileBatchDecodeSubmit>::submit_tile_to_device(
2177 ctx,
2178 &mut session,
2179 pool,
2180 input,
2181 fmt,
2182 backend,
2183 )
2184 })
2185 .collect::<Result<Vec<_>, _>>()?;
2186
2187 submissions
2188 .into_iter()
2189 .map(j2k_core::DeviceSubmission::wait)
2190 .collect()
2191 }
2192}
2193
2194impl TileBatchDecodeDevice for Codec {
2195 type Context = CpuJ2kContext;
2196 type DeviceSurface = Surface;
2197}
2198
2199fn upload_surface(
2200 bytes: Vec<u8>,
2201 dimensions: (u32, u32),
2202 fmt: PixelFormat,
2203 backend: BackendRequest,
2204) -> Result<Surface, Error> {
2205 let pitch_bytes = dimensions.0 as usize * fmt.bytes_per_pixel();
2206 match backend {
2207 BackendRequest::Cpu | BackendRequest::Auto => Ok(Surface {
2208 backend: BackendKind::Cpu,
2209 residency: SurfaceResidency::Host,
2210 dimensions,
2211 fmt,
2212 pitch_bytes,
2213 byte_offset: 0,
2214 storage: Storage::Host(bytes),
2215 }),
2216 BackendRequest::Metal => {
2217 #[cfg(target_os = "macos")]
2218 {
2219 let _ = bytes;
2220 Err(Error::UnsupportedMetalRequest {
2221 reason: CPU_STAGED_METAL_REQUIRES_EXPLICIT_API,
2222 })
2223 }
2224 #[cfg(not(target_os = "macos"))]
2225 {
2226 let _ = bytes;
2227 Err(Error::MetalUnavailable)
2228 }
2229 }
2230 BackendRequest::Cuda => Err(Error::UnsupportedBackend { request: backend }),
2231 }
2232}
2233
2234#[cfg(target_os = "macos")]
2235fn upload_surface_to_metal_with_device(
2236 bytes: &[u8],
2237 dimensions: (u32, u32),
2238 fmt: PixelFormat,
2239 device: &metal::DeviceRef,
2240) -> Surface {
2241 let pitch_bytes = dimensions.0 as usize * fmt.bytes_per_pixel();
2242 let buffer = device.new_buffer_with_data(
2243 bytes.as_ptr().cast(),
2244 bytes.len() as u64,
2245 MTLResourceOptions::StorageModeShared,
2246 );
2247 Surface {
2248 backend: BackendKind::Metal,
2249 residency: SurfaceResidency::CpuStagedMetalUpload,
2250 dimensions,
2251 fmt,
2252 pitch_bytes,
2253 byte_offset: 0,
2254 storage: Storage::Metal(buffer),
2255 }
2256}
2257
2258pub use j2k::{J2kContext, J2kScratchPool};
2259
2260#[cfg(test)]
2261mod tests {
2262 use super::*;
2263
2264 #[test]
2265 fn metal_runtime_failures_are_not_unsupported_errors() {
2266 for err in [
2267 Error::MetalRuntime {
2268 message: "runtime".to_string(),
2269 },
2270 Error::MetalKernel {
2271 message: "kernel".to_string(),
2272 },
2273 Error::MetalStatePoisoned {
2274 state: "J2K Metal session",
2275 },
2276 ] {
2277 assert!(!err.is_unsupported(), "{err:?}");
2278 }
2279 }
2280
2281 #[test]
2282 fn cpu_uploaded_surface_reports_host_residency() {
2283 let surface = upload_surface(
2284 vec![1, 2, 3],
2285 (1, 1),
2286 PixelFormat::Rgb8,
2287 BackendRequest::Cpu,
2288 )
2289 .expect("create CPU surface");
2290
2291 assert_eq!(surface.backend_kind(), BackendKind::Cpu);
2292 assert_eq!(surface.residency(), SurfaceResidency::Host);
2293 #[cfg(target_os = "macos")]
2294 assert!(surface.metal_buffer().is_none());
2295 }
2296
2297 #[test]
2298 fn download_into_reports_inconsistent_surface_storage_range() {
2299 let surface = Surface {
2300 backend: BackendKind::Cpu,
2301 residency: SurfaceResidency::Host,
2302 dimensions: (2, 1),
2303 fmt: PixelFormat::Gray8,
2304 pitch_bytes: 2,
2305 byte_offset: 0,
2306 storage: Storage::Host(vec![7]),
2307 };
2308 let mut out = [0_u8; 2];
2309
2310 let err = surface
2311 .download_into(&mut out, 2)
2312 .expect_err("inconsistent surface storage should be reported");
2313
2314 assert!(matches!(
2315 err,
2316 Error::MetalKernel { message }
2317 if message == "J2K Metal surface byte range 0..2 exceeds storage length 1"
2318 ));
2319 }
2320
2321 #[cfg(target_os = "macos")]
2322 #[test]
2323 fn metal_backend_sessions_own_distinct_direct_plan_caches() {
2324 let Some(device) = Device::system_default() else {
2325 eprintln!("skipping session cache ownership test: no Metal device");
2326 return;
2327 };
2328
2329 let first = MetalBackendSession::new(device.clone());
2330 let second = MetalBackendSession::new(device);
2331
2332 assert_ne!(
2333 first.direct_cache_ids_for_test(),
2334 second.direct_cache_ids_for_test()
2335 );
2336 }
2337
2338 #[cfg(target_os = "macos")]
2339 #[test]
2340 fn explicit_metal_request_does_not_stage_cpu_pixels() {
2341 if Device::system_default().is_none() {
2342 eprintln!("skipping surface residency test: no Metal device");
2343 return;
2344 }
2345
2346 let result = upload_surface(
2347 vec![1, 2, 3],
2348 (1, 1),
2349 PixelFormat::Rgb8,
2350 BackendRequest::Metal,
2351 );
2352
2353 assert!(matches!(
2354 result,
2355 Err(Error::UnsupportedMetalRequest { reason })
2356 if reason.contains("CPU-staged")
2357 && reason.contains("explicit")
2358 && reason.contains("Metal")
2359 ));
2360 }
2361
2362 #[cfg(target_os = "macos")]
2363 #[test]
2364 fn repeated_region_scaled_color_batch_reuses_prepared_plan() {
2365 if Device::system_default().is_none() {
2366 eprintln!("skipping repeated color plan reuse test: no Metal device");
2367 return;
2368 }
2369
2370 let pixels = j2k_test_support::gradient_u8(64, 64, 3);
2371 let options = j2k_native::EncodeOptions {
2372 reversible: true,
2373 num_decomposition_levels: 2,
2374 ..j2k_native::EncodeOptions::default()
2375 };
2376 let input = Arc::<[u8]>::from(
2377 j2k_native::encode(&pixels, 64, 64, 3, 8, false, &options).expect("encode rgb8"),
2378 );
2379 let roi = Rect {
2380 x: 8,
2381 y: 8,
2382 w: 32,
2383 h: 32,
2384 };
2385 let scale = Downscale::Quarter;
2386 let requests = vec![(input.clone(), roi, scale); 4];
2387 let _guard = hybrid::region_scaled_color_plan_test_lock_for_test();
2388 hybrid::reset_region_scaled_color_plan_builds_for_test();
2389
2390 let surfaces =
2391 hybrid::decode_region_scaled_color_batch_direct_to_device(&requests, PixelFormat::Rgb8)
2392 .expect("repeated RGB region-scaled batch");
2393
2394 assert_eq!(surfaces.len(), requests.len());
2395 assert_eq!(
2396 hybrid::region_scaled_color_plan_builds_for_test(),
2397 1,
2398 "repeated RGB ROI+scaled batches should build and crop one prepared direct color plan"
2399 );
2400 }
2401}