Skip to main content

j2k_metal/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Apple Metal GPU adapters for Rust JPEG 2000 / HTJ2K decode and encode paths.
4//!
5//! This crate wraps the CPU/native J2K implementation with optional
6//! Metal-resident decode surfaces, batch decode sessions, and lossless encode
7//! helpers on macOS. Non-macOS builds keep the same API surface and return
8//! `Error::MetalUnavailable` for explicit Metal-only requests.
9
10#![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)]
107/// Errors returned by the Metal J2K backend.
108pub enum Error {
109    /// Error returned by the CPU or native J2K decoder.
110    #[error(transparent)]
111    Decode(#[from] J2kError),
112    /// Output buffer validation failed.
113    #[error(transparent)]
114    Buffer(#[from] BufferError),
115    /// The requested backend is unsupported by this crate.
116    #[error("backend request {request:?} is not supported by j2k-metal")]
117    UnsupportedBackend {
118        /// Backend requested by the caller.
119        request: BackendRequest,
120    },
121    /// A Metal-specific request is structurally unsupported.
122    #[error("unsupported J2K Metal request: {reason}")]
123    UnsupportedMetalRequest {
124        /// Static reason describing the rejected request.
125        reason: &'static str,
126    },
127    /// Metal is not available on the current host.
128    #[error("Metal is unavailable on this host")]
129    MetalUnavailable,
130    /// Metal runtime creation or device setup failed.
131    #[error("Metal runtime error: {message}")]
132    MetalRuntime {
133        /// Runtime error message.
134        message: String,
135    },
136    /// Metal kernel launch, validation, or completion failed.
137    #[error("Metal kernel error: {message}")]
138    MetalKernel {
139        /// Kernel error message.
140        message: String,
141    },
142    /// Shared Metal backend state was poisoned by a prior panic.
143    #[error("Metal state `{state}` is poisoned")]
144    MetalStatePoisoned {
145        /// Name of the poisoned state.
146        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)]
203/// Decoded J2K image surface returned by the Metal backend.
204pub 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)]
215/// Where a decoded J2K surface is currently resident.
216pub enum SurfaceResidency {
217    /// Pixel bytes are resident in host memory.
218    Host,
219    /// Pixel bytes were produced directly by a Metal decode kernel.
220    MetalResidentDecode,
221    /// Pixel bytes were decoded on CPU and uploaded into a Metal buffer.
222    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    /// Current residency for the surface bytes.
230    pub fn residency(&self) -> SurfaceResidency {
231        self.residency
232    }
233
234    /// Number of bytes between consecutive rows.
235    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                // SAFETY: Metal surface byte views are bounded by validated dimensions and formats.
278                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    /// Return the tightly packed surface bytes.
289    ///
290    /// Metal-backed surfaces are expected to use host-addressable buffers. This
291    /// method panics only if the surface metadata is internally inconsistent;
292    /// fallible operations such as [`Self::download_into`] return those errors.
293    pub fn as_bytes(&self) -> &[u8] {
294        self.storage_bytes()
295            .expect("validated J2K Metal surface byte range")
296    }
297
298    /// Copy the tightly packed surface into a caller-provided strided buffer.
299    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    /// Return the Metal buffer and byte offset when the surface is Metal-backed.
312    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)]
400/// Reusable Metal device session for J2K decode and encode submissions.
401pub 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    /// Create a session bound to an existing Metal device.
413    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    /// Create a session from the system default Metal device.
424    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    /// Metal device used by this session.
431    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)]
472/// Placeholder Metal session for non-macOS builds.
473pub struct MetalBackendSession {
474    _private: (),
475}
476
477#[cfg(not(target_os = "macos"))]
478impl MetalBackendSession {
479    /// Return `Error::MetalUnavailable` on hosts without Metal support.
480    pub fn system_default() -> Result<Self, Error> {
481        Err(Error::MetalUnavailable)
482    }
483}
484
485#[derive(Clone, Default)]
486/// Shared batching session used by J2K Metal submit APIs.
487pub struct MetalSession {
488    shared: batch::SharedSession,
489    #[cfg(target_os = "macos")]
490    backend: Option<MetalBackendSession>,
491}
492
493impl MetalSession {
494    /// Create a batching session backed by an explicit Metal backend session.
495    #[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    /// Metal backend session owned by this batching session, if any.
504    #[cfg(target_os = "macos")]
505    pub fn backend_session(&self) -> Option<&MetalBackendSession> {
506        self.backend.as_ref()
507    }
508
509    /// Number of Metal or emulated submissions flushed through this session.
510    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/// Convenience wrapper for submitting a group of J2K/HTJ2K tiles to one
530/// decoder session.
531///
532/// This is intentionally codec-scoped: callers own slide metadata, tile
533/// coordinates, cache policy, and viewport decisions. The batch only preserves
534/// submission order and lets compatible tile requests share the Metal session.
535#[derive(Default)]
536pub struct MetalTileBatch {
537    session: MetalSession,
538    submissions: Vec<batch::MetalSubmission>,
539}
540
541impl MetalTileBatch {
542    /// Create an empty tile batch.
543    pub fn new() -> Self {
544        Self::default()
545    }
546
547    /// Create an empty tile batch with capacity for `capacity` submissions.
548    pub fn with_capacity(capacity: usize) -> Self {
549        Self {
550            submissions: Vec::with_capacity(capacity),
551            ..Self::default()
552        }
553    }
554
555    /// Number of queued tile requests.
556    pub fn len(&self) -> usize {
557        self.submissions.len()
558    }
559
560    /// Whether the batch has no queued tile requests.
561    pub fn is_empty(&self) -> bool {
562        self.submissions.is_empty()
563    }
564
565    /// Number of Metal session submissions already flushed.
566    ///
567    /// Queued requests normally do not increment this until `decode_all` waits
568    /// on the first result.
569    pub fn submissions(&self) -> Result<u64, Error> {
570        self.session.submissions()
571    }
572
573    /// Queue a full-tile decode request, copying the compressed tile bytes into
574    /// the batch.
575    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    /// Queue a full-tile decode request backed by shared compressed tile bytes.
585    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    /// Queue a region decode request, copying the compressed tile bytes into
604    /// the batch.
605    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    /// Queue a region decode request backed by shared compressed tile bytes.
616    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    /// Queue a scaled decode request, copying the compressed tile bytes into
636    /// the batch.
637    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    /// Queue a scaled decode request backed by shared compressed tile bytes.
648    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    /// Queue a region decode at reduced resolution, copying the compressed tile bytes.
668    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    /// Queue a region decode at reduced resolution backed by shared compressed tile bytes.
680    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    /// Decode all queued tile requests and return surfaces in submission order.
701    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
710/// JPEG 2000 decoder that can return host or Metal-resident surfaces.
711pub 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    /// Parse a J2K or HTJ2K codestream into a decoder.
730    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    /// Create a decoder from an already parsed J2K view.
750    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    /// Borrow the underlying CPU J2K decoder.
770    pub fn inner(&self) -> &CpuDecoder<'a> {
771        &self.inner
772    }
773
774    /// Decode a full image into a Metal-resident device surface.
775    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    /// Decode a full image into a host-backed surface.
804    pub fn decode_to_host_surface(&mut self, fmt: PixelFormat) -> Result<Surface, Error> {
805        self.decode_to_cpu_surface(fmt)
806    }
807
808    /// Decode a source region into a host-backed surface.
809    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    /// Decode a full image at reduced resolution into a host-backed surface.
822    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    /// Decode a source region at reduced resolution into a host-backed surface.
835    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    /// Decode a full image on CPU and upload the result into a Metal surface.
849    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    /// Decode a source region on CPU and upload the result into a Metal surface.
876    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    /// Decode a full image at reduced resolution on CPU and upload it into Metal.
913    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    /// Decode a scaled source region on CPU and upload it into a Metal surface.
945    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    /// Decode a source region into a Metal-resident device surface.
1608    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    /// Decode a full image at reduced resolution into a Metal-resident surface.
1700    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    /// Decode a scaled source region into a Metal-resident device surface.
1735    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)]
2025/// J2K codec marker used by J2K's generic decode traits.
2026pub 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}