Skip to main content

j2k_metal/batch_decoder/
decoder.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Persistent shared-contract Metal batch decoder.
4
5use super::{
6    BatchDecodeOptions, EncodedImage, Error, MetalBackendSession, MetalBatchDecodeResult,
7    MetalBatchGroup, MetalBatchGroupError, PreparedBatch, PreparedBatchGroup, PreparedImage,
8};
9#[cfg(target_os = "macos")]
10use super::{
11    MetalImageDestination, PixelFormat, PreparedColorPlanCache, PreparedGrayPlanCache,
12    SubmittedMetalPreparedBatch,
13};
14
15/// Persistent Metal batch decoder.
16///
17/// The decoder retains one backend session across calls, including its Metal
18/// runtime, command queue, pipelines, lookup tables, scratch pools, and prepared
19/// direct-plan caches. Inputs queued in one call are grouped by the existing
20/// distinct/repeated HTJ2K batch scheduler.
21pub struct MetalBatchDecoder {
22    pub(super) backend: MetalBackendSession,
23    pub(super) options: BatchDecodeOptions,
24    submission_count: u64,
25    #[cfg(target_os = "macos")]
26    pub(super) prepared_gray_plans: PreparedGrayPlanCache,
27    #[cfg(target_os = "macos")]
28    pub(super) prepared_color_plans: PreparedColorPlanCache,
29}
30
31impl core::fmt::Debug for MetalBatchDecoder {
32    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33        let mut debug = f.debug_struct("MetalBatchDecoder");
34        debug.field("backend", &self.backend);
35        debug.field("options", &self.options);
36        debug.field("submissions", &self.submission_count);
37        #[cfg(target_os = "macos")]
38        debug.field("prepared_gray_plans", &self.prepared_gray_plans.len());
39        #[cfg(target_os = "macos")]
40        debug.field("prepared_color_plans", &self.prepared_color_plans.len());
41        debug.finish_non_exhaustive()
42    }
43}
44
45impl MetalBatchDecoder {
46    /// Create a persistent decoder using the system default Metal device.
47    pub fn system_default() -> Result<Self, Error> {
48        Self::system_default_with_options(BatchDecodeOptions::default())
49    }
50
51    /// Create a persistent decoder with retained shared batch policy.
52    pub fn system_default_with_options(options: BatchDecodeOptions) -> Result<Self, Error> {
53        let backend = MetalBackendSession::system_default()?;
54        #[cfg(target_os = "macos")]
55        {
56            Ok(Self::with_backend_session_and_options(backend, options))
57        }
58        #[cfg(not(target_os = "macos"))]
59        {
60            let _ = backend;
61            let _ = options;
62            Err(Error::MetalUnavailable)
63        }
64    }
65
66    /// Create a persistent decoder from an existing backend session.
67    #[cfg(target_os = "macos")]
68    pub fn with_backend_session(backend: MetalBackendSession) -> Self {
69        Self::with_backend_session_and_options(backend, BatchDecodeOptions::default())
70    }
71
72    /// Create a persistent decoder from an existing backend session and
73    /// retain its shared batch policy.
74    #[cfg(target_os = "macos")]
75    pub fn with_backend_session_and_options(
76        backend: MetalBackendSession,
77        options: BatchDecodeOptions,
78    ) -> Self {
79        Self {
80            backend,
81            options,
82            submission_count: 0,
83            prepared_gray_plans: PreparedGrayPlanCache::new(
84                super::plan_cache::PREPARED_BATCH_PLAN_CACHE_CAP,
85            ),
86            prepared_color_plans: PreparedColorPlanCache::new(
87                super::plan_cache::PREPARED_BATCH_PLAN_CACHE_CAP,
88            ),
89        }
90    }
91
92    /// Shared preparation and output policy retained by this session.
93    #[must_use]
94    pub const fn options(&self) -> BatchDecodeOptions {
95        self.options
96    }
97
98    /// Backend session retained by this decoder.
99    #[cfg(target_os = "macos")]
100    pub fn backend_session(&self) -> &MetalBackendSession {
101        &self.backend
102    }
103
104    /// Number of grouped codec submissions completed by the retained session.
105    pub fn submissions(&self) -> Result<u64, Error> {
106        Ok(self.submission_count)
107    }
108
109    #[cfg(target_os = "macos")]
110    pub(super) fn record_submission(&mut self) {
111        self.submission_count = self.submission_count.saturating_add(1);
112    }
113
114    /// Parse, validate, and group shared codec inputs for repeated Metal decode.
115    pub fn prepare(&self, inputs: Vec<EncodedImage>) -> Result<PreparedBatch, Error> {
116        j2k::prepare_batch(inputs, self.options).map_err(Error::from)
117    }
118
119    /// Regroup caller-supplied prepared images under this session's retained
120    /// settings and output policy without reparsing their encoded bytes.
121    pub fn prepare_prepared_images(
122        &self,
123        images: Vec<PreparedImage>,
124    ) -> Result<PreparedBatch, Error> {
125        j2k::prepare_batch_from_images(images, self.options).map_err(Error::from)
126    }
127
128    /// Prepare and decode owned codec inputs into Metal-resident groups.
129    pub fn decode_batch(
130        &mut self,
131        inputs: Vec<EncodedImage>,
132    ) -> Result<MetalBatchDecodeResult, Error> {
133        let prepared = self.prepare(inputs)?;
134        self.decode_prepared(&prepared)
135    }
136
137    /// Regroup and decode caller-supplied prepared images without reparsing
138    /// their encoded bytes.
139    pub fn decode_prepared_images(
140        &mut self,
141        images: Vec<PreparedImage>,
142    ) -> Result<MetalBatchDecodeResult, Error> {
143        let prepared = self.prepare_prepared_images(images)?;
144        self.decode_prepared(&prepared)
145    }
146
147    /// Decode a reusable shared codec batch without consuming its inputs or plans.
148    pub fn decode_prepared(
149        &mut self,
150        prepared: &PreparedBatch,
151    ) -> Result<MetalBatchDecodeResult, Error> {
152        #[cfg(target_os = "macos")]
153        {
154            self.submit_prepared(prepared)?.wait()
155        }
156        #[cfg(not(target_os = "macos"))]
157        {
158            let mut budget = crate::batch_allocation::BatchMetadataBudget::new(
159                "J2K persistent Metal prepared batch output",
160            );
161            let mut groups = budget.try_vec(
162                prepared.groups().len(),
163                "J2K persistent Metal prepared output groups",
164            )?;
165            let mut group_errors = budget.try_vec(
166                prepared.groups().len(),
167                "J2K persistent Metal prepared group execution failures",
168            )?;
169            let mut errors = budget.try_vec(
170                prepared.errors().len(),
171                "J2K persistent Metal prepared indexed errors",
172            )?;
173            errors.extend_from_slice(prepared.errors());
174            for group in prepared.groups() {
175                match self.decode_prepared_group_with_options(group, prepared.options()) {
176                    Ok(decoded) => groups.push(decoded),
177                    Err(source) if source.session_is_unusable() => return Err(source),
178                    Err(source) => group_errors.push(MetalBatchGroupError::new(group, source)),
179                }
180            }
181            Ok(MetalBatchDecodeResult {
182                groups,
183                errors,
184                group_errors,
185            })
186        }
187    }
188
189    /// Commit every representable shared prepared group to codec-owned Metal
190    /// output storage without waiting on the CPU.
191    ///
192    /// Indexed preflight failures are retained in the returned guard. A
193    /// non-fatal group submission failure is retained alongside other pending
194    /// groups; a session-fatal failure aborts submission after safely retiring
195    /// any groups already committed.
196    #[cfg(target_os = "macos")]
197    pub fn submit_prepared(
198        &mut self,
199        prepared: &PreparedBatch,
200    ) -> Result<SubmittedMetalPreparedBatch, Error> {
201        let mut budget = crate::batch_allocation::BatchMetadataBudget::new(
202            "J2K submitted shared prepared Metal batch",
203        );
204        let mut pending_groups = budget.try_vec(
205            prepared.groups().len(),
206            "J2K submitted shared prepared Metal groups",
207        )?;
208        let mut errors = budget.try_vec(
209            prepared.errors().len(),
210            "J2K submitted shared prepared indexed errors",
211        )?;
212        errors.extend_from_slice(prepared.errors());
213        let mut group_errors = budget.try_vec(
214            prepared.groups().len(),
215            "J2K submitted shared prepared group errors",
216        )?;
217
218        for group in prepared.groups() {
219            match self.submit_prepared_resident_group(group, prepared.options()) {
220                Ok(pending) => pending_groups.push(pending),
221                Err(source) if source.session_is_unusable() => return Err(source),
222                Err(source) => group_errors.push(MetalBatchGroupError::new(group, source)),
223            }
224        }
225        Ok(SubmittedMetalPreparedBatch {
226            pending_groups,
227            errors,
228            group_errors,
229        })
230    }
231
232    /// Prepare and commit one shared encoded batch to codec-owned Metal output
233    /// storage without waiting on the CPU.
234    #[cfg(target_os = "macos")]
235    pub fn submit_batch(
236        &mut self,
237        inputs: Vec<EncodedImage>,
238    ) -> Result<SubmittedMetalPreparedBatch, Error> {
239        let prepared = self.prepare(inputs)?;
240        self.submit_prepared(&prepared)
241    }
242
243    /// Decode one homogeneous shared codec group using the preparation policy captured by the group.
244    pub fn decode_prepared_group(
245        &mut self,
246        group: &PreparedBatchGroup,
247    ) -> Result<MetalBatchGroup, Error> {
248        self.decode_prepared_group_with_options(group, group.options())
249    }
250    /// Validate a caller-owned destination before a direct final-store encode.
251    ///
252    /// This establishes the checked external-write handoff used by framework
253    /// adapters. Validation does not submit work or expose the raw buffer.
254    #[cfg(target_os = "macos")]
255    pub fn validate_destination(
256        &self,
257        destination: &MetalImageDestination,
258        dimensions: (u32, u32),
259        pixel_format: PixelFormat,
260    ) -> Result<(), Error> {
261        destination
262            .validate_device(self.backend_session().device())
263            .and_then(|()| destination.validate_image(dimensions, pixel_format))
264            .map_err(|source| {
265                crate::error::metal_kernel_support_error(
266                    "J2K Metal external decode destination validation failed",
267                    source,
268                )
269            })
270    }
271}
272
273impl j2k::BatchDecoder for MetalBatchDecoder {
274    type Output = MetalBatchDecodeResult;
275    type Error = Error;
276
277    fn options(&self) -> BatchDecodeOptions {
278        self.options
279    }
280
281    fn decode_prepared(&mut self, prepared: &PreparedBatch) -> Result<Self::Output, Self::Error> {
282        MetalBatchDecoder::decode_prepared(self, prepared)
283    }
284}