Skip to main content

j2k_ml/metal/
batch.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#[cfg(target_os = "macos")]
4use burn_core::tensor::backend::Backend;
5use burn_wgpu::{Wgpu, WgpuDevice};
6use j2k::{BatchDecodeOptions, EncodedImage, PreparedBatch, PreparedImage};
7use j2k_metal::MetalBatchDecoder as CodecDecoder;
8#[cfg(target_os = "macos")]
9use j2k_metal::{MetalBatchDecodeResult, SubmittedMetalPreparedBatch};
10
11#[cfg(target_os = "macos")]
12use crate::BurnBatchGroupError;
13use crate::{BurnBatchDecode, BurnDecodeError};
14
15/// Pending Metal codec decode whose completed pixels will be staged through
16/// host memory and uploaded with Burn's ordinary tensor API.
17#[cfg(target_os = "macos")]
18#[must_use = "submitted Metal upload batches must be waited or dropped"]
19pub struct SubmittedMetalBurnBatch {
20    pending: SubmittedMetalPreparedBatch,
21    device: WgpuDevice,
22}
23
24#[cfg(target_os = "macos")]
25impl core::fmt::Debug for SubmittedMetalBurnBatch {
26    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27        f.debug_struct("SubmittedMetalBurnBatch")
28            .field("pending", &self.pending)
29            .field("device", &self.device)
30            .finish()
31    }
32}
33
34#[cfg(target_os = "macos")]
35impl SubmittedMetalBurnBatch {
36    /// Number of successfully submitted homogeneous codec groups.
37    #[must_use]
38    pub fn len(&self) -> usize {
39        self.pending.len()
40    }
41
42    /// Whether no accelerator codec work was submitted.
43    #[must_use]
44    pub fn is_empty(&self) -> bool {
45        self.pending.is_empty()
46    }
47
48    /// Wait for Metal codec completion, copy each completed group to host
49    /// staging, and upload it into an ordinary Burn wgpu tensor.
50    pub fn wait(self) -> Result<BurnBatchDecode<Wgpu>, BurnDecodeError> {
51        materialize(self.pending.wait()?, &self.device)
52    }
53}
54
55/// Uninhabited pending Metal upload returned only for cross-platform API compatibility.
56#[cfg(not(target_os = "macos"))]
57#[derive(Debug)]
58pub struct SubmittedMetalBurnBatch {
59    _private: (),
60}
61
62#[cfg(not(target_os = "macos"))]
63impl SubmittedMetalBurnBatch {
64    /// No Metal groups can be submitted on this host.
65    #[must_use]
66    pub const fn len(&self) -> usize {
67        0
68    }
69
70    /// No Metal groups can be submitted on this host.
71    #[must_use]
72    pub const fn is_empty(&self) -> bool {
73        true
74    }
75
76    /// Return the typed Metal-unavailable error.
77    pub fn wait(self) -> Result<BurnBatchDecode<Wgpu>, BurnDecodeError> {
78        Err(unavailable())
79    }
80}
81
82/// Persistent Metal codec session followed by an explicit staged Burn upload.
83///
84/// JPEG 2000 decoding executes on Metal. Completed codec-owned device output is
85/// copied to host staging and then uploaded through [`burn_core::tensor::Tensor::from_data`].
86/// This type does not provide direct-destination or zero-copy behavior.
87pub struct MetalBurnDecoder {
88    codec: CodecDecoder,
89    device: WgpuDevice,
90}
91
92impl core::fmt::Debug for MetalBurnDecoder {
93    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
94        f.debug_struct("MetalBurnDecoder")
95            .field("codec", &self.codec)
96            .field("device", &self.device)
97            .field("options", &self.codec.options())
98            .finish_non_exhaustive()
99    }
100}
101
102impl MetalBurnDecoder {
103    /// Create a Metal codec session and target Burn's default wgpu device.
104    #[cfg(target_os = "macos")]
105    pub fn system_default(options: BatchDecodeOptions) -> Result<Self, BurnDecodeError> {
106        Ok(Self {
107            codec: CodecDecoder::system_default_with_options(options)?,
108            device: WgpuDevice::DefaultDevice,
109        })
110    }
111
112    /// Return a typed unavailable error on hosts without Metal.
113    #[cfg(not(target_os = "macos"))]
114    pub fn system_default(_options: BatchDecodeOptions) -> Result<Self, BurnDecodeError> {
115        Err(unavailable())
116    }
117
118    /// Borrow the persistent Metal codec session.
119    #[must_use]
120    pub const fn codec(&self) -> &CodecDecoder {
121        &self.codec
122    }
123
124    /// Borrow the Burn wgpu device receiving the staged upload.
125    #[must_use]
126    pub const fn device(&self) -> &WgpuDevice {
127        &self.device
128    }
129
130    /// Parse and group owned inputs once for repeated decode calls.
131    pub fn prepare(&self, inputs: Vec<EncodedImage>) -> Result<PreparedBatch, BurnDecodeError> {
132        Ok(self.codec.prepare(inputs)?)
133    }
134
135    /// Regroup caller-supplied prepared images without reparsing codestream bytes.
136    pub fn prepare_prepared_images(
137        &self,
138        images: Vec<PreparedImage>,
139    ) -> Result<PreparedBatch, BurnDecodeError> {
140        Ok(self.codec.prepare_prepared_images(images)?)
141    }
142
143    /// Decode on Metal, stage completed pixels through host memory, and upload to Burn.
144    pub fn decode(
145        &mut self,
146        inputs: Vec<EncodedImage>,
147    ) -> Result<BurnBatchDecode<Wgpu>, BurnDecodeError> {
148        self.submit(inputs)?.wait()
149    }
150
151    /// Regroup prepared images, decode on Metal, and perform the staged upload.
152    pub fn decode_prepared_images(
153        &mut self,
154        images: Vec<PreparedImage>,
155    ) -> Result<BurnBatchDecode<Wgpu>, BurnDecodeError> {
156        let prepared = self.prepare_prepared_images(images)?;
157        self.decode_prepared(&prepared)
158    }
159
160    /// Decode a reusable preparation on Metal and perform the staged upload.
161    pub fn decode_prepared(
162        &mut self,
163        prepared: &PreparedBatch,
164    ) -> Result<BurnBatchDecode<Wgpu>, BurnDecodeError> {
165        self.submit_prepared(prepared)?.wait()
166    }
167
168    /// Submit Metal codec work. The later [`SubmittedMetalBurnBatch::wait`]
169    /// performs a synchronous device-to-host copy and ordinary Burn upload.
170    #[cfg(target_os = "macos")]
171    pub fn submit(
172        &mut self,
173        inputs: Vec<EncodedImage>,
174    ) -> Result<SubmittedMetalBurnBatch, BurnDecodeError> {
175        let prepared = self.prepare(inputs)?;
176        self.submit_prepared(&prepared)
177    }
178
179    /// Return a typed unavailable error on hosts without Metal.
180    #[cfg(not(target_os = "macos"))]
181    pub fn submit(
182        &mut self,
183        _inputs: Vec<EncodedImage>,
184    ) -> Result<SubmittedMetalBurnBatch, BurnDecodeError> {
185        Err(unavailable())
186    }
187
188    /// Submit a reusable preparation to codec-owned Metal output.
189    #[cfg(target_os = "macos")]
190    pub fn submit_prepared(
191        &mut self,
192        prepared: &PreparedBatch,
193    ) -> Result<SubmittedMetalBurnBatch, BurnDecodeError> {
194        ensure_dtypes::<Wgpu>(prepared, &self.device)?;
195        Ok(SubmittedMetalBurnBatch {
196            pending: self.codec.submit_prepared(prepared)?,
197            device: self.device.clone(),
198        })
199    }
200
201    /// Return a typed unavailable error on hosts without Metal.
202    #[cfg(not(target_os = "macos"))]
203    pub fn submit_prepared(
204        &mut self,
205        _prepared: &PreparedBatch,
206    ) -> Result<SubmittedMetalBurnBatch, BurnDecodeError> {
207        Err(unavailable())
208    }
209}
210
211#[cfg(target_os = "macos")]
212fn ensure_dtypes<B: Backend>(
213    prepared: &PreparedBatch,
214    device: &B::Device,
215) -> Result<(), BurnDecodeError> {
216    for group in prepared.groups() {
217        let dtype = crate::batch_contract::dtype(group.info().sample_type)?;
218        if !B::supports_dtype(device, dtype) {
219            return Err(BurnDecodeError::UnsupportedDType { dtype });
220        }
221    }
222    Ok(())
223}
224
225#[cfg(target_os = "macos")]
226fn materialize(
227    decoded: MetalBatchDecodeResult,
228    device: &WgpuDevice,
229) -> Result<BurnBatchDecode<Wgpu>, BurnDecodeError> {
230    let (codec_groups, errors, codec_group_errors) = decoded.into_parts();
231    let mut groups = Vec::new();
232    groups
233        .try_reserve_exact(codec_groups.len())
234        .map_err(|_| BurnDecodeError::SizeOverflow)?;
235    for group in codec_groups {
236        let resident = group
237            .resident_batch()
238            .ok_or(BurnDecodeError::UnsupportedCodecContract)?;
239        // SAFETY: codec completion was established before this immutable read,
240        // and `checked_buffer_read_vec` copies the validated resident range.
241        let bytes = unsafe {
242            j2k_metal_support::checked_buffer_read_vec::<u8>(
243                resident.metal_buffer(),
244                resident.byte_offset(),
245                resident.byte_len(),
246            )
247        }
248        .map_err(|source| BurnDecodeError::AcceleratorInterop {
249            backend: "Metal staged readback",
250            message: source.to_string(),
251        })?;
252        let (info, source_indices, decoded_rects, warnings, _surfaces) = group.into_parts();
253        groups.push(crate::staging::materialize(
254            info,
255            source_indices,
256            decoded_rects,
257            warnings,
258            bytes,
259            device,
260        )?);
261    }
262    let group_errors = codec_group_errors
263        .into_iter()
264        .map(|error| {
265            let (source_indices, source) = error.into_parts();
266            BurnBatchGroupError::new(source_indices, BurnDecodeError::Metal(source))
267        })
268        .collect();
269    Ok(BurnBatchDecode {
270        groups,
271        errors,
272        group_errors,
273    })
274}
275
276#[cfg(not(target_os = "macos"))]
277fn unavailable() -> BurnDecodeError {
278    BurnDecodeError::AcceleratorInterop {
279        backend: "Metal",
280        message: "Metal is unavailable on this platform".to_string(),
281    }
282}