1#[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#[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 #[must_use]
38 pub fn len(&self) -> usize {
39 self.pending.len()
40 }
41
42 #[must_use]
44 pub fn is_empty(&self) -> bool {
45 self.pending.is_empty()
46 }
47
48 pub fn wait(self) -> Result<BurnBatchDecode<Wgpu>, BurnDecodeError> {
51 materialize(self.pending.wait()?, &self.device)
52 }
53}
54
55#[cfg(not(target_os = "macos"))]
57#[derive(Debug)]
58pub struct SubmittedMetalBurnBatch {
59 _private: (),
60}
61
62#[cfg(not(target_os = "macos"))]
63impl SubmittedMetalBurnBatch {
64 #[must_use]
66 pub const fn len(&self) -> usize {
67 0
68 }
69
70 #[must_use]
72 pub const fn is_empty(&self) -> bool {
73 true
74 }
75
76 pub fn wait(self) -> Result<BurnBatchDecode<Wgpu>, BurnDecodeError> {
78 Err(unavailable())
79 }
80}
81
82pub 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 #[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 #[cfg(not(target_os = "macos"))]
114 pub fn system_default(_options: BatchDecodeOptions) -> Result<Self, BurnDecodeError> {
115 Err(unavailable())
116 }
117
118 #[must_use]
120 pub const fn codec(&self) -> &CodecDecoder {
121 &self.codec
122 }
123
124 #[must_use]
126 pub const fn device(&self) -> &WgpuDevice {
127 &self.device
128 }
129
130 pub fn prepare(&self, inputs: Vec<EncodedImage>) -> Result<PreparedBatch, BurnDecodeError> {
132 Ok(self.codec.prepare(inputs)?)
133 }
134
135 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 pub fn decode(
145 &mut self,
146 inputs: Vec<EncodedImage>,
147 ) -> Result<BurnBatchDecode<Wgpu>, BurnDecodeError> {
148 self.submit(inputs)?.wait()
149 }
150
151 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 pub fn decode_prepared(
162 &mut self,
163 prepared: &PreparedBatch,
164 ) -> Result<BurnBatchDecode<Wgpu>, BurnDecodeError> {
165 self.submit_prepared(prepared)?.wait()
166 }
167
168 #[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 #[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 #[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 #[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 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}