1#![deny(unsafe_op_in_unsafe_fn)]
11#![warn(unreachable_pub)]
12
13#[cfg(target_os = "macos")]
14mod abi;
15mod batch;
16#[cfg(target_os = "macos")]
17mod buffers;
18#[cfg(target_os = "macos")]
19mod compute;
20mod encode;
21mod routing;
22mod session;
23pub mod viewport;
25
26use std::sync::Arc;
27#[cfg(target_os = "macos")]
28use std::sync::Mutex;
29#[cfg(target_os = "macos")]
30use std::sync::OnceLock;
31
32use j2k_core::{
33 copy_tight_pixels_to_strided_output, BackendKind, BackendRequest, BufferError, CodecError,
34 DecodeOutcome, DeviceMemoryRange, DeviceSubmission, DeviceSurface, Downscale, ImageCodec,
35 ImageDecode, ImageDecodeDevice, ImageDecodeSubmit, PixelFormat, Rect, TileBatchDecodeDevice,
36 TileBatchDecodeManyDevice, TileBatchDecodeSubmit,
37};
38use j2k_jpeg::{
39 adapter::{
40 build_fast420_packet, build_fast420_packet_for_decoder, build_fast422_packet,
41 build_fast422_packet_for_decoder, build_fast444_packet, build_fast444_packet_for_decoder,
42 decoder_bytes, JpegFast420PacketV1, JpegFast422PacketV1, JpegFast444PacketV1,
43 },
44 Decoder as CpuDecoder, DecoderContext as CpuDecoderContext, JpegError, JpegView,
45 ScratchPool as CpuScratchPool, Warning as CpuWarning,
46};
47use j2k_metal_support::{
48 cpu_host_route, metal_kernel_route, metal_unavailable_route, reject_explicit_metal_route,
49 reject_unsupported_backend_route, MetalRouteProfileLabels,
50};
51#[cfg(target_os = "macos")]
52use j2k_metal_support::{system_default_device, MetalSupportError};
53
54pub use encode::{
55 encode_jpeg_baseline_batch_from_metal_buffers, encode_jpeg_baseline_from_metal_buffer,
56 JpegBaselineMetalEncodeTile,
57};
58
59#[cfg(target_os = "macos")]
60use metal::foreign_types::ForeignType;
61#[cfg(target_os = "macos")]
62use metal::{
63 Buffer, BufferRef, CommandBuffer, Device, MTLPixelFormat, MTLResourceOptions, MTLStorageMode,
64 MTLTextureType, MTLTextureUsage, Texture, TextureDescriptor, TextureRef,
65};
66
67#[derive(Debug, thiserror::Error)]
68pub enum Error {
70 #[error(transparent)]
72 Decode(#[from] JpegError),
73 #[error(transparent)]
75 Encode(#[from] j2k_jpeg::JpegEncodeError),
76 #[error(transparent)]
78 Buffer(#[from] BufferError),
79 #[error("backend request {request:?} is not supported by j2k-jpeg-metal")]
81 UnsupportedBackend {
82 request: BackendRequest,
84 },
85 #[error("unsupported JPEG Metal request: {reason}")]
87 UnsupportedMetalRequest {
88 reason: &'static str,
90 },
91 #[error("Metal is unavailable on this host")]
93 MetalUnavailable,
94 #[error("Metal runtime error: {message}")]
96 MetalRuntime {
97 message: String,
99 },
100 #[error("Metal kernel error: {message}")]
102 MetalKernel {
103 message: String,
105 },
106 #[error("Metal state `{state}` is poisoned")]
108 MetalStatePoisoned {
109 state: &'static str,
111 },
112}
113
114impl CodecError for Error {
115 fn is_truncated(&self) -> bool {
116 matches!(self, Self::Decode(inner) if inner.is_truncated())
117 }
118
119 fn is_not_implemented(&self) -> bool {
120 matches!(self, Self::Decode(inner) if inner.is_not_implemented())
121 }
122
123 fn is_unsupported(&self) -> bool {
124 matches!(
125 self,
126 Self::UnsupportedBackend { .. }
127 | Self::MetalUnavailable
128 | Self::UnsupportedMetalRequest { .. }
129 ) || matches!(self, Self::Decode(inner) if inner.is_unsupported())
130 }
131
132 fn is_buffer_error(&self) -> bool {
133 matches!(self, Self::Buffer(_))
134 || matches!(self, Self::Decode(inner) if inner.is_buffer_error())
135 }
136}
137
138#[derive(Clone)]
139pub(crate) enum Storage {
140 Host(Vec<u8>),
141 #[cfg(target_os = "macos")]
142 Metal {
143 buffer: Buffer,
144 offset: usize,
145 },
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum SurfaceResidency {
151 Host,
153 MetalResidentDecode,
155 CpuStagedMetalUpload,
157}
158
159#[derive(Clone)]
160pub struct Surface {
162 backend: BackendKind,
163 residency: SurfaceResidency,
164 dimensions: (u32, u32),
165 fmt: PixelFormat,
166 pitch_bytes: usize,
167 storage: Storage,
168}
169
170impl Surface {
171 pub fn pitch_bytes(&self) -> usize {
173 self.pitch_bytes
174 }
175
176 pub fn residency(&self) -> SurfaceResidency {
178 self.residency
179 }
180
181 pub fn as_bytes(&self) -> &[u8] {
183 match &self.storage {
184 Storage::Host(bytes) => bytes,
185 #[cfg(target_os = "macos")]
186 Storage::Metal { buffer, offset } => {
187 let len = self.byte_len();
188 unsafe {
190 core::slice::from_raw_parts(buffer.contents().cast::<u8>().add(*offset), len)
191 }
192 }
193 }
194 }
195
196 pub fn download_into(&self, out: &mut [u8], stride: usize) -> Result<(), Error> {
198 copy_tight_pixels_to_strided_output(self.as_bytes(), self.dimensions, self.fmt, out, stride)
199 .map_err(Error::from)
200 }
201
202 #[cfg(target_os = "macos")]
203 pub fn metal_buffer(&self) -> Option<(&Buffer, usize)> {
205 match &self.storage {
206 Storage::Metal { buffer, offset } => Some((buffer, *offset)),
207 Storage::Host(_) => None,
208 }
209 }
210
211 #[cfg(target_os = "macos")]
212 pub(crate) fn from_metal_buffer(
213 buffer: Buffer,
214 dimensions: (u32, u32),
215 fmt: PixelFormat,
216 ) -> Self {
217 Self::from_metal_buffer_offset(buffer, dimensions, fmt, 0)
218 }
219
220 #[cfg(target_os = "macos")]
221 pub(crate) fn from_metal_buffer_offset(
222 buffer: Buffer,
223 dimensions: (u32, u32),
224 fmt: PixelFormat,
225 offset: usize,
226 ) -> Self {
227 Self {
228 backend: BackendKind::Metal,
229 residency: SurfaceResidency::MetalResidentDecode,
230 dimensions,
231 fmt,
232 pitch_bytes: dimensions.0 as usize * fmt.bytes_per_pixel(),
233 storage: Storage::Metal { buffer, offset },
234 }
235 }
236
237 #[cfg(target_os = "macos")]
238 pub(crate) fn from_cpu_staged_metal_buffer(
239 buffer: Buffer,
240 dimensions: (u32, u32),
241 fmt: PixelFormat,
242 ) -> Self {
243 Self::from_cpu_staged_metal_buffer_offset(buffer, dimensions, fmt, 0)
244 }
245
246 #[cfg(target_os = "macos")]
247 pub(crate) fn from_cpu_staged_metal_buffer_offset(
248 buffer: Buffer,
249 dimensions: (u32, u32),
250 fmt: PixelFormat,
251 offset: usize,
252 ) -> Self {
253 Self {
254 backend: BackendKind::Metal,
255 residency: SurfaceResidency::CpuStagedMetalUpload,
256 dimensions,
257 fmt,
258 pitch_bytes: dimensions.0 as usize * fmt.bytes_per_pixel(),
259 storage: Storage::Metal { buffer, offset },
260 }
261 }
262}
263
264impl DeviceSurface for Surface {
265 fn backend_kind(&self) -> BackendKind {
266 self.backend
267 }
268
269 fn residency(&self) -> j2k_core::SurfaceResidency {
270 match self.residency {
271 SurfaceResidency::Host => j2k_core::SurfaceResidency::Host,
272 SurfaceResidency::MetalResidentDecode => {
273 j2k_core::SurfaceResidency::MetalResidentDecode
274 }
275 SurfaceResidency::CpuStagedMetalUpload => {
276 j2k_core::SurfaceResidency::CpuStagedMetalUpload
277 }
278 }
279 }
280
281 fn dimensions(&self) -> (u32, u32) {
282 self.dimensions
283 }
284
285 fn pixel_format(&self) -> PixelFormat {
286 self.fmt
287 }
288
289 fn byte_len(&self) -> usize {
290 self.pitch_bytes * self.dimensions.1 as usize
291 }
292
293 fn memory_range(&self) -> Option<DeviceMemoryRange> {
294 match &self.storage {
295 Storage::Host(_) => None,
296 #[cfg(target_os = "macos")]
297 Storage::Metal { buffer, offset } => Some(DeviceMemoryRange::new(
298 BackendKind::Metal,
299 u64::try_from(buffer.as_ptr() as usize).ok()?,
300 *offset,
301 self.byte_len(),
302 )),
303 }
304 }
305}
306
307#[cfg(target_os = "macos")]
308#[doc(hidden)]
309#[derive(Clone)]
310pub struct ResidentPrivateJpegTile {
311 pub buffer: Buffer,
312 pub byte_offset: usize,
313 pub dimensions: (u32, u32),
314 pub pixel_format: PixelFormat,
315 pub pitch_bytes: usize,
316 pub status_buffer: Buffer,
317 pub command_buffer: CommandBuffer,
318}
319
320#[cfg(target_os = "macos")]
321#[derive(Clone)]
322pub struct MetalBatchOutputBuffer {
324 buffer: Buffer,
325 dimensions: (u32, u32),
326 fmt: PixelFormat,
327 pitch_bytes: usize,
328 tile_stride_bytes: usize,
329 tile_capacity: usize,
330}
331
332#[cfg(target_os = "macos")]
333impl MetalBatchOutputBuffer {
334 pub fn new_rgb8_tiles(
336 session: &MetalBackendSession,
337 dimensions: (u32, u32),
338 tile_capacity: usize,
339 ) -> Result<Self, Error> {
340 Self::new_tiles(session, dimensions, PixelFormat::Rgb8, tile_capacity)
341 }
342
343 pub fn ensure_rgb8_tiles(
349 &mut self,
350 session: &MetalBackendSession,
351 dimensions: (u32, u32),
352 tile_capacity: usize,
353 ) -> Result<(), Error> {
354 if self.dimensions == dimensions
355 && self.fmt == PixelFormat::Rgb8
356 && self.tile_capacity >= tile_capacity
357 {
358 return Ok(());
359 }
360
361 *self = Self::new_rgb8_tiles(session, dimensions, tile_capacity)?;
362 Ok(())
363 }
364
365 pub fn ensure_rgb8_scaled_tiles(
367 &mut self,
368 session: &MetalBackendSession,
369 full_dimensions: (u32, u32),
370 scale: Downscale,
371 tile_capacity: usize,
372 ) -> Result<(), Error> {
373 self.ensure_rgb8_tiles(session, scaled_dims(full_dimensions, scale), tile_capacity)
374 }
375
376 pub fn ensure_rgb8_region_scaled_tiles(
378 &mut self,
379 session: &MetalBackendSession,
380 roi: Rect,
381 scale: Downscale,
382 tile_capacity: usize,
383 ) -> Result<(), Error> {
384 let scaled = roi.scaled_covering(scale);
385 self.ensure_rgb8_tiles(session, (scaled.w, scaled.h), tile_capacity)
386 }
387
388 pub fn ensure_rgb8_batch_report(
393 &mut self,
394 session: &MetalBackendSession,
395 report: &JpegMetalResidentBatchReport,
396 ) -> Result<(), Error> {
397 let Some(dimensions) = report_required_output_dimensions(report)? else {
398 return Ok(());
399 };
400 self.ensure_rgb8_tiles(session, dimensions, report.required_tile_capacity())
401 }
402
403 fn new_tiles(
404 session: &MetalBackendSession,
405 dimensions: (u32, u32),
406 fmt: PixelFormat,
407 tile_capacity: usize,
408 ) -> Result<Self, Error> {
409 if dimensions.0 == 0 || dimensions.1 == 0 || tile_capacity == 0 {
410 return Err(Error::UnsupportedMetalRequest {
411 reason: "JPEG Metal batch output requires nonzero dimensions and tile capacity",
412 });
413 }
414 let row_bytes = dimensions
415 .0
416 .checked_mul(u32::try_from(fmt.bytes_per_pixel()).map_err(|_| {
417 BufferError::SizeOverflow {
418 what: "JPEG Metal output row bytes",
419 }
420 })?)
421 .ok_or(BufferError::SizeOverflow {
422 what: "JPEG Metal output row bytes",
423 })? as usize;
424 let tile_stride_bytes =
425 row_bytes
426 .checked_mul(dimensions.1 as usize)
427 .ok_or(BufferError::SizeOverflow {
428 what: "JPEG Metal output tile bytes",
429 })?;
430 let byte_len =
431 tile_stride_bytes
432 .checked_mul(tile_capacity)
433 .ok_or(BufferError::SizeOverflow {
434 what: "JPEG Metal batch output bytes",
435 })?;
436 let byte_len_u64 = u64::try_from(byte_len).map_err(|_| BufferError::SizeOverflow {
437 what: "JPEG Metal batch output bytes",
438 })?;
439 let buffer = session
440 .device()
441 .new_buffer(byte_len_u64, MTLResourceOptions::StorageModeShared);
442 Ok(Self {
443 buffer,
444 dimensions,
445 fmt,
446 pitch_bytes: row_bytes,
447 tile_stride_bytes,
448 tile_capacity,
449 })
450 }
451
452 pub fn buffer(&self) -> &BufferRef {
454 self.buffer.as_ref()
455 }
456
457 pub fn dimensions(&self) -> (u32, u32) {
459 self.dimensions
460 }
461
462 pub fn pixel_format(&self) -> PixelFormat {
464 self.fmt
465 }
466
467 pub fn tile_capacity(&self) -> usize {
469 self.tile_capacity
470 }
471
472 pub fn pitch_bytes(&self) -> usize {
474 self.pitch_bytes
475 }
476
477 pub fn tile_stride_bytes(&self) -> usize {
479 self.tile_stride_bytes
480 }
481
482 pub fn byte_len(&self) -> usize {
484 self.tile_stride_bytes * self.tile_capacity
485 }
486
487 pub(crate) fn clone_buffer(&self) -> Buffer {
488 self.buffer.clone()
489 }
490}
491
492#[cfg(target_os = "macos")]
493#[derive(Clone)]
494pub struct MetalBatchTextureOutput {
496 textures: Vec<Texture>,
497 dimensions: (u32, u32),
498 fmt: PixelFormat,
499 metal_fmt: MTLPixelFormat,
500}
501
502#[cfg(target_os = "macos")]
503impl MetalBatchTextureOutput {
504 pub fn new_rgba8_tiles(
506 session: &MetalBackendSession,
507 dimensions: (u32, u32),
508 tile_capacity: usize,
509 ) -> Result<Self, Error> {
510 if dimensions.0 == 0 || dimensions.1 == 0 || tile_capacity == 0 {
511 return Err(Error::UnsupportedMetalRequest {
512 reason:
513 "JPEG Metal batch texture output requires nonzero dimensions and tile capacity",
514 });
515 }
516
517 let descriptor = TextureDescriptor::new();
518 descriptor.set_texture_type(MTLTextureType::D2);
519 descriptor.set_pixel_format(MTLPixelFormat::RGBA8Unorm);
520 descriptor.set_width(u64::from(dimensions.0));
521 descriptor.set_height(u64::from(dimensions.1));
522 descriptor.set_depth(1);
523 descriptor.set_mipmap_level_count(1);
524 descriptor.set_sample_count(1);
525 descriptor.set_storage_mode(MTLStorageMode::Private);
526 descriptor.set_usage(MTLTextureUsage::ShaderRead | MTLTextureUsage::ShaderWrite);
527
528 let mut textures = Vec::with_capacity(tile_capacity);
529 for _ in 0..tile_capacity {
530 textures.push(session.device().new_texture(&descriptor));
531 }
532
533 Ok(Self {
534 textures,
535 dimensions,
536 fmt: PixelFormat::Rgba8,
537 metal_fmt: MTLPixelFormat::RGBA8Unorm,
538 })
539 }
540
541 pub fn ensure_rgba8_tiles(
547 &mut self,
548 session: &MetalBackendSession,
549 dimensions: (u32, u32),
550 tile_capacity: usize,
551 ) -> Result<(), Error> {
552 if self.dimensions == dimensions
553 && self.fmt == PixelFormat::Rgba8
554 && self.metal_fmt == MTLPixelFormat::RGBA8Unorm
555 && self.tile_capacity() >= tile_capacity
556 {
557 return Ok(());
558 }
559
560 *self = Self::new_rgba8_tiles(session, dimensions, tile_capacity)?;
561 Ok(())
562 }
563
564 pub fn ensure_rgba8_scaled_tiles(
566 &mut self,
567 session: &MetalBackendSession,
568 full_dimensions: (u32, u32),
569 scale: Downscale,
570 tile_capacity: usize,
571 ) -> Result<(), Error> {
572 self.ensure_rgba8_tiles(session, scaled_dims(full_dimensions, scale), tile_capacity)
573 }
574
575 pub fn ensure_rgba8_region_scaled_tiles(
577 &mut self,
578 session: &MetalBackendSession,
579 roi: Rect,
580 scale: Downscale,
581 tile_capacity: usize,
582 ) -> Result<(), Error> {
583 let scaled = roi.scaled_covering(scale);
584 self.ensure_rgba8_tiles(session, (scaled.w, scaled.h), tile_capacity)
585 }
586
587 pub fn ensure_rgba8_batch_report(
592 &mut self,
593 session: &MetalBackendSession,
594 report: &JpegMetalResidentBatchReport,
595 ) -> Result<(), Error> {
596 let Some(dimensions) = report_required_output_dimensions(report)? else {
597 return Ok(());
598 };
599 self.ensure_rgba8_tiles(session, dimensions, report.required_tile_capacity())
600 }
601
602 pub fn dimensions(&self) -> (u32, u32) {
604 self.dimensions
605 }
606
607 pub fn pixel_format(&self) -> PixelFormat {
609 self.fmt
610 }
611
612 pub fn metal_pixel_format(&self) -> MTLPixelFormat {
614 self.metal_fmt
615 }
616
617 pub fn tile_capacity(&self) -> usize {
619 self.textures.len()
620 }
621
622 pub fn texture(&self, index: usize) -> Option<&TextureRef> {
624 self.textures.get(index).map(std::convert::AsRef::as_ref)
625 }
626
627 pub(crate) fn clone_texture(&self, index: usize) -> Option<Texture> {
628 self.textures.get(index).cloned()
629 }
630
631 pub(crate) fn clone_slots(&self, indices: &[usize]) -> Result<Self, Error> {
632 let mut textures = Vec::with_capacity(indices.len());
633 for &index in indices {
634 textures.push(
635 self.clone_texture(index)
636 .ok_or_else(|| Error::MetalKernel {
637 message: "JPEG Metal batch texture output slot was missing".to_string(),
638 })?,
639 );
640 }
641 Ok(Self {
642 textures,
643 dimensions: self.dimensions,
644 fmt: self.fmt,
645 metal_fmt: self.metal_fmt,
646 })
647 }
648}
649
650#[cfg(target_os = "macos")]
651#[derive(Clone)]
652pub struct MetalTextureTile {
654 texture: Texture,
655 dimensions: (u32, u32),
656 fmt: PixelFormat,
657}
658
659#[cfg(target_os = "macos")]
660impl MetalTextureTile {
661 pub(crate) fn new(texture: Texture, dimensions: (u32, u32), fmt: PixelFormat) -> Self {
662 Self {
663 texture,
664 dimensions,
665 fmt,
666 }
667 }
668
669 pub fn texture(&self) -> &TextureRef {
671 self.texture.as_ref()
672 }
673
674 pub fn dimensions(&self) -> (u32, u32) {
676 self.dimensions
677 }
678
679 pub fn pixel_format(&self) -> PixelFormat {
681 self.fmt
682 }
683}
684
685#[cfg(target_os = "macos")]
686#[derive(Clone)]
687pub struct MetalBackendSession {
689 device: Device,
690 runtime: Arc<OnceLock<Result<compute::MetalRuntime, MetalSupportError>>>,
691}
692
693#[cfg(target_os = "macos")]
694impl MetalBackendSession {
695 pub fn new(device: Device) -> Self {
697 Self {
698 device,
699 runtime: Arc::new(OnceLock::new()),
700 }
701 }
702
703 pub fn system_default() -> Result<Self, Error> {
705 system_default_device()
706 .map(Self::new)
707 .map_err(|error| compute::runtime_initialization_error(&error))
708 }
709
710 pub fn device(&self) -> &metal::DeviceRef {
712 self.device.as_ref()
713 }
714}
715
716#[cfg(target_os = "macos")]
717impl j2k_core::AcceleratorSession for MetalBackendSession {
718 fn backend_kind(&self) -> BackendKind {
719 BackendKind::Metal
720 }
721}
722
723#[cfg(target_os = "macos")]
724impl core::fmt::Debug for MetalBackendSession {
725 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
726 f.debug_struct("MetalBackendSession")
727 .field("device", &self.device.name())
728 .field("runtime_initialized", &self.runtime.get().is_some())
729 .finish()
730 }
731}
732
733#[cfg(not(target_os = "macos"))]
734#[derive(Clone, Copy, Debug, Default)]
735pub struct MetalBackendSession {
737 _private: (),
738}
739
740#[cfg(not(target_os = "macos"))]
741impl MetalBackendSession {
742 pub fn system_default() -> Result<Self, Error> {
744 Err(Error::MetalUnavailable)
745 }
746}
747
748#[derive(Default)]
749pub struct MetalSession {
751 shared: session::SharedSession,
752}
753
754impl MetalSession {
755 #[cfg(target_os = "macos")]
757 pub fn with_backend_session(backend_session: MetalBackendSession) -> Self {
758 Self {
759 shared: session::SharedSession(Arc::new(Mutex::new(
760 session::SessionState::with_backend_session(backend_session),
761 ))),
762 }
763 }
764
765 pub fn submissions(&self) -> Result<u64, Error> {
767 Ok(self.shared.lock()?.submissions)
768 }
769}
770
771impl core::fmt::Debug for MetalSession {
772 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
773 f.debug_struct("MetalSession")
774 .field("submissions", &self.submissions())
775 .finish()
776 }
777}
778
779#[derive(Default)]
786pub struct JpegTileBatch {
787 session: MetalSession,
788 submissions: Vec<batch::MetalSubmission>,
789}
790
791impl JpegTileBatch {
792 pub fn new() -> Self {
794 Self::default()
795 }
796
797 pub fn with_capacity(capacity: usize) -> Self {
799 Self {
800 submissions: Vec::with_capacity(capacity),
801 ..Self::default()
802 }
803 }
804
805 pub fn len(&self) -> usize {
807 self.submissions.len()
808 }
809
810 pub fn is_empty(&self) -> bool {
812 self.submissions.is_empty()
813 }
814
815 pub fn submissions(&self) -> Result<u64, Error> {
820 self.session.submissions()
821 }
822
823 pub fn push_tile(
826 &mut self,
827 input: &[u8],
828 fmt: PixelFormat,
829 backend: BackendRequest,
830 ) -> Result<usize, Error> {
831 self.push_shared_tile(Arc::<[u8]>::from(input), fmt, backend)
832 }
833
834 pub fn push_shared_tile(
836 &mut self,
837 input: Arc<[u8]>,
838 fmt: PixelFormat,
839 backend: BackendRequest,
840 ) -> Result<usize, Error> {
841 self.push_shared_request(input, fmt, backend, batch::BatchOp::Full)
842 }
843
844 pub fn push_tile_region(
847 &mut self,
848 input: &[u8],
849 fmt: PixelFormat,
850 roi: Rect,
851 backend: BackendRequest,
852 ) -> Result<usize, Error> {
853 self.push_shared_tile_region(Arc::<[u8]>::from(input), fmt, roi, backend)
854 }
855
856 pub fn push_shared_tile_region(
858 &mut self,
859 input: Arc<[u8]>,
860 fmt: PixelFormat,
861 roi: Rect,
862 backend: BackendRequest,
863 ) -> Result<usize, Error> {
864 self.push_shared_request(input, fmt, backend, batch::BatchOp::Region(roi))
865 }
866
867 pub fn push_tile_scaled(
870 &mut self,
871 input: &[u8],
872 fmt: PixelFormat,
873 scale: Downscale,
874 backend: BackendRequest,
875 ) -> Result<usize, Error> {
876 self.push_shared_tile_scaled(Arc::<[u8]>::from(input), fmt, scale, backend)
877 }
878
879 pub fn push_shared_tile_scaled(
881 &mut self,
882 input: Arc<[u8]>,
883 fmt: PixelFormat,
884 scale: Downscale,
885 backend: BackendRequest,
886 ) -> Result<usize, Error> {
887 self.push_shared_request(input, fmt, backend, batch::BatchOp::Scaled(scale))
888 }
889
890 pub fn push_tile_region_scaled(
893 &mut self,
894 input: &[u8],
895 fmt: PixelFormat,
896 roi: Rect,
897 scale: Downscale,
898 backend: BackendRequest,
899 ) -> Result<usize, Error> {
900 self.push_shared_tile_region_scaled(Arc::<[u8]>::from(input), fmt, roi, scale, backend)
901 }
902
903 pub fn push_shared_tile_region_scaled(
906 &mut self,
907 input: Arc<[u8]>,
908 fmt: PixelFormat,
909 roi: Rect,
910 scale: Downscale,
911 backend: BackendRequest,
912 ) -> Result<usize, Error> {
913 self.push_shared_request(
914 input,
915 fmt,
916 backend,
917 batch::BatchOp::RegionScaled { roi, scale },
918 )
919 }
920
921 pub fn decode_all(self) -> Result<Vec<Surface>, Error> {
923 let mut surfaces = Vec::with_capacity(self.submissions.len());
924 for submission in self.submissions {
925 surfaces.push(submission.wait()?);
926 }
927 Ok(surfaces)
928 }
929
930 fn push_shared_request(
931 &mut self,
932 input: Arc<[u8]>,
933 fmt: PixelFormat,
934 backend: BackendRequest,
935 op: batch::BatchOp,
936 ) -> Result<usize, Error> {
937 let slot = self.submissions.len();
938 let submission = {
939 let mut state = self.session.shared.lock()?;
940 let (fast444_packet, fast422_packet, fast420_packet) =
941 state.resolve_fast_packets(&input, backend);
942 let slot = state.queue_request(batch::QueuedRequest::new_shared(
943 input,
944 fmt,
945 backend,
946 op,
947 fast444_packet,
948 fast422_packet,
949 fast420_packet,
950 ));
951 batch::MetalSubmission {
952 session: self.session.shared.clone(),
953 slot,
954 }
955 };
956 self.submissions.push(submission);
957 Ok(slot)
958 }
959}
960
961pub struct Decoder<'a> {
963 inner: CpuDecoder<'a>,
964 source: Arc<[u8]>,
965 fast444_packet: Option<Arc<JpegFast444PacketV1>>,
966 fast422_packet: Option<Arc<JpegFast422PacketV1>>,
967 fast420_packet: Option<Arc<JpegFast420PacketV1>>,
968}
969
970impl<'a> Decoder<'a> {
971 pub fn new(input: &'a [u8]) -> Result<Self, Error> {
973 let inner = CpuDecoder::new(input)?;
974 Ok(Self {
975 fast444_packet: build_fast444_packet(input).ok().map(Arc::new),
976 fast422_packet: build_fast422_packet(input).ok().map(Arc::new),
977 fast420_packet: build_fast420_packet(input).ok().map(Arc::new),
978 inner,
979 source: Arc::<[u8]>::from(input),
980 })
981 }
982
983 pub fn from_view(view: JpegView<'a>) -> Result<Self, Error> {
985 let inner = CpuDecoder::from_view(view)?;
986 let source = Arc::<[u8]>::from(decoder_bytes(&inner));
987 let fast444_packet = build_fast444_packet_for_decoder(&inner).ok().map(Arc::new);
988 let fast422_packet = build_fast422_packet_for_decoder(&inner).ok().map(Arc::new);
989 let fast420_packet = build_fast420_packet_for_decoder(&inner).ok().map(Arc::new);
990 Ok(Self {
991 inner,
992 source,
993 fast444_packet,
994 fast422_packet,
995 fast420_packet,
996 })
997 }
998
999 pub fn inner(&self) -> &CpuDecoder<'a> {
1001 &self.inner
1002 }
1003
1004 #[cfg(target_os = "macos")]
1005 pub(crate) fn fast444_packet(&self) -> Option<&JpegFast444PacketV1> {
1006 self.fast444_packet.as_deref()
1007 }
1008
1009 #[cfg(target_os = "macos")]
1010 pub(crate) fn fast422_packet(&self) -> Option<&JpegFast422PacketV1> {
1011 self.fast422_packet.as_deref()
1012 }
1013
1014 #[cfg(target_os = "macos")]
1015 pub(crate) fn fast420_packet(&self) -> Option<&JpegFast420PacketV1> {
1016 self.fast420_packet.as_deref()
1017 }
1018
1019 #[cfg(target_os = "macos")]
1020 pub(crate) fn rgb8_region_scaled_metal_request(
1021 &self,
1022 roi: Rect,
1023 scale: Downscale,
1024 ) -> batch::QueuedRequest {
1025 self.rgb8_metal_request(batch::BatchOp::RegionScaled { roi, scale })
1026 }
1027
1028 #[cfg(target_os = "macos")]
1029 pub(crate) fn rgb8_metal_request(&self, op: batch::BatchOp) -> batch::QueuedRequest {
1030 batch::QueuedRequest::new_shared(
1031 Arc::clone(&self.source),
1032 PixelFormat::Rgb8,
1033 BackendRequest::Metal,
1034 op,
1035 self.fast444_packet.clone(),
1036 self.fast422_packet.clone(),
1037 self.fast420_packet.clone(),
1038 )
1039 }
1040
1041 pub fn into_inner(self) -> CpuDecoder<'a> {
1043 self.inner
1044 }
1045
1046 pub fn decode_region_scaled_to_device(
1048 &mut self,
1049 fmt: PixelFormat,
1050 roi: Rect,
1051 scale: Downscale,
1052 backend: BackendRequest,
1053 ) -> Result<Surface, Error> {
1054 let mut pool = CpuScratchPool::new();
1055 decode_region_scaled_surface_from_decoder(
1056 &self.inner,
1057 &mut pool,
1058 fmt,
1059 roi,
1060 scale,
1061 backend,
1062 self.fast444_packet.as_deref(),
1063 self.fast422_packet.as_deref(),
1064 self.fast420_packet.as_deref(),
1065 )
1066 }
1067
1068 pub fn decode_to_device_with_session(
1070 &mut self,
1071 fmt: PixelFormat,
1072 session: &MetalBackendSession,
1073 ) -> Result<Surface, Error> {
1074 #[cfg(target_os = "macos")]
1075 {
1076 let mut pool = CpuScratchPool::new();
1077 let decision = choose_route(
1078 &self.inner,
1079 BackendRequest::Metal,
1080 fmt,
1081 batch::BatchOp::Full,
1082 self.fast444_packet.as_deref(),
1083 self.fast422_packet.as_deref(),
1084 self.fast420_packet.as_deref(),
1085 );
1086 if let Some(err) = routing::decision_error(decision) {
1087 return Err(err);
1088 }
1089 match decision {
1090 routing::RouteDecision::MetalKernel => {
1091 reject_cpu_staged_metal_upload(compute::decode_to_surface_with_session(
1092 &self.inner,
1093 &mut pool,
1094 fmt,
1095 self.fast444_packet.as_deref(),
1096 self.fast422_packet.as_deref(),
1097 self.fast420_packet.as_deref(),
1098 session,
1099 )?)
1100 }
1101 routing::RouteDecision::CpuHost
1102 | routing::RouteDecision::RejectExplicitMetal { .. }
1103 | routing::RouteDecision::RejectUnsupportedBackend { .. }
1104 | routing::RouteDecision::MetalUnavailable => unreachable!("handled above"),
1105 }
1106 }
1107 #[cfg(not(target_os = "macos"))]
1108 {
1109 let _ = session;
1110 let decision = choose_route(
1111 &self.inner,
1112 BackendRequest::Metal,
1113 fmt,
1114 batch::BatchOp::Full,
1115 self.fast444_packet.as_deref(),
1116 self.fast422_packet.as_deref(),
1117 self.fast420_packet.as_deref(),
1118 );
1119 if let Some(err) = routing::decision_error(decision) {
1120 return Err(err);
1121 }
1122 Err(Error::MetalUnavailable)
1123 }
1124 }
1125
1126 #[cfg(target_os = "macos")]
1127 #[doc(hidden)]
1128 pub fn decode_private_rgb8_tile_with_session(
1129 &mut self,
1130 session: &MetalBackendSession,
1131 ) -> Result<ResidentPrivateJpegTile, Error> {
1132 let decision = choose_route(
1133 &self.inner,
1134 BackendRequest::Metal,
1135 PixelFormat::Rgb8,
1136 batch::BatchOp::Full,
1137 self.fast444_packet.as_deref(),
1138 self.fast422_packet.as_deref(),
1139 self.fast420_packet.as_deref(),
1140 );
1141 if let Some(err) = routing::decision_error(decision) {
1142 return Err(err);
1143 }
1144 match decision {
1145 routing::RouteDecision::MetalKernel => compute::decode_private_rgb8_tile_with_session(
1146 &self.inner,
1147 self.fast444_packet.as_deref(),
1148 self.fast422_packet.as_deref(),
1149 self.fast420_packet.as_deref(),
1150 session,
1151 ),
1152 routing::RouteDecision::CpuHost
1153 | routing::RouteDecision::RejectExplicitMetal { .. }
1154 | routing::RouteDecision::RejectUnsupportedBackend { .. }
1155 | routing::RouteDecision::MetalUnavailable => unreachable!("handled above"),
1156 }
1157 }
1158}
1159
1160impl ImageCodec for Decoder<'_> {
1161 type Error = Error;
1162 type Warning = CpuWarning;
1163 type Pool = CpuScratchPool;
1164}
1165
1166impl<'a> ImageDecode<'a> for Decoder<'a> {
1167 type View = JpegView<'a>;
1168
1169 fn inspect(input: &'a [u8]) -> Result<j2k_core::Info, Self::Error> {
1170 Ok(CpuDecoder::inspect(input)?.to_core_info())
1171 }
1172
1173 fn parse(input: &'a [u8]) -> Result<Self::View, Self::Error> {
1174 Ok(JpegView::parse(input)?)
1175 }
1176
1177 fn from_view(view: Self::View) -> Result<Self, Self::Error> {
1178 Self::from_view(view)
1179 }
1180
1181 fn decode_into(
1182 &mut self,
1183 out: &mut [u8],
1184 stride: usize,
1185 fmt: PixelFormat,
1186 ) -> Result<DecodeOutcome<Self::Warning>, Self::Error> {
1187 Ok(self.inner.decode_into(out, stride, fmt)?.into())
1188 }
1189
1190 fn decode_into_with_scratch(
1191 &mut self,
1192 pool: &mut Self::Pool,
1193 out: &mut [u8],
1194 stride: usize,
1195 fmt: PixelFormat,
1196 ) -> Result<DecodeOutcome<Self::Warning>, Self::Error> {
1197 Ok(self
1198 .inner
1199 .decode_into_with_scratch(pool, out, stride, fmt)?
1200 .into())
1201 }
1202
1203 fn decode_region_into(
1204 &mut self,
1205 pool: &mut Self::Pool,
1206 out: &mut [u8],
1207 stride: usize,
1208 fmt: PixelFormat,
1209 roi: Rect,
1210 ) -> Result<DecodeOutcome<Self::Warning>, Self::Error> {
1211 Ok(self
1212 .inner
1213 .decode_region_into_with_scratch(pool, out, stride, fmt, roi.into())?
1214 .into())
1215 }
1216
1217 fn decode_scaled_into(
1218 &mut self,
1219 pool: &mut Self::Pool,
1220 out: &mut [u8],
1221 stride: usize,
1222 fmt: PixelFormat,
1223 scale: Downscale,
1224 ) -> Result<DecodeOutcome<Self::Warning>, Self::Error> {
1225 Ok(self
1226 .inner
1227 .decode_scaled_into_with_scratch(pool, out, stride, fmt, scale)?
1228 .into())
1229 }
1230
1231 fn decode_region_scaled_into(
1232 &mut self,
1233 pool: &mut Self::Pool,
1234 out: &mut [u8],
1235 stride: usize,
1236 fmt: PixelFormat,
1237 roi: Rect,
1238 scale: Downscale,
1239 ) -> Result<DecodeOutcome<Self::Warning>, Self::Error> {
1240 Ok(self
1241 .inner
1242 .decode_region_scaled_into_with_scratch(pool, out, stride, fmt, roi.into(), scale)?
1243 .into())
1244 }
1245}
1246
1247impl<'a> ImageDecodeDevice<'a> for Decoder<'a> {
1248 type DeviceSurface = Surface;
1249}
1250
1251#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1252pub struct Codec;
1254
1255#[cfg(target_os = "macos")]
1256struct Rgb8MetalBatchPlan {
1257 requests: Vec<batch::QueuedRequest>,
1258 output_dimensions: Option<(u32, u32)>,
1259}
1260
1261#[cfg(target_os = "macos")]
1262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1263pub struct JpegMetalResidentBatchReport {
1265 pub op: j2k_jpeg::JpegDecodeOp,
1267 pub tile_count: usize,
1269 pub output_dimensions: Option<(u32, u32)>,
1271 pub eligibility: j2k_jpeg::JpegBackendEligibility,
1273}
1274
1275#[cfg(target_os = "macos")]
1276impl JpegMetalResidentBatchReport {
1277 #[must_use]
1279 pub fn required_tile_capacity(&self) -> usize {
1280 self.tile_count
1281 }
1282}
1283
1284#[cfg(target_os = "macos")]
1285fn report_required_output_dimensions(
1286 report: &JpegMetalResidentBatchReport,
1287) -> Result<Option<(u32, u32)>, Error> {
1288 if !report.eligibility.eligible {
1289 return Err(Error::UnsupportedMetalRequest {
1290 reason: report
1291 .eligibility
1292 .reason
1293 .unwrap_or("JPEG Metal resident batch report is not eligible"),
1294 });
1295 }
1296 if report.tile_count == 0 {
1297 return Ok(None);
1298 }
1299 report
1300 .output_dimensions
1301 .ok_or(Error::UnsupportedMetalRequest {
1302 reason: "JPEG Metal resident batch report is missing output dimensions",
1303 })
1304 .map(Some)
1305}
1306
1307#[cfg(target_os = "macos")]
1308fn rgb8_metal_output_dimensions_for_op(
1309 full_dimensions: (u32, u32),
1310 op: j2k_jpeg::JpegDecodeOp,
1311) -> Option<(u32, u32)> {
1312 match op {
1313 j2k_jpeg::JpegDecodeOp::Full => Some(full_dimensions),
1314 j2k_jpeg::JpegDecodeOp::Scaled(scale) => Some(scaled_dims(full_dimensions, scale)),
1315 j2k_jpeg::JpegDecodeOp::RegionScaled { roi, scale } => {
1316 let scaled = Rect {
1317 x: roi.x,
1318 y: roi.y,
1319 w: roi.w,
1320 h: roi.h,
1321 }
1322 .scaled_covering(scale);
1323 Some((scaled.w, scaled.h))
1324 }
1325 j2k_jpeg::JpegDecodeOp::Region(_) => None,
1326 }
1327}
1328
1329#[cfg(target_os = "macos")]
1330fn decoder_resident_sampling_family(decoder: &Decoder<'_>) -> batch::SamplingFamily {
1331 if decoder.fast420_packet().is_some() {
1332 batch::SamplingFamily::Fast420
1333 } else if decoder.fast422_packet().is_some() {
1334 batch::SamplingFamily::Fast422
1335 } else if decoder.fast444_packet().is_some() {
1336 batch::SamplingFamily::Fast444
1337 } else {
1338 batch::SamplingFamily::Other
1339 }
1340}
1341
1342#[cfg(target_os = "macos")]
1343fn decoder_resident_restart_interval_mcus(decoder: &Decoder<'_>) -> u32 {
1344 if let Some(packet) = decoder.fast420_packet() {
1345 packet.restart_interval_mcus
1346 } else if let Some(packet) = decoder.fast422_packet() {
1347 packet.restart_interval_mcus
1348 } else if let Some(packet) = decoder.fast444_packet() {
1349 packet.restart_interval_mcus
1350 } else {
1351 0
1352 }
1353}
1354
1355impl ImageCodec for Codec {
1356 type Error = Error;
1357 type Warning = CpuWarning;
1358 type Pool = CpuScratchPool;
1359}
1360
1361#[cfg(target_os = "macos")]
1364#[derive(Clone, Copy)]
1365pub enum Rgb8MetalBatchSource<'a, 'b> {
1366 Bytes(&'a [&'a [u8]]),
1368 Decoders(&'a [&'a Decoder<'b>]),
1371}
1372
1373#[cfg(target_os = "macos")]
1374impl Rgb8MetalBatchSource<'_, '_> {
1375 fn is_empty(&self) -> bool {
1376 match self {
1377 Rgb8MetalBatchSource::Bytes(inputs) => inputs.is_empty(),
1378 Rgb8MetalBatchSource::Decoders(decoders) => decoders.is_empty(),
1379 }
1380 }
1381}
1382
1383#[cfg(target_os = "macos")]
1385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1386pub enum Rgb8MetalBatchOp {
1387 Full,
1389 Scaled(Downscale),
1391 RegionScaled {
1393 roi: Rect,
1395 scale: Downscale,
1397 },
1398}
1399
1400#[cfg(target_os = "macos")]
1402#[derive(Clone, Copy)]
1403pub struct Rgb8MetalBatchRequest<'a, 'b> {
1404 pub source: Rgb8MetalBatchSource<'a, 'b>,
1406 pub op: Rgb8MetalBatchOp,
1408}
1409
1410#[cfg(target_os = "macos")]
1412pub enum MetalBufferBatchTarget<'a> {
1413 Reusable(&'a MetalBatchOutputBuffer),
1415 Resizable(&'a mut MetalBatchOutputBuffer),
1417}
1418
1419#[cfg(target_os = "macos")]
1421pub enum MetalTextureBatchTarget<'a> {
1422 Reusable(&'a MetalBatchTextureOutput),
1424 Resizable(&'a mut MetalBatchTextureOutput),
1426}
1427
1428impl Codec {
1429 #[cfg(target_os = "macos")]
1430 pub fn inspect_rgb8_decoder_batch_metal_output(
1436 decoders: &[&Decoder<'_>],
1437 op: j2k_jpeg::JpegDecodeOp,
1438 ) -> JpegMetalResidentBatchReport {
1439 if decoders.is_empty() {
1440 return JpegMetalResidentBatchReport {
1441 op,
1442 tile_count: 0,
1443 output_dimensions: None,
1444 eligibility: j2k_jpeg::JpegBackendEligibility {
1445 eligible: true,
1446 reason: None,
1447 },
1448 };
1449 }
1450
1451 let mut output_dimensions = None;
1452 let mut sampling_family = None;
1453 for decoder in decoders {
1454 let request = j2k_jpeg::JpegCapabilityRequest {
1455 op,
1456 fmt: PixelFormat::Rgb8,
1457 };
1458 let report = j2k_jpeg::JpegCapabilityReport::for_decoder(decoder.inner(), request);
1459 let eligibility = report.metal_resident_rgb8_batch_output();
1460 if !eligibility.eligible {
1461 return JpegMetalResidentBatchReport {
1462 op,
1463 tile_count: decoders.len(),
1464 output_dimensions: None,
1465 eligibility,
1466 };
1467 }
1468
1469 if decoder.fast444_packet().is_none()
1470 && decoder.fast422_packet().is_none()
1471 && decoder.fast420_packet().is_none()
1472 {
1473 return JpegMetalResidentBatchReport {
1474 op,
1475 tile_count: decoders.len(),
1476 output_dimensions: None,
1477 eligibility: j2k_jpeg::JpegBackendEligibility {
1478 eligible: false,
1479 reason: Some(
1480 "JPEG Metal reusable resident batch output requires cached fast-packet state",
1481 ),
1482 },
1483 };
1484 }
1485
1486 let Some(dimensions) =
1487 rgb8_metal_output_dimensions_for_op(decoder.inner().info().dimensions, op)
1488 else {
1489 return JpegMetalResidentBatchReport {
1490 op,
1491 tile_count: decoders.len(),
1492 output_dimensions: None,
1493 eligibility,
1494 };
1495 };
1496 if let Some(first) = output_dimensions {
1497 if first != dimensions {
1498 return JpegMetalResidentBatchReport {
1499 op,
1500 tile_count: decoders.len(),
1501 output_dimensions: None,
1502 eligibility: j2k_jpeg::JpegBackendEligibility {
1503 eligible: false,
1504 reason: Some(
1505 "JPEG Metal reusable RGB8 batch output requires matching output dimensions",
1506 ),
1507 },
1508 };
1509 }
1510 } else {
1511 output_dimensions = Some(dimensions);
1512 }
1513
1514 let decoder_sampling_family = decoder_resident_sampling_family(decoder);
1515 if let Some(first) = sampling_family {
1516 if first != decoder_sampling_family {
1517 return JpegMetalResidentBatchReport {
1518 op,
1519 tile_count: decoders.len(),
1520 output_dimensions: None,
1521 eligibility: j2k_jpeg::JpegBackendEligibility {
1522 eligible: false,
1523 reason: Some(
1524 "JPEG Metal reusable resident batch output requires one batch to use the same fast-packet sampling family",
1525 ),
1526 },
1527 };
1528 }
1529 } else {
1530 sampling_family = Some(decoder_sampling_family);
1531 }
1532
1533 if op == j2k_jpeg::JpegDecodeOp::Full
1534 && matches!(
1535 decoder_sampling_family,
1536 batch::SamplingFamily::Fast422 | batch::SamplingFamily::Fast444
1537 )
1538 && decoder_resident_restart_interval_mcus(decoder) != 0
1539 {
1540 return JpegMetalResidentBatchReport {
1541 op,
1542 tile_count: decoders.len(),
1543 output_dimensions: None,
1544 eligibility: j2k_jpeg::JpegBackendEligibility {
1545 eligible: false,
1546 reason: Some(
1547 "JPEG Metal reusable resident batch output does not support restart-coded full-tile 4:2:2 or 4:4:4 batches",
1548 ),
1549 },
1550 };
1551 }
1552 }
1553
1554 JpegMetalResidentBatchReport {
1555 op,
1556 tile_count: decoders.len(),
1557 output_dimensions,
1558 eligibility: j2k_jpeg::JpegBackendEligibility {
1559 eligible: true,
1560 reason: None,
1561 },
1562 }
1563 }
1564
1565 #[cfg(target_os = "macos")]
1566 fn observe_rgb8_batch_output_dimensions(
1567 first_output_dimensions: &mut Option<(u32, u32)>,
1568 output_dimensions: (u32, u32),
1569 ) -> Result<(), Error> {
1570 if let Some(first) = *first_output_dimensions {
1571 if first != output_dimensions {
1572 return Err(Error::UnsupportedMetalRequest {
1573 reason:
1574 "JPEG Metal reusable RGB8 batch output requires matching output dimensions",
1575 });
1576 }
1577 } else {
1578 *first_output_dimensions = Some(output_dimensions);
1579 }
1580 Ok(())
1581 }
1582
1583 #[cfg(target_os = "macos")]
1584 fn rgb8_metal_batch_requests(
1585 inputs: &[&[u8]],
1586 mut op_for_decoder: impl FnMut(&CpuDecoder<'_>) -> batch::BatchOp,
1587 ) -> Result<Vec<batch::QueuedRequest>, Error> {
1588 let plan = Self::rgb8_metal_batch_requests_with_output_dimensions(inputs, |decoder| {
1589 (op_for_decoder(decoder), decoder.info().dimensions)
1590 })?;
1591 Ok(plan.requests)
1592 }
1593
1594 #[cfg(target_os = "macos")]
1595 fn rgb8_metal_batch_requests_with_output_dimensions(
1596 inputs: &[&[u8]],
1597 mut op_and_dimensions_for_decoder: impl FnMut(&CpuDecoder<'_>) -> (batch::BatchOp, (u32, u32)),
1598 ) -> Result<Rgb8MetalBatchPlan, Error> {
1599 let mut state = session::SessionState::default();
1600 let mut requests = Vec::with_capacity(inputs.len());
1601 let mut first_output_dimensions = None;
1602 for input in inputs {
1603 let decoder = CpuDecoder::new(input)?;
1604 let (op, output_dimensions) = op_and_dimensions_for_decoder(&decoder);
1605 Self::observe_rgb8_batch_output_dimensions(
1606 &mut first_output_dimensions,
1607 output_dimensions,
1608 )?;
1609 let input = state.intern_input_slice(input);
1610 let (fast444_packet, fast422_packet, fast420_packet) =
1611 state.resolve_fast_packets(&input, BackendRequest::Metal);
1612 requests.push(batch::QueuedRequest::new_shared(
1613 input,
1614 PixelFormat::Rgb8,
1615 BackendRequest::Metal,
1616 op,
1617 fast444_packet,
1618 fast422_packet,
1619 fast420_packet,
1620 ));
1621 }
1622 Ok(Rgb8MetalBatchPlan {
1623 requests,
1624 output_dimensions: first_output_dimensions,
1625 })
1626 }
1627
1628 #[cfg(target_os = "macos")]
1629 fn rgb8_metal_decoder_batch_requests_with_output_dimensions(
1630 decoders: &[&Decoder<'_>],
1631 mut op_and_dimensions_for_decoder: impl FnMut(&Decoder<'_>) -> (batch::BatchOp, (u32, u32)),
1632 ) -> Result<Rgb8MetalBatchPlan, Error> {
1633 let mut requests = Vec::with_capacity(decoders.len());
1634 let mut first_output_dimensions = None;
1635 for decoder in decoders {
1636 let (op, output_dimensions) = op_and_dimensions_for_decoder(decoder);
1637 Self::observe_rgb8_batch_output_dimensions(
1638 &mut first_output_dimensions,
1639 output_dimensions,
1640 )?;
1641 requests.push(decoder.rgb8_metal_request(op));
1642 }
1643 Ok(Rgb8MetalBatchPlan {
1644 requests,
1645 output_dimensions: first_output_dimensions,
1646 })
1647 }
1648 #[cfg(target_os = "macos")]
1649 fn rgb8_batch_op_and_dimensions(
1650 op: Rgb8MetalBatchOp,
1651 dimensions: (u32, u32),
1652 ) -> (batch::BatchOp, (u32, u32)) {
1653 match op {
1654 Rgb8MetalBatchOp::Full => (batch::BatchOp::Full, dimensions),
1655 Rgb8MetalBatchOp::Scaled(scale) => {
1656 let (w, h) = dimensions;
1657 (
1658 batch::BatchOp::RegionScaled {
1659 roi: Rect { x: 0, y: 0, w, h },
1660 scale,
1661 },
1662 scaled_dims((w, h), scale),
1663 )
1664 }
1665 Rgb8MetalBatchOp::RegionScaled { roi, scale } => {
1666 let scaled = roi.scaled_covering(scale);
1667 (
1668 batch::BatchOp::RegionScaled { roi, scale },
1669 (scaled.w, scaled.h),
1670 )
1671 }
1672 }
1673 }
1674
1675 #[cfg(target_os = "macos")]
1676 fn rgb8_batch_jpeg_decode_op(op: Rgb8MetalBatchOp) -> j2k_jpeg::JpegDecodeOp {
1677 match op {
1678 Rgb8MetalBatchOp::Full => j2k_jpeg::JpegDecodeOp::Full,
1679 Rgb8MetalBatchOp::Scaled(scale) => j2k_jpeg::JpegDecodeOp::Scaled(scale),
1680 Rgb8MetalBatchOp::RegionScaled { roi, scale } => j2k_jpeg::JpegDecodeOp::RegionScaled {
1681 roi: roi.into(),
1682 scale,
1683 },
1684 }
1685 }
1686
1687 #[cfg(target_os = "macos")]
1688 fn plan_rgb8_metal_batch(
1689 source: Rgb8MetalBatchSource<'_, '_>,
1690 op: Rgb8MetalBatchOp,
1691 track_output_dimensions: bool,
1692 ) -> Result<(Rgb8MetalBatchPlan, usize), Error> {
1693 match source {
1694 Rgb8MetalBatchSource::Bytes(inputs) => {
1695 if track_output_dimensions {
1696 Self::rgb8_metal_batch_requests_with_output_dimensions(inputs, |decoder| {
1697 Self::rgb8_batch_op_and_dimensions(op, decoder.info().dimensions)
1698 })
1699 .map(|plan| (plan, inputs.len()))
1700 } else {
1701 Self::rgb8_metal_batch_requests(inputs, |decoder| {
1702 Self::rgb8_batch_op_and_dimensions(op, decoder.info().dimensions).0
1703 })
1704 .map(|requests| {
1705 (
1706 Rgb8MetalBatchPlan {
1707 requests,
1708 output_dimensions: None,
1709 },
1710 inputs.len(),
1711 )
1712 })
1713 }
1714 }
1715 Rgb8MetalBatchSource::Decoders(decoders) => {
1716 Self::rgb8_metal_decoder_batch_requests_with_output_dimensions(
1717 decoders,
1718 |decoder| {
1719 Self::rgb8_batch_op_and_dimensions(op, decoder.inner().info().dimensions)
1720 },
1721 )
1722 .map(|plan| (plan, decoders.len()))
1723 }
1724 }
1725 }
1726
1727 #[cfg(target_os = "macos")]
1728 const fn rgb8_buffer_batch_unsupported_reason(op: Rgb8MetalBatchOp) -> &'static str {
1729 match op {
1730 Rgb8MetalBatchOp::Full => {
1731 "JPEG Metal reusable batch output currently supports batchable full-tile RGB8 fast 4:2:0, 4:2:2, or 4:4:4 inputs"
1732 }
1733 Rgb8MetalBatchOp::Scaled(_) => {
1734 "JPEG Metal reusable scaled batch output currently supports batchable RGB8 fast 4:2:0, 4:2:2, or 4:4:4 inputs with half, quarter, or eighth scaling"
1735 }
1736 Rgb8MetalBatchOp::RegionScaled { .. } => {
1737 "JPEG Metal reusable region-scaled batch output currently supports batchable RGB8 fast 4:2:0, 4:2:2, or 4:4:4 inputs with matching output shapes"
1738 }
1739 }
1740 }
1741
1742 #[cfg(target_os = "macos")]
1743 const fn rgb8_texture_batch_unsupported_reason(op: Rgb8MetalBatchOp) -> &'static str {
1744 match op {
1745 Rgb8MetalBatchOp::Full => {
1746 "JPEG Metal texture batch output currently supports batchable full-tile RGB8 fast 4:2:0, 4:2:2, or 4:4:4 inputs"
1747 }
1748 Rgb8MetalBatchOp::Scaled(_) => {
1749 "JPEG Metal texture scaled batch output currently supports batchable RGB8 fast 4:2:0, 4:2:2, or 4:4:4 inputs with half, quarter, or eighth scaling"
1750 }
1751 Rgb8MetalBatchOp::RegionScaled { .. } => {
1752 "JPEG Metal texture region-scaled batch output currently supports batchable RGB8 fast 4:2:0, 4:2:2, or 4:4:4 inputs with matching output shapes"
1753 }
1754 }
1755 }
1756
1757 #[cfg(target_os = "macos")]
1758 pub fn decode_rgb8_batch_into_buffer_with_session(
1765 request: Rgb8MetalBatchRequest<'_, '_>,
1766 target: MetalBufferBatchTarget<'_>,
1767 session: &MetalBackendSession,
1768 ) -> Result<Vec<Result<Surface, Error>>, Error> {
1769 if request.source.is_empty() {
1770 return Ok(Vec::new());
1771 }
1772
1773 let resizable = matches!(target, MetalBufferBatchTarget::Resizable(_));
1774 let (plan, tile_count) =
1775 Self::plan_rgb8_metal_batch(request.source, request.op, resizable)?;
1776 let output: &MetalBatchOutputBuffer = match target {
1777 MetalBufferBatchTarget::Reusable(output) => output,
1778 MetalBufferBatchTarget::Resizable(output) => {
1779 if let Rgb8MetalBatchSource::Decoders(decoders) = request.source {
1780 let report = Self::inspect_rgb8_decoder_batch_metal_output(
1781 decoders,
1782 Self::rgb8_batch_jpeg_decode_op(request.op),
1783 );
1784 output.ensure_rgb8_batch_report(session, &report)?;
1785 }
1786 let Some(output_dimensions) = plan.output_dimensions else {
1787 return Ok(Vec::new());
1788 };
1789 output.ensure_rgb8_tiles(session, output_dimensions, tile_count)?;
1790 output
1791 }
1792 };
1793
1794 let results = match request.op {
1795 Rgb8MetalBatchOp::Full => compute::decode_full_rgb8_batch_into_output_with_session(
1796 &plan.requests,
1797 output,
1798 session,
1799 )?,
1800 Rgb8MetalBatchOp::Scaled(_) | Rgb8MetalBatchOp::RegionScaled { .. } => {
1801 compute::decode_region_scaled_rgb8_batch_into_output_with_session(
1802 &plan.requests,
1803 output,
1804 session,
1805 )?
1806 }
1807 };
1808 results.ok_or(Error::UnsupportedMetalRequest {
1809 reason: Self::rgb8_buffer_batch_unsupported_reason(request.op),
1810 })
1811 }
1812
1813 #[cfg(target_os = "macos")]
1814 pub fn decode_rgb8_batch_into_textures_with_session(
1821 request: Rgb8MetalBatchRequest<'_, '_>,
1822 target: MetalTextureBatchTarget<'_>,
1823 session: &MetalBackendSession,
1824 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
1825 if request.source.is_empty() {
1826 return Ok(Vec::new());
1827 }
1828
1829 let resizable = matches!(target, MetalTextureBatchTarget::Resizable(_));
1830 let (plan, tile_count) =
1831 Self::plan_rgb8_metal_batch(request.source, request.op, resizable)?;
1832 let output: &MetalBatchTextureOutput = match target {
1833 MetalTextureBatchTarget::Reusable(output) => output,
1834 MetalTextureBatchTarget::Resizable(output) => {
1835 if let Rgb8MetalBatchSource::Decoders(decoders) = request.source {
1836 let report = Self::inspect_rgb8_decoder_batch_metal_output(
1837 decoders,
1838 Self::rgb8_batch_jpeg_decode_op(request.op),
1839 );
1840 output.ensure_rgba8_batch_report(session, &report)?;
1841 }
1842 let Some(output_dimensions) = plan.output_dimensions else {
1843 return Ok(Vec::new());
1844 };
1845 output.ensure_rgba8_tiles(session, output_dimensions, tile_count)?;
1846 output
1847 }
1848 };
1849
1850 let results = match request.op {
1851 Rgb8MetalBatchOp::Full => compute::decode_full_rgb8_batch_into_textures_with_session(
1852 &plan.requests,
1853 output,
1854 session,
1855 )?,
1856 Rgb8MetalBatchOp::Scaled(_) | Rgb8MetalBatchOp::RegionScaled { .. } => {
1857 compute::decode_region_scaled_rgb8_batch_into_textures_with_session(
1858 &plan.requests,
1859 output,
1860 session,
1861 )?
1862 }
1863 };
1864 results.ok_or(Error::UnsupportedMetalRequest {
1865 reason: Self::rgb8_texture_batch_unsupported_reason(request.op),
1866 })
1867 }
1868
1869 #[cfg(target_os = "macos")]
1870 pub fn decode_rgb8_decoder_batch_into_resizable_metal_textures_with_session(
1876 decoders: &[&Decoder<'_>],
1877 output: &mut MetalBatchTextureOutput,
1878 session: &MetalBackendSession,
1879 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
1880 Self::decode_rgb8_batch_into_textures_with_session(
1881 Rgb8MetalBatchRequest {
1882 source: Rgb8MetalBatchSource::Decoders(decoders),
1883 op: Rgb8MetalBatchOp::Full,
1884 },
1885 MetalTextureBatchTarget::Resizable(output),
1886 session,
1887 )
1888 }
1889
1890 #[cfg(target_os = "macos")]
1891 pub fn decode_rgb8_region_scaled_batch_into_resizable_metal_buffer_with_session(
1897 inputs: &[&[u8]],
1898 roi: Rect,
1899 scale: Downscale,
1900 output: &mut MetalBatchOutputBuffer,
1901 session: &MetalBackendSession,
1902 ) -> Result<Vec<Result<Surface, Error>>, Error> {
1903 Self::decode_rgb8_batch_into_buffer_with_session(
1904 Rgb8MetalBatchRequest {
1905 source: Rgb8MetalBatchSource::Bytes(inputs),
1906 op: Rgb8MetalBatchOp::RegionScaled { roi, scale },
1907 },
1908 MetalBufferBatchTarget::Resizable(output),
1909 session,
1910 )
1911 }
1912
1913 #[cfg(target_os = "macos")]
1914 pub fn decode_rgb8_region_scaled_batch_into_resizable_metal_textures_with_session(
1920 inputs: &[&[u8]],
1921 roi: Rect,
1922 scale: Downscale,
1923 output: &mut MetalBatchTextureOutput,
1924 session: &MetalBackendSession,
1925 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
1926 Self::decode_rgb8_batch_into_textures_with_session(
1927 Rgb8MetalBatchRequest {
1928 source: Rgb8MetalBatchSource::Bytes(inputs),
1929 op: Rgb8MetalBatchOp::RegionScaled { roi, scale },
1930 },
1931 MetalTextureBatchTarget::Resizable(output),
1932 session,
1933 )
1934 }
1935
1936 #[allow(clippy::too_many_arguments)]
1937 pub fn submit_tile_region_scaled_to_device(
1939 ctx: &mut j2k_core::DecoderContext<CpuDecoderContext>,
1940 session: &mut MetalSession,
1941 pool: &mut CpuScratchPool,
1942 input: &[u8],
1943 fmt: PixelFormat,
1944 roi: Rect,
1945 scale: Downscale,
1946 backend: BackendRequest,
1947 ) -> Result<<Self as TileBatchDecodeSubmit>::SubmittedSurface, Error> {
1948 let _ = (ctx, pool);
1949 let slot = {
1950 let mut state = session.shared.lock()?;
1951 let input = state.intern_input_slice(input);
1952 let (fast444_packet, fast422_packet, fast420_packet) =
1953 state.resolve_fast_packets(&input, backend);
1954 state.queue_request(batch::QueuedRequest::new_shared(
1955 input,
1956 fmt,
1957 backend,
1958 batch::BatchOp::RegionScaled { roi, scale },
1959 fast444_packet,
1960 fast422_packet,
1961 fast420_packet,
1962 ))
1963 };
1964 Ok(batch::MetalSubmission {
1965 session: session.shared.clone(),
1966 slot,
1967 })
1968 }
1969}
1970
1971impl<'a> ImageDecodeSubmit<'a> for Decoder<'a> {
1972 type Session = MetalSession;
1973 type DeviceSurface = Surface;
1974 type SubmittedSurface = batch::MetalSubmission;
1975
1976 fn submit_to_device(
1977 &mut self,
1978 session: &mut Self::Session,
1979 fmt: PixelFormat,
1980 backend: BackendRequest,
1981 ) -> Result<Self::SubmittedSurface, Self::Error> {
1982 let fast444_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
1983 self.fast444_packet.clone()
1984 } else {
1985 None
1986 };
1987 let fast422_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
1988 self.fast422_packet.clone()
1989 } else {
1990 None
1991 };
1992 let fast420_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
1993 self.fast420_packet.clone()
1994 } else {
1995 None
1996 };
1997 let slot = session
1998 .shared
1999 .lock()?
2000 .queue_request(batch::QueuedRequest::new_shared(
2001 Arc::clone(&self.source),
2002 fmt,
2003 backend,
2004 batch::BatchOp::Full,
2005 fast444_packet,
2006 fast422_packet,
2007 fast420_packet,
2008 ));
2009 Ok(batch::MetalSubmission {
2010 session: session.shared.clone(),
2011 slot,
2012 })
2013 }
2014
2015 fn submit_region_to_device(
2016 &mut self,
2017 session: &mut Self::Session,
2018 fmt: PixelFormat,
2019 roi: Rect,
2020 backend: BackendRequest,
2021 ) -> Result<Self::SubmittedSurface, Self::Error> {
2022 let fast444_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
2023 self.fast444_packet.clone()
2024 } else {
2025 None
2026 };
2027 let fast422_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
2028 self.fast422_packet.clone()
2029 } else {
2030 None
2031 };
2032 let fast420_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
2033 self.fast420_packet.clone()
2034 } else {
2035 None
2036 };
2037 let slot = session
2038 .shared
2039 .lock()?
2040 .queue_request(batch::QueuedRequest::new_shared(
2041 Arc::clone(&self.source),
2042 fmt,
2043 backend,
2044 batch::BatchOp::Region(roi),
2045 fast444_packet,
2046 fast422_packet,
2047 fast420_packet,
2048 ));
2049 Ok(batch::MetalSubmission {
2050 session: session.shared.clone(),
2051 slot,
2052 })
2053 }
2054
2055 fn submit_scaled_to_device(
2056 &mut self,
2057 session: &mut Self::Session,
2058 fmt: PixelFormat,
2059 scale: Downscale,
2060 backend: BackendRequest,
2061 ) -> Result<Self::SubmittedSurface, Self::Error> {
2062 let fast444_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
2063 self.fast444_packet.clone()
2064 } else {
2065 None
2066 };
2067 let fast422_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
2068 self.fast422_packet.clone()
2069 } else {
2070 None
2071 };
2072 let fast420_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
2073 self.fast420_packet.clone()
2074 } else {
2075 None
2076 };
2077 let slot = session
2078 .shared
2079 .lock()?
2080 .queue_request(batch::QueuedRequest::new_shared(
2081 Arc::clone(&self.source),
2082 fmt,
2083 backend,
2084 batch::BatchOp::Scaled(scale),
2085 fast444_packet,
2086 fast422_packet,
2087 fast420_packet,
2088 ));
2089 Ok(batch::MetalSubmission {
2090 session: session.shared.clone(),
2091 slot,
2092 })
2093 }
2094
2095 fn submit_region_scaled_to_device(
2096 &mut self,
2097 session: &mut Self::Session,
2098 fmt: PixelFormat,
2099 roi: Rect,
2100 scale: Downscale,
2101 backend: BackendRequest,
2102 ) -> Result<Self::SubmittedSurface, Self::Error> {
2103 let fast444_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
2104 self.fast444_packet.clone()
2105 } else {
2106 None
2107 };
2108 let fast422_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
2109 self.fast422_packet.clone()
2110 } else {
2111 None
2112 };
2113 let fast420_packet = if matches!(backend, BackendRequest::Auto | BackendRequest::Metal) {
2114 self.fast420_packet.clone()
2115 } else {
2116 None
2117 };
2118 let slot = session
2119 .shared
2120 .lock()?
2121 .queue_request(batch::QueuedRequest::new_shared(
2122 Arc::clone(&self.source),
2123 fmt,
2124 backend,
2125 batch::BatchOp::RegionScaled { roi, scale },
2126 fast444_packet,
2127 fast422_packet,
2128 fast420_packet,
2129 ));
2130 Ok(batch::MetalSubmission {
2131 session: session.shared.clone(),
2132 slot,
2133 })
2134 }
2135}
2136
2137impl TileBatchDecodeSubmit for Codec {
2138 type Context = CpuDecoderContext;
2139 type Session = MetalSession;
2140 type DeviceSurface = Surface;
2141 type SubmittedSurface = batch::MetalSubmission;
2142
2143 fn submit_tile_to_device(
2144 ctx: &mut j2k_core::DecoderContext<Self::Context>,
2145 session: &mut Self::Session,
2146 pool: &mut Self::Pool,
2147 input: &[u8],
2148 fmt: PixelFormat,
2149 backend: BackendRequest,
2150 ) -> Result<Self::SubmittedSurface, Self::Error> {
2151 let _ = (ctx, pool);
2152 let slot = {
2153 let mut state = session.shared.lock()?;
2154 let input = state.intern_input_slice(input);
2155 let (fast444_packet, fast422_packet, fast420_packet) =
2156 state.resolve_fast_packets(&input, backend);
2157 state.queue_request(batch::QueuedRequest::new_shared(
2158 input,
2159 fmt,
2160 backend,
2161 batch::BatchOp::Full,
2162 fast444_packet,
2163 fast422_packet,
2164 fast420_packet,
2165 ))
2166 };
2167 Ok(batch::MetalSubmission {
2168 session: session.shared.clone(),
2169 slot,
2170 })
2171 }
2172
2173 fn submit_tile_region_to_device(
2174 ctx: &mut j2k_core::DecoderContext<Self::Context>,
2175 session: &mut Self::Session,
2176 pool: &mut Self::Pool,
2177 input: &[u8],
2178 fmt: PixelFormat,
2179 roi: Rect,
2180 backend: BackendRequest,
2181 ) -> Result<Self::SubmittedSurface, Self::Error> {
2182 let _ = (ctx, pool);
2183 let slot = {
2184 let mut state = session.shared.lock()?;
2185 let input = state.intern_input_slice(input);
2186 let (fast444_packet, fast422_packet, fast420_packet) =
2187 state.resolve_fast_packets(&input, backend);
2188 state.queue_request(batch::QueuedRequest::new_shared(
2189 input,
2190 fmt,
2191 backend,
2192 batch::BatchOp::Region(roi),
2193 fast444_packet,
2194 fast422_packet,
2195 fast420_packet,
2196 ))
2197 };
2198 Ok(batch::MetalSubmission {
2199 session: session.shared.clone(),
2200 slot,
2201 })
2202 }
2203
2204 fn submit_tile_scaled_to_device(
2205 ctx: &mut j2k_core::DecoderContext<Self::Context>,
2206 session: &mut Self::Session,
2207 pool: &mut Self::Pool,
2208 input: &[u8],
2209 fmt: PixelFormat,
2210 scale: Downscale,
2211 backend: BackendRequest,
2212 ) -> Result<Self::SubmittedSurface, Self::Error> {
2213 let _ = (ctx, pool);
2214 let slot = {
2215 let mut state = session.shared.lock()?;
2216 let input = state.intern_input_slice(input);
2217 let (fast444_packet, fast422_packet, fast420_packet) =
2218 state.resolve_fast_packets(&input, backend);
2219 state.queue_request(batch::QueuedRequest::new_shared(
2220 input,
2221 fmt,
2222 backend,
2223 batch::BatchOp::Scaled(scale),
2224 fast444_packet,
2225 fast422_packet,
2226 fast420_packet,
2227 ))
2228 };
2229 Ok(batch::MetalSubmission {
2230 session: session.shared.clone(),
2231 slot,
2232 })
2233 }
2234
2235 fn submit_tile_region_scaled_to_device(
2236 ctx: &mut j2k_core::DecoderContext<Self::Context>,
2237 session: &mut Self::Session,
2238 pool: &mut Self::Pool,
2239 input: &[u8],
2240 fmt: PixelFormat,
2241 roi: Rect,
2242 scale: Downscale,
2243 backend: BackendRequest,
2244 ) -> Result<Self::SubmittedSurface, Self::Error> {
2245 Codec::submit_tile_region_scaled_to_device(
2246 ctx, session, pool, input, fmt, roi, scale, backend,
2247 )
2248 }
2249}
2250
2251impl TileBatchDecodeDevice for Codec {
2252 type Context = CpuDecoderContext;
2253 type DeviceSurface = Surface;
2254}
2255
2256impl TileBatchDecodeManyDevice for Codec {
2257 type Context = CpuDecoderContext;
2258 type DeviceSurface = Surface;
2259
2260 fn decode_tiles_to_device(
2261 ctx: &mut j2k_core::DecoderContext<Self::Context>,
2262 pool: &mut Self::Pool,
2263 inputs: &[&[u8]],
2264 fmt: PixelFormat,
2265 backend: BackendRequest,
2266 ) -> Result<Vec<Self::DeviceSurface>, Self::Error> {
2267 if inputs.is_empty() {
2268 return Ok(Vec::new());
2269 }
2270
2271 let mut session = MetalSession::default();
2272 let submissions = inputs
2273 .iter()
2274 .map(|input| {
2275 <Self as TileBatchDecodeSubmit>::submit_tile_to_device(
2276 ctx,
2277 &mut session,
2278 pool,
2279 input,
2280 fmt,
2281 backend,
2282 )
2283 })
2284 .collect::<Result<Vec<_>, _>>()?;
2285
2286 submissions
2287 .into_iter()
2288 .map(DeviceSubmission::wait)
2289 .collect()
2290 }
2291}
2292
2293pub(crate) fn decode_surface_from_bytes(
2294 input: &[u8],
2295 fmt: PixelFormat,
2296 backend: BackendRequest,
2297 op: batch::BatchOp,
2298 fast444_packet: Option<Arc<JpegFast444PacketV1>>,
2299 fast422_packet: Option<Arc<JpegFast422PacketV1>>,
2300 fast420_packet: Option<Arc<JpegFast420PacketV1>>,
2301) -> Result<Surface, Error> {
2302 let decoder = CpuDecoder::new(input)?;
2303 let mut pool = CpuScratchPool::new();
2304 let build_auto_packets =
2305 matches!(backend, BackendRequest::Auto) && decoder.info().restart_interval.is_some();
2306 let build_metal_packets = matches!(backend, BackendRequest::Metal);
2307 let fast444_packet = if build_auto_packets || build_metal_packets {
2308 fast444_packet.or_else(|| {
2309 build_fast444_packet_for_decoder(&decoder)
2310 .ok()
2311 .map(Arc::new)
2312 })
2313 } else {
2314 None
2315 };
2316 let fast422_packet = if build_auto_packets || build_metal_packets {
2317 fast422_packet.or_else(|| {
2318 build_fast422_packet_for_decoder(&decoder)
2319 .ok()
2320 .map(Arc::new)
2321 })
2322 } else {
2323 None
2324 };
2325 let fast420_packet = if build_auto_packets || build_metal_packets {
2326 fast420_packet.or_else(|| {
2327 build_fast420_packet_for_decoder(&decoder)
2328 .ok()
2329 .map(Arc::new)
2330 })
2331 } else {
2332 None
2333 };
2334 decode_surface_from_decoder(
2335 &decoder,
2336 &mut pool,
2337 fmt,
2338 backend,
2339 op,
2340 fast444_packet.as_deref(),
2341 fast422_packet.as_deref(),
2342 fast420_packet.as_deref(),
2343 )
2344}
2345
2346#[cfg(not(target_os = "macos"))]
2347#[allow(clippy::unnecessary_wraps)]
2348pub(crate) fn decode_compatible_batch(
2349 requests: &[batch::QueuedRequest],
2350) -> Result<Option<Vec<Result<Surface, Error>>>, Error> {
2351 let _ = requests;
2352 Ok(None)
2353}
2354
2355#[allow(clippy::unnecessary_wraps)]
2356pub(crate) fn decode_compatible_batch_with_session(
2357 requests: &[batch::QueuedRequest],
2358 session: &mut session::SessionState,
2359) -> Result<Option<Vec<Result<Surface, Error>>>, Error> {
2360 #[cfg(target_os = "macos")]
2361 {
2362 compute::decode_full_batch_to_surfaces_with_session_state(requests, session)
2363 }
2364 #[cfg(not(target_os = "macos"))]
2365 {
2366 let _ = session;
2367 decode_compatible_batch(requests)
2368 }
2369}
2370
2371#[cfg(target_os = "macos")]
2372#[doc(hidden)]
2373pub fn decode_rgb8_batch_to_device_with_session(
2374 inputs: &[&[u8]],
2375 session: &MetalBackendSession,
2376) -> Result<Option<Vec<Result<Surface, Error>>>, Error> {
2377 if inputs.len() < 2 {
2378 return Ok(None);
2379 }
2380
2381 let mut state = session::SessionState::default();
2382 let mut requests = Vec::with_capacity(inputs.len());
2383 for input in inputs {
2384 let input = state.intern_input_slice(input);
2385 let (fast444_packet, fast422_packet, fast420_packet) =
2386 state.resolve_fast_packets(&input, BackendRequest::Metal);
2387 requests.push(batch::QueuedRequest::new_shared(
2388 input,
2389 PixelFormat::Rgb8,
2390 BackendRequest::Metal,
2391 batch::BatchOp::Full,
2392 fast444_packet,
2393 fast422_packet,
2394 fast420_packet,
2395 ));
2396 }
2397
2398 compute::decode_full_batch_to_surfaces_with_session(&requests, session)
2399}
2400
2401#[allow(clippy::too_many_arguments)]
2402fn decode_surface_from_decoder(
2403 decoder: &CpuDecoder<'_>,
2404 pool: &mut CpuScratchPool,
2405 fmt: PixelFormat,
2406 backend: BackendRequest,
2407 op: batch::BatchOp,
2408 fast444_packet: Option<&JpegFast444PacketV1>,
2409 fast422_packet: Option<&JpegFast422PacketV1>,
2410 fast420_packet: Option<&JpegFast420PacketV1>,
2411) -> Result<Surface, Error> {
2412 match op {
2413 batch::BatchOp::Full => match backend {
2414 BackendRequest::Cpu => decode_full_cpu_upload(decoder, pool, fmt),
2415 BackendRequest::Auto | BackendRequest::Metal => {
2416 let decision = choose_route(
2417 decoder,
2418 backend,
2419 fmt,
2420 op,
2421 fast444_packet,
2422 fast422_packet,
2423 fast420_packet,
2424 );
2425 if let Some(err) = routing::decision_error(decision) {
2426 return Err(err);
2427 }
2428 match decision {
2429 routing::RouteDecision::CpuHost => decode_full_cpu_upload(decoder, pool, fmt),
2430 routing::RouteDecision::MetalKernel => {
2431 #[cfg(target_os = "macos")]
2432 {
2433 reject_cpu_staged_metal_upload(compute::decode_to_surface(
2434 decoder,
2435 pool,
2436 fmt,
2437 fast444_packet,
2438 fast422_packet,
2439 fast420_packet,
2440 )?)
2441 }
2442 #[cfg(not(target_os = "macos"))]
2443 {
2444 let _ = (
2445 decoder,
2446 pool,
2447 fmt,
2448 fast444_packet,
2449 fast422_packet,
2450 fast420_packet,
2451 );
2452 Err(Error::MetalUnavailable)
2453 }
2454 }
2455 routing::RouteDecision::RejectExplicitMetal { .. }
2456 | routing::RouteDecision::RejectUnsupportedBackend { .. }
2457 | routing::RouteDecision::MetalUnavailable => unreachable!("handled above"),
2458 }
2459 }
2460 BackendRequest::Cuda => Err(Error::UnsupportedBackend { request: backend }),
2461 },
2462 batch::BatchOp::Region(roi) => match backend {
2463 BackendRequest::Cpu => decode_region_cpu_upload(decoder, pool, fmt, roi),
2464 BackendRequest::Auto | BackendRequest::Metal => {
2465 let decision = choose_route(
2466 decoder,
2467 backend,
2468 fmt,
2469 op,
2470 fast444_packet,
2471 fast422_packet,
2472 fast420_packet,
2473 );
2474 if let Some(err) = routing::decision_error(decision) {
2475 return Err(err);
2476 }
2477 match decision {
2478 routing::RouteDecision::CpuHost => {
2479 decode_region_cpu_upload(decoder, pool, fmt, roi)
2480 }
2481 routing::RouteDecision::MetalKernel => {
2482 #[cfg(target_os = "macos")]
2483 {
2484 reject_cpu_staged_metal_upload(compute::decode_region_to_surface(
2485 decoder,
2486 pool,
2487 fmt,
2488 roi.into(),
2489 fast444_packet,
2490 fast422_packet,
2491 fast420_packet,
2492 )?)
2493 }
2494 #[cfg(not(target_os = "macos"))]
2495 {
2496 let _ = (
2497 decoder,
2498 pool,
2499 fmt,
2500 roi,
2501 fast444_packet,
2502 fast422_packet,
2503 fast420_packet,
2504 );
2505 Err(Error::MetalUnavailable)
2506 }
2507 }
2508 routing::RouteDecision::RejectExplicitMetal { .. }
2509 | routing::RouteDecision::RejectUnsupportedBackend { .. }
2510 | routing::RouteDecision::MetalUnavailable => unreachable!("handled above"),
2511 }
2512 }
2513 BackendRequest::Cuda => Err(Error::UnsupportedBackend { request: backend }),
2514 },
2515 batch::BatchOp::Scaled(scale) => match backend {
2516 BackendRequest::Cpu => decode_scaled_cpu_upload(decoder, pool, fmt, scale),
2517 BackendRequest::Auto | BackendRequest::Metal => {
2518 let decision = choose_route(
2519 decoder,
2520 backend,
2521 fmt,
2522 op,
2523 fast444_packet,
2524 fast422_packet,
2525 fast420_packet,
2526 );
2527 if let Some(err) = routing::decision_error(decision) {
2528 return Err(err);
2529 }
2530 match decision {
2531 routing::RouteDecision::CpuHost => {
2532 decode_scaled_cpu_upload(decoder, pool, fmt, scale)
2533 }
2534 routing::RouteDecision::MetalKernel => {
2535 #[cfg(target_os = "macos")]
2536 {
2537 reject_cpu_staged_metal_upload(compute::decode_scaled_to_surface(
2538 decoder,
2539 pool,
2540 fmt,
2541 scale,
2542 fast444_packet,
2543 fast422_packet,
2544 fast420_packet,
2545 )?)
2546 }
2547 #[cfg(not(target_os = "macos"))]
2548 {
2549 let _ = (
2550 decoder,
2551 pool,
2552 fmt,
2553 scale,
2554 fast444_packet,
2555 fast422_packet,
2556 fast420_packet,
2557 );
2558 Err(Error::MetalUnavailable)
2559 }
2560 }
2561 routing::RouteDecision::RejectExplicitMetal { .. }
2562 | routing::RouteDecision::RejectUnsupportedBackend { .. }
2563 | routing::RouteDecision::MetalUnavailable => unreachable!("handled above"),
2564 }
2565 }
2566 BackendRequest::Cuda => Err(Error::UnsupportedBackend { request: backend }),
2567 },
2568 batch::BatchOp::RegionScaled { roi, scale } => decode_region_scaled_surface_from_decoder(
2569 decoder,
2570 pool,
2571 fmt,
2572 roi,
2573 scale,
2574 backend,
2575 fast444_packet,
2576 fast422_packet,
2577 fast420_packet,
2578 ),
2579 }
2580}
2581
2582fn decode_full_cpu_upload(
2583 decoder: &CpuDecoder<'_>,
2584 pool: &mut CpuScratchPool,
2585 fmt: PixelFormat,
2586) -> Result<Surface, Error> {
2587 let dims = decoder.info().dimensions;
2588 let stride = dims.0 as usize * fmt.bytes_per_pixel();
2589 let mut out = vec![0u8; stride * dims.1 as usize];
2590 decoder.decode_into_with_scratch(pool, &mut out, stride, fmt)?;
2591 upload_surface(out, dims, fmt, BackendRequest::Cpu)
2592}
2593
2594fn decode_region_cpu_upload(
2595 decoder: &CpuDecoder<'_>,
2596 pool: &mut CpuScratchPool,
2597 fmt: PixelFormat,
2598 roi: Rect,
2599) -> Result<Surface, Error> {
2600 let dims = (roi.w, roi.h);
2601 let stride = dims.0 as usize * fmt.bytes_per_pixel();
2602 let mut out = vec![0u8; stride * dims.1 as usize];
2603 decoder.decode_region_into_with_scratch(pool, &mut out, stride, fmt, roi.into())?;
2604 upload_surface(out, dims, fmt, BackendRequest::Cpu)
2605}
2606
2607fn decode_scaled_cpu_upload(
2608 decoder: &CpuDecoder<'_>,
2609 pool: &mut CpuScratchPool,
2610 fmt: PixelFormat,
2611 scale: Downscale,
2612) -> Result<Surface, Error> {
2613 let dims = scaled_dims(decoder.info().dimensions, scale);
2614 let stride = dims.0 as usize * fmt.bytes_per_pixel();
2615 let mut out = vec![0u8; stride * dims.1 as usize];
2616 decoder.decode_scaled_into_with_scratch(pool, &mut out, stride, fmt, scale)?;
2617 upload_surface(out, dims, fmt, BackendRequest::Cpu)
2618}
2619
2620#[allow(clippy::too_many_arguments)]
2621fn decode_region_scaled_surface_from_decoder(
2622 decoder: &CpuDecoder<'_>,
2623 pool: &mut CpuScratchPool,
2624 fmt: PixelFormat,
2625 roi: Rect,
2626 scale: Downscale,
2627 backend: BackendRequest,
2628 fast444_packet: Option<&JpegFast444PacketV1>,
2629 fast422_packet: Option<&JpegFast422PacketV1>,
2630 fast420_packet: Option<&JpegFast420PacketV1>,
2631) -> Result<Surface, Error> {
2632 match backend {
2633 BackendRequest::Cpu => {
2634 decode_region_scaled_cpu_upload(decoder, pool, fmt, roi, scale, BackendRequest::Cpu)
2635 }
2636 BackendRequest::Auto | BackendRequest::Metal => {
2637 let decision = choose_route(
2638 decoder,
2639 backend,
2640 fmt,
2641 batch::BatchOp::RegionScaled { roi, scale },
2642 fast444_packet,
2643 fast422_packet,
2644 fast420_packet,
2645 );
2646 if let Some(err) = routing::decision_error(decision) {
2647 return Err(err);
2648 }
2649 match decision {
2650 routing::RouteDecision::CpuHost => decode_region_scaled_cpu_upload(
2651 decoder,
2652 pool,
2653 fmt,
2654 roi,
2655 scale,
2656 BackendRequest::Cpu,
2657 ),
2658 routing::RouteDecision::MetalKernel => {
2659 #[cfg(target_os = "macos")]
2660 {
2661 reject_cpu_staged_metal_upload(compute::decode_region_scaled_to_surface(
2662 decoder,
2663 pool,
2664 fmt,
2665 roi.into(),
2666 scale,
2667 fast444_packet,
2668 fast422_packet,
2669 fast420_packet,
2670 )?)
2671 }
2672 #[cfg(not(target_os = "macos"))]
2673 {
2674 let _ = (
2675 decoder,
2676 pool,
2677 fmt,
2678 roi,
2679 scale,
2680 fast444_packet,
2681 fast422_packet,
2682 fast420_packet,
2683 );
2684 Err(Error::MetalUnavailable)
2685 }
2686 }
2687 routing::RouteDecision::RejectExplicitMetal { .. }
2688 | routing::RouteDecision::RejectUnsupportedBackend { .. }
2689 | routing::RouteDecision::MetalUnavailable => unreachable!("handled above"),
2690 }
2691 }
2692 BackendRequest::Cuda => Err(Error::UnsupportedBackend { request: backend }),
2693 }
2694}
2695
2696#[cfg(target_os = "macos")]
2697fn reject_cpu_staged_metal_upload(surface: Surface) -> Result<Surface, Error> {
2698 if surface.residency() == SurfaceResidency::CpuStagedMetalUpload {
2699 return Err(Error::UnsupportedMetalRequest {
2700 reason: "JPEG Metal explicit device decode requires a direct resident Metal decode; use the CPU path for CPU-staged output",
2701 });
2702 }
2703 Ok(surface)
2704}
2705
2706#[allow(clippy::too_many_arguments)]
2707fn choose_route(
2708 decoder: &CpuDecoder<'_>,
2709 backend: BackendRequest,
2710 fmt: PixelFormat,
2711 op: batch::BatchOp,
2712 fast444_packet: Option<&JpegFast444PacketV1>,
2713 fast422_packet: Option<&JpegFast422PacketV1>,
2714 fast420_packet: Option<&JpegFast420PacketV1>,
2715) -> routing::RouteDecision {
2716 let capabilities = routing::JpegMetalCapabilities::for_request(
2717 decoder,
2718 fmt,
2719 op,
2720 fast444_packet,
2721 fast422_packet,
2722 fast420_packet,
2723 );
2724 let decision = routing::decide_route(backend, capabilities);
2725 if j2k_profile::gpu_route_profile_enabled() {
2726 let request_s = format!("{backend:?}");
2727 let fmt_s = format!("{fmt:?}");
2728 let has_fast_packet_s = capabilities.has_fast_packet().to_string();
2729 let supports_format_s = capabilities.supports_output_format().to_string();
2730 let labels = jpeg_route_decision_profile(decision);
2731 j2k_profile::emit_gpu_route_profile(
2732 "jpeg",
2733 "metal",
2734 &[
2735 ("request", request_s.as_str()),
2736 ("fmt", fmt_s.as_str()),
2737 ("op", jpeg_batch_op_profile(op)),
2738 ("has_fast_packet", has_fast_packet_s.as_str()),
2739 ("supports_output_format", supports_format_s.as_str()),
2740 ("decision", labels.decision),
2741 ("reason", labels.reason),
2742 ],
2743 );
2744 }
2745 decision
2746}
2747
2748fn jpeg_batch_op_profile(op: batch::BatchOp) -> &'static str {
2749 match op {
2750 batch::BatchOp::Full => "full",
2751 batch::BatchOp::Region(_) => "region",
2752 batch::BatchOp::Scaled(_) => "scaled",
2753 batch::BatchOp::RegionScaled { .. } => "region_scaled",
2754 }
2755}
2756
2757fn jpeg_route_decision_profile(decision: routing::RouteDecision) -> MetalRouteProfileLabels {
2758 match decision {
2759 routing::RouteDecision::CpuHost => cpu_host_route(),
2760 routing::RouteDecision::MetalKernel => metal_kernel_route(),
2761 routing::RouteDecision::RejectExplicitMetal { reason } => {
2762 let reason_code = if reason.contains("fast") {
2763 "no_fast_packet"
2764 } else {
2765 "unsupported_format"
2766 };
2767 reject_explicit_metal_route(reason_code)
2768 }
2769 routing::RouteDecision::RejectUnsupportedBackend { .. } => {
2770 reject_unsupported_backend_route()
2771 }
2772 routing::RouteDecision::MetalUnavailable => metal_unavailable_route(),
2773 }
2774}
2775
2776fn decode_region_scaled_cpu_upload(
2777 decoder: &CpuDecoder<'_>,
2778 pool: &mut CpuScratchPool,
2779 fmt: PixelFormat,
2780 roi: Rect,
2781 scale: Downscale,
2782 backend: BackendRequest,
2783) -> Result<Surface, Error> {
2784 let scaled = roi.scaled_covering(scale);
2785 let dims = (scaled.w, scaled.h);
2786 let stride = dims.0 as usize * fmt.bytes_per_pixel();
2787 let mut out = vec![0u8; stride * dims.1 as usize];
2788 decoder.decode_region_scaled_into_with_scratch(
2789 pool,
2790 &mut out,
2791 stride,
2792 fmt,
2793 roi.into(),
2794 scale,
2795 )?;
2796 upload_surface(out, dims, fmt, backend)
2797}
2798
2799fn scaled_dims(full: (u32, u32), scale: Downscale) -> (u32, u32) {
2800 (
2801 full.0.div_ceil(scale.denominator()),
2802 full.1.div_ceil(scale.denominator()),
2803 )
2804}
2805
2806pub(crate) fn upload_surface(
2807 bytes: Vec<u8>,
2808 dimensions: (u32, u32),
2809 fmt: PixelFormat,
2810 backend: BackendRequest,
2811) -> Result<Surface, Error> {
2812 let pitch_bytes = dimensions.0 as usize * fmt.bytes_per_pixel();
2813 match backend {
2814 BackendRequest::Cpu => Ok(Surface {
2815 backend: BackendKind::Cpu,
2816 residency: SurfaceResidency::Host,
2817 dimensions,
2818 fmt,
2819 pitch_bytes,
2820 storage: Storage::Host(bytes),
2821 }),
2822 BackendRequest::Auto | BackendRequest::Metal => {
2823 #[cfg(target_os = "macos")]
2824 {
2825 let device = Device::system_default().ok_or(Error::MetalUnavailable)?;
2826 let buffer = device.new_buffer_with_data(
2827 bytes.as_ptr().cast(),
2828 bytes.len() as u64,
2829 MTLResourceOptions::StorageModeShared,
2830 );
2831 Ok(Surface {
2832 backend: BackendKind::Metal,
2833 residency: SurfaceResidency::CpuStagedMetalUpload,
2834 dimensions,
2835 fmt,
2836 pitch_bytes,
2837 storage: Storage::Metal { buffer, offset: 0 },
2838 })
2839 }
2840 #[cfg(not(target_os = "macos"))]
2841 {
2842 if matches!(backend, BackendRequest::Auto) {
2843 Ok(Surface {
2844 backend: BackendKind::Cpu,
2845 residency: SurfaceResidency::Host,
2846 dimensions,
2847 fmt,
2848 pitch_bytes,
2849 storage: Storage::Host(bytes),
2850 })
2851 } else {
2852 Err(Error::MetalUnavailable)
2853 }
2854 }
2855 }
2856 BackendRequest::Cuda => Err(Error::UnsupportedBackend { request: backend }),
2857 }
2858}
2859
2860pub use j2k_jpeg::{
2861 DecoderContext, Downscale as JpegDownscale, PixelFormat as JpegPixelFormat, ScratchPool,
2862};
2863pub use j2k_jpeg::{Info, Rect as JpegRectPublic};
2864
2865#[cfg(test)]
2866mod tests {
2867 use super::*;
2868
2869 #[cfg(target_os = "macos")]
2872 fn decode_rgb8_batch_into_metal_buffer_with_session(
2873 inputs: &[&[u8]],
2874 output: &MetalBatchOutputBuffer,
2875 session: &MetalBackendSession,
2876 ) -> Result<Vec<Result<Surface, Error>>, Error> {
2877 Codec::decode_rgb8_batch_into_buffer_with_session(
2878 Rgb8MetalBatchRequest {
2879 source: Rgb8MetalBatchSource::Bytes(inputs),
2880 op: Rgb8MetalBatchOp::Full,
2881 },
2882 MetalBufferBatchTarget::Reusable(output),
2883 session,
2884 )
2885 }
2886
2887 #[cfg(target_os = "macos")]
2888 fn decode_rgb8_batch_into_metal_textures_with_session(
2889 inputs: &[&[u8]],
2890 output: &MetalBatchTextureOutput,
2891 session: &MetalBackendSession,
2892 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
2893 Codec::decode_rgb8_batch_into_textures_with_session(
2894 Rgb8MetalBatchRequest {
2895 source: Rgb8MetalBatchSource::Bytes(inputs),
2896 op: Rgb8MetalBatchOp::Full,
2897 },
2898 MetalTextureBatchTarget::Reusable(output),
2899 session,
2900 )
2901 }
2902
2903 #[cfg(target_os = "macos")]
2904 fn decode_rgb8_decoder_batch_into_metal_buffer_with_session(
2905 decoders: &[&Decoder<'_>],
2906 output: &MetalBatchOutputBuffer,
2907 session: &MetalBackendSession,
2908 ) -> Result<Vec<Result<Surface, Error>>, Error> {
2909 Codec::decode_rgb8_batch_into_buffer_with_session(
2910 Rgb8MetalBatchRequest {
2911 source: Rgb8MetalBatchSource::Decoders(decoders),
2912 op: Rgb8MetalBatchOp::Full,
2913 },
2914 MetalBufferBatchTarget::Reusable(output),
2915 session,
2916 )
2917 }
2918
2919 #[cfg(target_os = "macos")]
2920 fn decode_rgb8_decoder_batch_into_metal_textures_with_session(
2921 decoders: &[&Decoder<'_>],
2922 output: &MetalBatchTextureOutput,
2923 session: &MetalBackendSession,
2924 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
2925 Codec::decode_rgb8_batch_into_textures_with_session(
2926 Rgb8MetalBatchRequest {
2927 source: Rgb8MetalBatchSource::Decoders(decoders),
2928 op: Rgb8MetalBatchOp::Full,
2929 },
2930 MetalTextureBatchTarget::Reusable(output),
2931 session,
2932 )
2933 }
2934
2935 #[cfg(target_os = "macos")]
2936 fn decode_rgb8_decoder_batch_into_resizable_metal_buffer_with_session(
2937 decoders: &[&Decoder<'_>],
2938 output: &mut MetalBatchOutputBuffer,
2939 session: &MetalBackendSession,
2940 ) -> Result<Vec<Result<Surface, Error>>, Error> {
2941 Codec::decode_rgb8_batch_into_buffer_with_session(
2942 Rgb8MetalBatchRequest {
2943 source: Rgb8MetalBatchSource::Decoders(decoders),
2944 op: Rgb8MetalBatchOp::Full,
2945 },
2946 MetalBufferBatchTarget::Resizable(output),
2947 session,
2948 )
2949 }
2950
2951 #[cfg(target_os = "macos")]
2952 fn decode_rgb8_scaled_batch_into_metal_buffer_with_session(
2953 inputs: &[&[u8]],
2954 scale: Downscale,
2955 output: &MetalBatchOutputBuffer,
2956 session: &MetalBackendSession,
2957 ) -> Result<Vec<Result<Surface, Error>>, Error> {
2958 Codec::decode_rgb8_batch_into_buffer_with_session(
2959 Rgb8MetalBatchRequest {
2960 source: Rgb8MetalBatchSource::Bytes(inputs),
2961 op: Rgb8MetalBatchOp::Scaled(scale),
2962 },
2963 MetalBufferBatchTarget::Reusable(output),
2964 session,
2965 )
2966 }
2967
2968 #[cfg(target_os = "macos")]
2969 fn decode_rgb8_scaled_batch_into_metal_textures_with_session(
2970 inputs: &[&[u8]],
2971 scale: Downscale,
2972 output: &MetalBatchTextureOutput,
2973 session: &MetalBackendSession,
2974 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
2975 Codec::decode_rgb8_batch_into_textures_with_session(
2976 Rgb8MetalBatchRequest {
2977 source: Rgb8MetalBatchSource::Bytes(inputs),
2978 op: Rgb8MetalBatchOp::Scaled(scale),
2979 },
2980 MetalTextureBatchTarget::Reusable(output),
2981 session,
2982 )
2983 }
2984
2985 #[cfg(target_os = "macos")]
2986 fn decode_rgb8_scaled_batch_into_resizable_metal_textures_with_session(
2987 inputs: &[&[u8]],
2988 scale: Downscale,
2989 output: &mut MetalBatchTextureOutput,
2990 session: &MetalBackendSession,
2991 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
2992 Codec::decode_rgb8_batch_into_textures_with_session(
2993 Rgb8MetalBatchRequest {
2994 source: Rgb8MetalBatchSource::Bytes(inputs),
2995 op: Rgb8MetalBatchOp::Scaled(scale),
2996 },
2997 MetalTextureBatchTarget::Resizable(output),
2998 session,
2999 )
3000 }
3001
3002 #[cfg(target_os = "macos")]
3003 fn decode_rgb8_decoder_scaled_batch_into_metal_buffer_with_session(
3004 decoders: &[&Decoder<'_>],
3005 scale: Downscale,
3006 output: &MetalBatchOutputBuffer,
3007 session: &MetalBackendSession,
3008 ) -> Result<Vec<Result<Surface, Error>>, Error> {
3009 Codec::decode_rgb8_batch_into_buffer_with_session(
3010 Rgb8MetalBatchRequest {
3011 source: Rgb8MetalBatchSource::Decoders(decoders),
3012 op: Rgb8MetalBatchOp::Scaled(scale),
3013 },
3014 MetalBufferBatchTarget::Reusable(output),
3015 session,
3016 )
3017 }
3018
3019 #[cfg(target_os = "macos")]
3020 fn decode_rgb8_decoder_scaled_batch_into_metal_textures_with_session(
3021 decoders: &[&Decoder<'_>],
3022 scale: Downscale,
3023 output: &MetalBatchTextureOutput,
3024 session: &MetalBackendSession,
3025 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
3026 Codec::decode_rgb8_batch_into_textures_with_session(
3027 Rgb8MetalBatchRequest {
3028 source: Rgb8MetalBatchSource::Decoders(decoders),
3029 op: Rgb8MetalBatchOp::Scaled(scale),
3030 },
3031 MetalTextureBatchTarget::Reusable(output),
3032 session,
3033 )
3034 }
3035
3036 #[cfg(target_os = "macos")]
3037 fn decode_rgb8_decoder_scaled_batch_into_resizable_metal_buffer_with_session(
3038 decoders: &[&Decoder<'_>],
3039 scale: Downscale,
3040 output: &mut MetalBatchOutputBuffer,
3041 session: &MetalBackendSession,
3042 ) -> Result<Vec<Result<Surface, Error>>, Error> {
3043 Codec::decode_rgb8_batch_into_buffer_with_session(
3044 Rgb8MetalBatchRequest {
3045 source: Rgb8MetalBatchSource::Decoders(decoders),
3046 op: Rgb8MetalBatchOp::Scaled(scale),
3047 },
3048 MetalBufferBatchTarget::Resizable(output),
3049 session,
3050 )
3051 }
3052
3053 #[cfg(target_os = "macos")]
3054 fn decode_rgb8_decoder_scaled_batch_into_resizable_metal_textures_with_session(
3055 decoders: &[&Decoder<'_>],
3056 scale: Downscale,
3057 output: &mut MetalBatchTextureOutput,
3058 session: &MetalBackendSession,
3059 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
3060 Codec::decode_rgb8_batch_into_textures_with_session(
3061 Rgb8MetalBatchRequest {
3062 source: Rgb8MetalBatchSource::Decoders(decoders),
3063 op: Rgb8MetalBatchOp::Scaled(scale),
3064 },
3065 MetalTextureBatchTarget::Resizable(output),
3066 session,
3067 )
3068 }
3069
3070 #[cfg(target_os = "macos")]
3071 fn decode_rgb8_region_scaled_batch_into_metal_buffer_with_session(
3072 inputs: &[&[u8]],
3073 roi: Rect,
3074 scale: Downscale,
3075 output: &MetalBatchOutputBuffer,
3076 session: &MetalBackendSession,
3077 ) -> Result<Vec<Result<Surface, Error>>, Error> {
3078 Codec::decode_rgb8_batch_into_buffer_with_session(
3079 Rgb8MetalBatchRequest {
3080 source: Rgb8MetalBatchSource::Bytes(inputs),
3081 op: Rgb8MetalBatchOp::RegionScaled { roi, scale },
3082 },
3083 MetalBufferBatchTarget::Reusable(output),
3084 session,
3085 )
3086 }
3087
3088 #[cfg(target_os = "macos")]
3089 fn decode_rgb8_region_scaled_batch_into_metal_textures_with_session(
3090 inputs: &[&[u8]],
3091 roi: Rect,
3092 scale: Downscale,
3093 output: &MetalBatchTextureOutput,
3094 session: &MetalBackendSession,
3095 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
3096 Codec::decode_rgb8_batch_into_textures_with_session(
3097 Rgb8MetalBatchRequest {
3098 source: Rgb8MetalBatchSource::Bytes(inputs),
3099 op: Rgb8MetalBatchOp::RegionScaled { roi, scale },
3100 },
3101 MetalTextureBatchTarget::Reusable(output),
3102 session,
3103 )
3104 }
3105
3106 #[cfg(target_os = "macos")]
3107 fn decode_rgb8_decoder_region_scaled_batch_into_metal_buffer_with_session(
3108 decoders: &[&Decoder<'_>],
3109 roi: Rect,
3110 scale: Downscale,
3111 output: &MetalBatchOutputBuffer,
3112 session: &MetalBackendSession,
3113 ) -> Result<Vec<Result<Surface, Error>>, Error> {
3114 Codec::decode_rgb8_batch_into_buffer_with_session(
3115 Rgb8MetalBatchRequest {
3116 source: Rgb8MetalBatchSource::Decoders(decoders),
3117 op: Rgb8MetalBatchOp::RegionScaled { roi, scale },
3118 },
3119 MetalBufferBatchTarget::Reusable(output),
3120 session,
3121 )
3122 }
3123
3124 #[cfg(target_os = "macos")]
3125 fn decode_rgb8_decoder_region_scaled_batch_into_metal_textures_with_session(
3126 decoders: &[&Decoder<'_>],
3127 roi: Rect,
3128 scale: Downscale,
3129 output: &MetalBatchTextureOutput,
3130 session: &MetalBackendSession,
3131 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
3132 Codec::decode_rgb8_batch_into_textures_with_session(
3133 Rgb8MetalBatchRequest {
3134 source: Rgb8MetalBatchSource::Decoders(decoders),
3135 op: Rgb8MetalBatchOp::RegionScaled { roi, scale },
3136 },
3137 MetalTextureBatchTarget::Reusable(output),
3138 session,
3139 )
3140 }
3141
3142 #[cfg(target_os = "macos")]
3143 fn decode_rgb8_decoder_region_scaled_batch_into_resizable_metal_buffer_with_session(
3144 decoders: &[&Decoder<'_>],
3145 roi: Rect,
3146 scale: Downscale,
3147 output: &mut MetalBatchOutputBuffer,
3148 session: &MetalBackendSession,
3149 ) -> Result<Vec<Result<Surface, Error>>, Error> {
3150 Codec::decode_rgb8_batch_into_buffer_with_session(
3151 Rgb8MetalBatchRequest {
3152 source: Rgb8MetalBatchSource::Decoders(decoders),
3153 op: Rgb8MetalBatchOp::RegionScaled { roi, scale },
3154 },
3155 MetalBufferBatchTarget::Resizable(output),
3156 session,
3157 )
3158 }
3159
3160 #[cfg(target_os = "macos")]
3161 fn decode_rgb8_decoder_region_scaled_batch_into_resizable_metal_textures_with_session(
3162 decoders: &[&Decoder<'_>],
3163 roi: Rect,
3164 scale: Downscale,
3165 output: &mut MetalBatchTextureOutput,
3166 session: &MetalBackendSession,
3167 ) -> Result<Vec<Result<MetalTextureTile, Error>>, Error> {
3168 Codec::decode_rgb8_batch_into_textures_with_session(
3169 Rgb8MetalBatchRequest {
3170 source: Rgb8MetalBatchSource::Decoders(decoders),
3171 op: Rgb8MetalBatchOp::RegionScaled { roi, scale },
3172 },
3173 MetalTextureBatchTarget::Resizable(output),
3174 session,
3175 )
3176 }
3177
3178 #[cfg(target_os = "macos")]
3179 use j2k_jpeg::adapter::build_fast422_packet;
3180 use j2k_jpeg::adapter::{build_fast420_packet, build_fast444_packet};
3181 #[cfg(target_os = "macos")]
3182 use j2k_jpeg::{
3183 encode_jpeg_baseline, JpegBackend, JpegEncodeOptions, JpegSamples, JpegSubsampling,
3184 };
3185
3186 const BASELINE_420: &[u8] = include_bytes!("../fixtures/jpeg/baseline_420_16x16.jpg");
3187 const BASELINE_420_RESTART: &[u8] =
3188 include_bytes!("../fixtures/jpeg/baseline_420_restart_32x16.jpg");
3189 #[cfg(target_os = "macos")]
3190 const BASELINE_422: &[u8] = include_bytes!("../fixtures/jpeg/baseline_422_16x8.jpg");
3191 const BASELINE_444: &[u8] = include_bytes!("../fixtures/jpeg/baseline_444_8x8.jpg");
3192 #[cfg(not(target_os = "macos"))]
3193 const GRAYSCALE: &[u8] = include_bytes!("../fixtures/jpeg/grayscale_8x8.jpg");
3194
3195 #[test]
3196 fn metal_runtime_failures_are_not_unsupported_errors() {
3197 for err in [
3198 Error::MetalRuntime {
3199 message: "runtime".to_string(),
3200 },
3201 Error::MetalKernel {
3202 message: "kernel".to_string(),
3203 },
3204 Error::MetalStatePoisoned {
3205 state: "JPEG Metal session",
3206 },
3207 ] {
3208 assert!(!err.is_unsupported(), "{err:?}");
3209 }
3210 }
3211
3212 #[test]
3213 fn auto_route_prefers_cpu_host_for_nonrestart_packets() {
3214 let decoder_420 = CpuDecoder::new(BASELINE_420).expect("420 decoder");
3215 let packet_420 = build_fast420_packet(BASELINE_420).expect("420 packet");
3216 assert_eq!(
3217 choose_route(
3218 &decoder_420,
3219 BackendRequest::Auto,
3220 PixelFormat::Rgb8,
3221 batch::BatchOp::Full,
3222 None,
3223 None,
3224 Some(&packet_420),
3225 ),
3226 routing::RouteDecision::CpuHost
3227 );
3228
3229 let decoder_444 = CpuDecoder::new(BASELINE_444).expect("444 decoder");
3230 let packet_444 = build_fast444_packet(BASELINE_444).expect("444 packet");
3231 assert_eq!(
3232 choose_route(
3233 &decoder_444,
3234 BackendRequest::Auto,
3235 PixelFormat::Rgb8,
3236 batch::BatchOp::Scaled(Downscale::Quarter),
3237 Some(&packet_444),
3238 None,
3239 None,
3240 ),
3241 routing::RouteDecision::CpuHost
3242 );
3243 }
3244
3245 #[test]
3246 fn auto_route_keeps_small_single_restart_packets_on_cpu_host() {
3247 let decoder = CpuDecoder::new(BASELINE_420_RESTART).expect("restart decoder");
3248 let packet = build_fast420_packet(BASELINE_420_RESTART).expect("restart packet");
3249
3250 assert_eq!(
3251 choose_route(
3252 &decoder,
3253 BackendRequest::Auto,
3254 PixelFormat::Rgb8,
3255 batch::BatchOp::Full,
3256 None,
3257 None,
3258 Some(&packet)
3259 ),
3260 routing::RouteDecision::CpuHost
3261 );
3262 assert_eq!(
3263 choose_route(
3264 &decoder,
3265 BackendRequest::Auto,
3266 PixelFormat::Rgb8,
3267 batch::BatchOp::Region(Rect {
3268 x: 0,
3269 y: 0,
3270 w: 16,
3271 h: 16,
3272 }),
3273 None,
3274 None,
3275 Some(&packet),
3276 ),
3277 routing::RouteDecision::CpuHost
3278 );
3279 }
3280
3281 #[cfg(target_os = "macos")]
3282 #[test]
3283 fn metal_backend_session_reuses_compiled_runtime() {
3284 let session = MetalBackendSession::system_default().expect("Metal backend session");
3285 assert!(session.runtime.get().is_none());
3286
3287 let mut first = Decoder::new(BASELINE_420).expect("first decoder");
3288 let first_surface = first
3289 .decode_to_device_with_session(PixelFormat::Rgb8, &session)
3290 .expect("first session decode");
3291 assert_eq!(
3292 first_surface.residency(),
3293 SurfaceResidency::MetalResidentDecode
3294 );
3295 let first_runtime = session
3296 .runtime
3297 .get()
3298 .and_then(|runtime| runtime.as_ref().ok())
3299 .map(std::ptr::from_ref::<compute::MetalRuntime>)
3300 .expect("session runtime after first decode");
3301
3302 let mut second = Decoder::new(BASELINE_420).expect("second decoder");
3303 second
3304 .decode_to_device_with_session(PixelFormat::Rgb8, &session)
3305 .expect("second session decode");
3306 let second_runtime = session
3307 .runtime
3308 .get()
3309 .and_then(|runtime| runtime.as_ref().ok())
3310 .map(std::ptr::from_ref::<compute::MetalRuntime>)
3311 .expect("session runtime after second decode");
3312
3313 assert_eq!(first_runtime, second_runtime);
3314 }
3315
3316 #[cfg(target_os = "macos")]
3317 #[test]
3318 fn jpeg_rgb8_batch_decode_uses_backend_session_runtime() {
3319 let session = MetalBackendSession::system_default().expect("Metal backend session");
3320 assert!(session.runtime.get().is_none());
3321
3322 let inputs = [BASELINE_420, BASELINE_420];
3323 let results = decode_rgb8_batch_to_device_with_session(&inputs, &session)
3324 .expect("session batch decode")
3325 .expect("baseline JPEG batch should use Metal batch path");
3326
3327 assert_eq!(results.len(), 2);
3328 assert!(session.runtime.get().is_some());
3329 for result in results {
3330 let surface = result.expect("surface");
3331 assert_eq!(surface.backend_kind(), BackendKind::Metal);
3332 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
3333 assert_eq!(surface.dimensions(), (16, 16));
3334 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
3335 }
3336 }
3337
3338 #[cfg(target_os = "macos")]
3339 #[test]
3340 fn queued_jpeg_batch_decode_uses_metal_session_runtime() {
3341 use j2k_core::DeviceSubmission as _;
3342
3343 let backend_session = MetalBackendSession::system_default().expect("Metal backend session");
3344 assert!(backend_session.runtime.get().is_none());
3345 let mut session = MetalSession::with_backend_session(backend_session.clone());
3346 let mut ctx = j2k_core::DecoderContext::<j2k_jpeg::DecoderContext>::new();
3347 let mut pool = ScratchPool::new();
3348
3349 let submissions = (0..2)
3350 .map(|_| {
3351 <Codec as j2k_core::TileBatchDecodeSubmit>::submit_tile_to_device(
3352 &mut ctx,
3353 &mut session,
3354 &mut pool,
3355 BASELINE_420,
3356 PixelFormat::Rgb8,
3357 BackendRequest::Metal,
3358 )
3359 .expect("queued Metal tile submit")
3360 })
3361 .collect::<Vec<_>>();
3362
3363 for submission in submissions {
3364 let surface = submission.wait().expect("queued Metal surface");
3365 assert_eq!(surface.backend_kind(), BackendKind::Metal);
3366 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
3367 assert_eq!(surface.dimensions(), (16, 16));
3368 }
3369
3370 assert_eq!(session.submissions().expect("session submissions"), 1);
3371 assert!(
3372 backend_session.runtime.get().is_some(),
3373 "queued MetalSession batch decode should reuse its backend runtime"
3374 );
3375 }
3376
3377 #[cfg(target_os = "macos")]
3378 #[test]
3379 fn default_queued_jpeg_batch_decode_lazily_initializes_backend_session() {
3380 use j2k_core::DeviceSubmission as _;
3381
3382 let mut session = MetalSession::default();
3383 assert!(session
3384 .shared
3385 .0
3386 .lock()
3387 .expect("metal session")
3388 .backend_session
3389 .is_none());
3390 let mut ctx = j2k_core::DecoderContext::<j2k_jpeg::DecoderContext>::new();
3391 let mut pool = ScratchPool::new();
3392
3393 let submissions = (0..2)
3394 .map(|_| {
3395 <Codec as j2k_core::TileBatchDecodeSubmit>::submit_tile_to_device(
3396 &mut ctx,
3397 &mut session,
3398 &mut pool,
3399 BASELINE_420,
3400 PixelFormat::Rgb8,
3401 BackendRequest::Metal,
3402 )
3403 .expect("queued Metal tile submit")
3404 })
3405 .collect::<Vec<_>>();
3406
3407 for submission in submissions {
3408 let surface = submission.wait().expect("queued Metal surface");
3409 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
3410 }
3411
3412 let runtime_initialized = session
3413 .shared
3414 .0
3415 .lock()
3416 .expect("metal session")
3417 .backend_session
3418 .as_ref()
3419 .and_then(|backend| backend.runtime.get())
3420 .is_some();
3421 assert!(runtime_initialized);
3422 }
3423
3424 #[cfg(target_os = "macos")]
3425 #[test]
3426 fn rgb8_batch_decode_can_write_into_reusable_metal_output_buffer() {
3427 let session = MetalBackendSession::system_default().expect("Metal backend session");
3428 let output =
3429 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (16, 16), 2).expect("output buffer");
3430 let inputs = [BASELINE_420, BASELINE_420];
3431 let (expected, _) = CpuDecoder::new(BASELINE_420)
3432 .expect("cpu decoder")
3433 .decode(PixelFormat::Rgb8)
3434 .expect("cpu decode");
3435
3436 let surfaces = decode_rgb8_batch_into_metal_buffer_with_session(&inputs, &output, &session)
3437 .expect("decode into reusable output");
3438
3439 assert_eq!(surfaces.len(), 2);
3440 assert_eq!(output.tile_capacity(), 2);
3441 assert_eq!(
3442 output.tile_stride_bytes(),
3443 16 * 16 * PixelFormat::Rgb8.bytes_per_pixel()
3444 );
3445 for (index, result) in surfaces.into_iter().enumerate() {
3446 let surface = result.expect("surface");
3447 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
3448 assert_eq!(surface.dimensions(), (16, 16));
3449 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
3450 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
3451 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
3452 assert_eq!(offset, index * output.tile_stride_bytes());
3453 assert_eq!(surface.as_bytes(), expected.as_slice());
3454 }
3455 }
3456
3457 #[cfg(target_os = "macos")]
3458 #[test]
3459 fn rgb8_decoder_batch_resizes_reusable_metal_output_buffer() {
3460 let session = MetalBackendSession::system_default().expect("Metal backend session");
3461 let mut output =
3462 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (1, 1), 1).expect("output buffer");
3463 let first = Decoder::new(BASELINE_420).expect("first decoder");
3464 let second = Decoder::new(BASELINE_420).expect("second decoder");
3465 let decoders = [&first, &second];
3466 let (expected, _) = CpuDecoder::new(BASELINE_420)
3467 .expect("cpu decoder")
3468 .decode(PixelFormat::Rgb8)
3469 .expect("cpu decode");
3470
3471 let surfaces = decode_rgb8_decoder_batch_into_resizable_metal_buffer_with_session(
3472 &decoders,
3473 &mut output,
3474 &session,
3475 )
3476 .expect("decode cached decoder batch into resizable reusable output");
3477
3478 assert_eq!(output.dimensions(), (16, 16));
3479 assert_eq!(output.tile_capacity(), 2);
3480 assert_eq!(surfaces.len(), 2);
3481 for (index, result) in surfaces.into_iter().enumerate() {
3482 let surface = result.expect("surface");
3483 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
3484 assert_eq!(surface.dimensions(), (16, 16));
3485 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
3486 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
3487 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
3488 assert_eq!(offset, index * output.tile_stride_bytes());
3489 assert_eq!(surface.as_bytes(), expected.as_slice());
3490 }
3491 }
3492
3493 #[cfg(target_os = "macos")]
3494 #[test]
3495 fn rgb8_decoder_batch_can_write_into_fixed_metal_output_buffer() {
3496 let session = MetalBackendSession::system_default().expect("Metal backend session");
3497 let output =
3498 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (16, 16), 2).expect("output buffer");
3499 let first = Decoder::new(BASELINE_420).expect("first decoder");
3500 let second = Decoder::new(BASELINE_420).expect("second decoder");
3501 let decoders = [&first, &second];
3502 let (expected, _) = CpuDecoder::new(BASELINE_420)
3503 .expect("cpu decoder")
3504 .decode(PixelFormat::Rgb8)
3505 .expect("cpu decode");
3506
3507 let surfaces =
3508 decode_rgb8_decoder_batch_into_metal_buffer_with_session(&decoders, &output, &session)
3509 .expect("decode cached decoder batch into fixed reusable output");
3510
3511 assert_eq!(surfaces.len(), 2);
3512 assert_eq!(output.dimensions(), (16, 16));
3513 assert_eq!(output.tile_capacity(), 2);
3514 for (index, result) in surfaces.into_iter().enumerate() {
3515 let surface = result.expect("surface");
3516 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
3517 assert_eq!(surface.dimensions(), (16, 16));
3518 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
3519 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
3520 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
3521 assert_eq!(offset, index * output.tile_stride_bytes());
3522 assert_eq!(surface.as_bytes(), expected.as_slice());
3523 }
3524 }
3525
3526 #[cfg(target_os = "macos")]
3527 #[test]
3528 fn rgb8_decoder_batch_rejects_mixed_output_dimensions_without_resizing_buffer() {
3529 let session = MetalBackendSession::system_default().expect("Metal backend session");
3530 let mut output =
3531 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (1, 1), 1).expect("output buffer");
3532 let first = Decoder::new(BASELINE_420).expect("first decoder");
3533 let second = Decoder::new(BASELINE_444).expect("second decoder");
3534 let decoders = [&first, &second];
3535
3536 let Err(err) = decode_rgb8_decoder_batch_into_resizable_metal_buffer_with_session(
3537 &decoders,
3538 &mut output,
3539 &session,
3540 ) else {
3541 panic!("mixed output dimensions should be rejected");
3542 };
3543
3544 assert!(matches!(err, Error::UnsupportedMetalRequest { .. }));
3545 assert_eq!(output.dimensions(), (1, 1));
3546 assert_eq!(output.tile_capacity(), 1);
3547 }
3548
3549 #[cfg(target_os = "macos")]
3550 #[test]
3551 fn rgb8_decoder_batch_rejects_mixed_sampling_without_resizing_buffer() {
3552 let session = MetalBackendSession::system_default().expect("Metal backend session");
3553 let mut output =
3554 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (1, 1), 1).expect("output buffer");
3555 let rgb = j2k_test_support::patterned_rgb8(16, 16);
3556 let fast420 = encode_jpeg_baseline(
3557 JpegSamples::Rgb8 {
3558 data: &rgb,
3559 width: 16,
3560 height: 16,
3561 },
3562 JpegEncodeOptions {
3563 quality: 90,
3564 subsampling: JpegSubsampling::Ybr420,
3565 restart_interval: None,
3566 backend: JpegBackend::Cpu,
3567 },
3568 )
3569 .expect("encode fast420 jpeg");
3570 let fast444 = encode_jpeg_baseline(
3571 JpegSamples::Rgb8 {
3572 data: &rgb,
3573 width: 16,
3574 height: 16,
3575 },
3576 JpegEncodeOptions {
3577 quality: 90,
3578 subsampling: JpegSubsampling::Ybr444,
3579 restart_interval: None,
3580 backend: JpegBackend::Cpu,
3581 },
3582 )
3583 .expect("encode fast444 jpeg");
3584 let first = Decoder::new(&fast420.data).expect("first decoder");
3585 let second = Decoder::new(&fast444.data).expect("second decoder");
3586 let decoders = [&first, &second];
3587
3588 let Err(err) = decode_rgb8_decoder_batch_into_resizable_metal_buffer_with_session(
3589 &decoders,
3590 &mut output,
3591 &session,
3592 ) else {
3593 panic!("mixed sampling should be rejected");
3594 };
3595
3596 assert!(matches!(
3597 err,
3598 Error::UnsupportedMetalRequest { reason }
3599 if reason.contains("same fast-packet sampling family")
3600 ));
3601 assert_eq!(output.dimensions(), (1, 1));
3602 assert_eq!(output.tile_capacity(), 1);
3603 }
3604
3605 #[cfg(target_os = "macos")]
3606 #[test]
3607 fn rgb8_decoder_batch_metal_report_exposes_required_output_shape() {
3608 let first = Decoder::new(BASELINE_420).expect("first decoder");
3609 let second = Decoder::new(BASELINE_420).expect("second decoder");
3610 let decoders = [&first, &second];
3611
3612 let full =
3613 Codec::inspect_rgb8_decoder_batch_metal_output(&decoders, j2k_jpeg::JpegDecodeOp::Full);
3614 let scaled = Codec::inspect_rgb8_decoder_batch_metal_output(
3615 &decoders,
3616 j2k_jpeg::JpegDecodeOp::Scaled(Downscale::Quarter),
3617 );
3618 let roi = Rect {
3619 x: 1,
3620 y: 2,
3621 w: 10,
3622 h: 9,
3623 };
3624 let region_scaled = Codec::inspect_rgb8_decoder_batch_metal_output(
3625 &decoders,
3626 j2k_jpeg::JpegDecodeOp::RegionScaled {
3627 roi: j2k_jpeg::Rect {
3628 x: roi.x,
3629 y: roi.y,
3630 w: roi.w,
3631 h: roi.h,
3632 },
3633 scale: Downscale::Quarter,
3634 },
3635 );
3636
3637 assert!(full.eligibility.eligible);
3638 assert_eq!(full.tile_count, 2);
3639 assert_eq!(full.output_dimensions, Some((16, 16)));
3640 assert_eq!(full.required_tile_capacity(), 2);
3641
3642 assert!(scaled.eligibility.eligible);
3643 assert_eq!(scaled.output_dimensions, Some((4, 4)));
3644
3645 assert!(region_scaled.eligibility.eligible);
3646 let expected = roi.scaled_covering(Downscale::Quarter);
3647 assert_eq!(
3648 region_scaled.output_dimensions,
3649 Some((expected.w, expected.h))
3650 );
3651 }
3652
3653 #[cfg(target_os = "macos")]
3654 #[test]
3655 fn rgb8_decoder_batch_metal_report_rejects_incompatible_batches_without_launch() {
3656 let first = Decoder::new(BASELINE_420).expect("first decoder");
3657 let second = Decoder::new(BASELINE_444).expect("second decoder");
3658 let decoders = [&first, &second];
3659
3660 let mixed =
3661 Codec::inspect_rgb8_decoder_batch_metal_output(&decoders, j2k_jpeg::JpegDecodeOp::Full);
3662 let region = Codec::inspect_rgb8_decoder_batch_metal_output(
3663 &[&first],
3664 j2k_jpeg::JpegDecodeOp::Region(j2k_jpeg::Rect {
3665 x: 0,
3666 y: 0,
3667 w: 8,
3668 h: 8,
3669 }),
3670 );
3671
3672 assert!(!mixed.eligibility.eligible);
3673 assert_eq!(mixed.output_dimensions, None);
3674 assert!(mixed
3675 .eligibility
3676 .reason
3677 .expect("mixed rejection")
3678 .contains("matching output dimensions"));
3679
3680 assert!(!region.eligibility.eligible);
3681 assert!(region
3682 .eligibility
3683 .reason
3684 .expect("region rejection")
3685 .contains("full, scaled, or region-scaled"));
3686 }
3687
3688 #[cfg(target_os = "macos")]
3689 #[test]
3690 fn rgb8_decoder_batch_metal_report_rejects_mixed_sampling_family() {
3691 let rgb = j2k_test_support::patterned_rgb8(16, 16);
3692 let fast420 = encode_jpeg_baseline(
3693 JpegSamples::Rgb8 {
3694 data: &rgb,
3695 width: 16,
3696 height: 16,
3697 },
3698 JpegEncodeOptions {
3699 quality: 90,
3700 subsampling: JpegSubsampling::Ybr420,
3701 restart_interval: None,
3702 backend: JpegBackend::Cpu,
3703 },
3704 )
3705 .expect("encode fast420 jpeg");
3706 let fast444 = encode_jpeg_baseline(
3707 JpegSamples::Rgb8 {
3708 data: &rgb,
3709 width: 16,
3710 height: 16,
3711 },
3712 JpegEncodeOptions {
3713 quality: 90,
3714 subsampling: JpegSubsampling::Ybr444,
3715 restart_interval: None,
3716 backend: JpegBackend::Cpu,
3717 },
3718 )
3719 .expect("encode fast444 jpeg");
3720 let first = Decoder::new(&fast420.data).expect("first decoder");
3721 let second = Decoder::new(&fast444.data).expect("second decoder");
3722 let decoders = [&first, &second];
3723
3724 let report =
3725 Codec::inspect_rgb8_decoder_batch_metal_output(&decoders, j2k_jpeg::JpegDecodeOp::Full);
3726
3727 assert!(!report.eligibility.eligible);
3728 assert_eq!(report.output_dimensions, None);
3729 assert!(report
3730 .eligibility
3731 .reason
3732 .expect("mixed sampling rejection")
3733 .contains("same fast-packet sampling family"));
3734 }
3735
3736 #[cfg(target_os = "macos")]
3737 #[test]
3738 fn rgb8_decoder_batch_metal_report_rejects_restart_fast422_full_tiles() {
3739 let rgb = j2k_test_support::patterned_rgb8(64, 32);
3740 let jpeg = encode_jpeg_baseline(
3741 JpegSamples::Rgb8 {
3742 data: &rgb,
3743 width: 64,
3744 height: 32,
3745 },
3746 JpegEncodeOptions {
3747 quality: 90,
3748 subsampling: JpegSubsampling::Ybr422,
3749 restart_interval: Some(4),
3750 backend: JpegBackend::Cpu,
3751 },
3752 )
3753 .expect("encode restart fast422 jpeg");
3754 let packet = build_fast422_packet(&jpeg.data).expect("restart fast422 packet");
3755 assert_ne!(packet.restart_interval_mcus, 0);
3756 let first = Decoder::new(&jpeg.data).expect("first decoder");
3757 let second = Decoder::new(&jpeg.data).expect("second decoder");
3758 let decoders = [&first, &second];
3759
3760 let full =
3761 Codec::inspect_rgb8_decoder_batch_metal_output(&decoders, j2k_jpeg::JpegDecodeOp::Full);
3762 let scaled = Codec::inspect_rgb8_decoder_batch_metal_output(
3763 &decoders,
3764 j2k_jpeg::JpegDecodeOp::Scaled(Downscale::Half),
3765 );
3766
3767 assert!(!full.eligibility.eligible);
3768 assert_eq!(full.output_dimensions, None);
3769 assert!(full
3770 .eligibility
3771 .reason
3772 .expect("restart fast422 full rejection")
3773 .contains("restart-coded full-tile 4:2:2 or 4:4:4"));
3774
3775 assert!(scaled.eligibility.eligible);
3776 assert_eq!(scaled.output_dimensions, Some((32, 16)));
3777 }
3778
3779 #[cfg(target_os = "macos")]
3780 #[test]
3781 fn rgb8_fast444_batch_decode_can_write_into_reusable_metal_output_buffer() {
3782 let session = MetalBackendSession::system_default().expect("Metal backend session");
3783 let output =
3784 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (8, 8), 2).expect("output buffer");
3785 let inputs = [BASELINE_444, BASELINE_444];
3786 let (expected, _) = CpuDecoder::new(BASELINE_444)
3787 .expect("cpu decoder")
3788 .decode(PixelFormat::Rgb8)
3789 .expect("cpu decode");
3790
3791 let surfaces = decode_rgb8_batch_into_metal_buffer_with_session(&inputs, &output, &session)
3792 .expect("decode into reusable output");
3793
3794 assert_eq!(surfaces.len(), 2);
3795 for (index, result) in surfaces.into_iter().enumerate() {
3796 let surface = result.expect("surface");
3797 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
3798 assert_eq!(surface.dimensions(), (8, 8));
3799 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
3800 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
3801 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
3802 assert_eq!(offset, index * output.tile_stride_bytes());
3803 assert_eq!(surface.as_bytes(), expected.as_slice());
3804 }
3805 }
3806
3807 #[cfg(target_os = "macos")]
3808 fn assert_table_mixed_full_buffer_groups_resident(
3809 subsampling: JpegSubsampling,
3810 dimensions: (u32, u32),
3811 first_quality: u8,
3812 second_quality: u8,
3813 ) {
3814 let session = MetalBackendSession::system_default().expect("Metal backend session");
3815 let rgb_a = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
3816 let mut rgb_b = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
3817 let mut rgb_c = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
3818 for (index, pixel) in rgb_b.chunks_exact_mut(3).enumerate() {
3819 let delta = patterned_index_byte(index)
3820 .wrapping_mul(43)
3821 .wrapping_add(17);
3822 pixel[0] ^= delta.rotate_left(1);
3823 pixel[1] = pixel[1].wrapping_sub(delta);
3824 pixel[2] = pixel[2].wrapping_add(delta.rotate_right(2));
3825 }
3826 for (index, pixel) in rgb_c.chunks_exact_mut(3).enumerate() {
3827 let delta = patterned_index_byte(index)
3828 .wrapping_mul(47)
3829 .wrapping_add(23);
3830 pixel[0] = pixel[0].wrapping_add(delta.rotate_left(2));
3831 pixel[1] ^= delta.rotate_right(1);
3832 pixel[2] = pixel[2].wrapping_sub(delta);
3833 }
3834
3835 let jpeg_a = encode_jpeg_baseline(
3836 JpegSamples::Rgb8 {
3837 data: &rgb_a,
3838 width: dimensions.0,
3839 height: dimensions.1,
3840 },
3841 JpegEncodeOptions {
3842 quality: first_quality,
3843 subsampling,
3844 restart_interval: None,
3845 backend: JpegBackend::Cpu,
3846 },
3847 )
3848 .expect("encode first table-mixed full buffer jpeg");
3849 let jpeg_b = encode_jpeg_baseline(
3850 JpegSamples::Rgb8 {
3851 data: &rgb_b,
3852 width: dimensions.0,
3853 height: dimensions.1,
3854 },
3855 JpegEncodeOptions {
3856 quality: second_quality,
3857 subsampling,
3858 restart_interval: None,
3859 backend: JpegBackend::Cpu,
3860 },
3861 )
3862 .expect("encode second table-mixed full buffer jpeg");
3863 let jpeg_c = encode_jpeg_baseline(
3864 JpegSamples::Rgb8 {
3865 data: &rgb_c,
3866 width: dimensions.0,
3867 height: dimensions.1,
3868 },
3869 JpegEncodeOptions {
3870 quality: first_quality,
3871 subsampling,
3872 restart_interval: None,
3873 backend: JpegBackend::Cpu,
3874 },
3875 )
3876 .expect("encode third table-mixed full buffer jpeg");
3877
3878 match subsampling {
3879 JpegSubsampling::Ybr420 => {
3880 let packet_a = build_fast420_packet(&jpeg_a.data).expect("first packet");
3881 let packet_b = build_fast420_packet(&jpeg_b.data).expect("second packet");
3882 let packet_c = build_fast420_packet(&jpeg_c.data).expect("third packet");
3883 assert_eq!(packet_a.y_quant, packet_c.y_quant);
3884 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
3885 assert_eq!(
3886 packet_a.entropy_checkpoints.len(),
3887 packet_c.entropy_checkpoints.len()
3888 );
3889 assert_ne!(packet_a.y_quant, packet_b.y_quant);
3890 }
3891 JpegSubsampling::Ybr422 => {
3892 let packet_a = build_fast422_packet(&jpeg_a.data).expect("first packet");
3893 let packet_b = build_fast422_packet(&jpeg_b.data).expect("second packet");
3894 let packet_c = build_fast422_packet(&jpeg_c.data).expect("third packet");
3895 assert_eq!(packet_a.y_quant, packet_c.y_quant);
3896 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
3897 assert_eq!(
3898 packet_a.entropy_checkpoints.len(),
3899 packet_c.entropy_checkpoints.len()
3900 );
3901 assert_ne!(packet_a.y_quant, packet_b.y_quant);
3902 }
3903 JpegSubsampling::Ybr444 => {
3904 let packet_a = build_fast444_packet(&jpeg_a.data).expect("first packet");
3905 let packet_b = build_fast444_packet(&jpeg_b.data).expect("second packet");
3906 let packet_c = build_fast444_packet(&jpeg_c.data).expect("third packet");
3907 assert_eq!(packet_a.y_quant, packet_c.y_quant);
3908 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
3909 assert_eq!(
3910 packet_a.entropy_checkpoints.len(),
3911 packet_c.entropy_checkpoints.len()
3912 );
3913 assert_ne!(packet_a.y_quant, packet_b.y_quant);
3914 }
3915 JpegSubsampling::Gray => panic!("table-mixed buffer helper expects YCbCr sampling"),
3916 }
3917
3918 let output =
3919 MetalBatchOutputBuffer::new_rgb8_tiles(&session, dimensions, 3).expect("output buffer");
3920 let inputs = [
3921 jpeg_a.data.as_slice(),
3922 jpeg_b.data.as_slice(),
3923 jpeg_c.data.as_slice(),
3924 ];
3925 let expected_tiles = inputs
3926 .iter()
3927 .map(|input| {
3928 CpuDecoder::new(input)
3929 .expect("cpu decoder")
3930 .decode(PixelFormat::Rgb8)
3931 .expect("cpu decode")
3932 .0
3933 })
3934 .collect::<Vec<_>>();
3935 assert_ne!(expected_tiles[0], expected_tiles[1]);
3936 assert_ne!(expected_tiles[0], expected_tiles[2]);
3937 assert_ne!(expected_tiles[1], expected_tiles[2]);
3938
3939 let surfaces = decode_rgb8_batch_into_metal_buffer_with_session(&inputs, &output, &session)
3940 .expect("decode table-mixed full tiles into reusable output buffer");
3941
3942 assert_eq!(surfaces.len(), 3);
3943 for (index, surface) in surfaces.into_iter().enumerate() {
3944 let surface = surface.expect("surface");
3945 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
3946 assert_eq!(surface.dimensions(), dimensions);
3947 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
3948 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
3949 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
3950 assert_eq!(offset, index * output.tile_stride_bytes());
3951 assert_eq!(surface.as_bytes(), expected_tiles[index].as_slice());
3952 }
3953 }
3954
3955 #[cfg(target_os = "macos")]
3956 #[test]
3957 fn rgb8_table_mixed_fast420_buffer_batch_groups_resident_dispatches() {
3958 assert_table_mixed_full_buffer_groups_resident(JpegSubsampling::Ybr420, (128, 96), 90, 72);
3959 }
3960
3961 #[cfg(target_os = "macos")]
3962 #[test]
3963 fn rgb8_table_mixed_fast422_buffer_batch_groups_resident_dispatches() {
3964 assert_table_mixed_full_buffer_groups_resident(JpegSubsampling::Ybr422, (128, 96), 91, 73);
3965 }
3966
3967 #[cfg(target_os = "macos")]
3968 #[test]
3969 fn rgb8_table_mixed_fast444_buffer_batch_groups_resident_dispatches() {
3970 assert_table_mixed_full_buffer_groups_resident(JpegSubsampling::Ybr444, (96, 96), 92, 74);
3971 }
3972
3973 #[cfg(target_os = "macos")]
3974 #[test]
3975 fn rgb8_scaled_batch_decode_can_write_into_reusable_metal_output_buffer() {
3976 let session = MetalBackendSession::system_default().expect("Metal backend session");
3977 let scale = Downscale::Quarter;
3978 let output =
3979 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (4, 4), 2).expect("output buffer");
3980 let inputs = [BASELINE_420, BASELINE_420];
3981 let (expected, _) = CpuDecoder::new(BASELINE_420)
3982 .expect("cpu decoder")
3983 .decode_scaled(PixelFormat::Rgb8, scale)
3984 .expect("cpu scaled decode");
3985
3986 let surfaces = decode_rgb8_scaled_batch_into_metal_buffer_with_session(
3987 &inputs, scale, &output, &session,
3988 )
3989 .expect("decode scaled into reusable output");
3990
3991 assert_eq!(surfaces.len(), 2);
3992 for (index, result) in surfaces.into_iter().enumerate() {
3993 let surface = result.expect("surface");
3994 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
3995 assert_eq!(surface.dimensions(), (4, 4));
3996 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
3997 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
3998 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
3999 assert_eq!(offset, index * output.tile_stride_bytes());
4000 assert_eq!(surface.as_bytes(), expected.as_slice());
4001 }
4002 }
4003
4004 #[cfg(target_os = "macos")]
4005 #[test]
4006 fn rgb8_decoder_scaled_batch_resizes_reusable_metal_output_buffer() {
4007 let session = MetalBackendSession::system_default().expect("Metal backend session");
4008 let scale = Downscale::Quarter;
4009 let mut output =
4010 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (1, 1), 1).expect("output buffer");
4011 let first = Decoder::new(BASELINE_420).expect("first decoder");
4012 let second = Decoder::new(BASELINE_420).expect("second decoder");
4013 let decoders = [&first, &second];
4014 let (expected, _) = CpuDecoder::new(BASELINE_420)
4015 .expect("cpu decoder")
4016 .decode_scaled(PixelFormat::Rgb8, scale)
4017 .expect("cpu scaled decode");
4018
4019 let surfaces = decode_rgb8_decoder_scaled_batch_into_resizable_metal_buffer_with_session(
4020 &decoders,
4021 scale,
4022 &mut output,
4023 &session,
4024 )
4025 .expect("decode cached decoder scaled batch into resizable reusable output");
4026
4027 assert_eq!(output.dimensions(), (4, 4));
4028 assert_eq!(output.tile_capacity(), 2);
4029 assert_eq!(surfaces.len(), 2);
4030 for (index, result) in surfaces.into_iter().enumerate() {
4031 let surface = result.expect("surface");
4032 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
4033 assert_eq!(surface.dimensions(), (4, 4));
4034 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
4035 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
4036 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
4037 assert_eq!(offset, index * output.tile_stride_bytes());
4038 assert_eq!(surface.as_bytes(), expected.as_slice());
4039 }
4040 }
4041
4042 #[cfg(target_os = "macos")]
4043 #[test]
4044 fn rgb8_decoder_scaled_batch_can_write_into_fixed_metal_output_buffer() {
4045 let session = MetalBackendSession::system_default().expect("Metal backend session");
4046 let scale = Downscale::Quarter;
4047 let output =
4048 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (4, 4), 2).expect("output buffer");
4049 let first = Decoder::new(BASELINE_420).expect("first decoder");
4050 let second = Decoder::new(BASELINE_420).expect("second decoder");
4051 let decoders = [&first, &second];
4052 let (expected, _) = CpuDecoder::new(BASELINE_420)
4053 .expect("cpu decoder")
4054 .decode_scaled(PixelFormat::Rgb8, scale)
4055 .expect("cpu scaled decode");
4056
4057 let surfaces = decode_rgb8_decoder_scaled_batch_into_metal_buffer_with_session(
4058 &decoders, scale, &output, &session,
4059 )
4060 .expect("decode cached decoder scaled batch into fixed reusable output");
4061
4062 assert_eq!(surfaces.len(), 2);
4063 assert_eq!(output.dimensions(), (4, 4));
4064 assert_eq!(output.tile_capacity(), 2);
4065 for (index, result) in surfaces.into_iter().enumerate() {
4066 let surface = result.expect("surface");
4067 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
4068 assert_eq!(surface.dimensions(), (4, 4));
4069 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
4070 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
4071 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
4072 assert_eq!(offset, index * output.tile_stride_bytes());
4073 assert_eq!(surface.as_bytes(), expected.as_slice());
4074 }
4075 }
4076
4077 #[cfg(target_os = "macos")]
4078 #[test]
4079 fn rgb8_region_scaled_batch_decode_can_write_into_reusable_metal_output_buffer() {
4080 let session = MetalBackendSession::system_default().expect("Metal backend session");
4081 let roi = Rect {
4082 x: 1,
4083 y: 2,
4084 w: 5,
4085 h: 4,
4086 };
4087 let scale = Downscale::Quarter;
4088 let scaled = roi.scaled_covering(scale);
4089 let output = MetalBatchOutputBuffer::new_rgb8_tiles(&session, (scaled.w, scaled.h), 2)
4090 .expect("output buffer");
4091 let inputs = [BASELINE_444, BASELINE_444];
4092 let (expected, _) = CpuDecoder::new(BASELINE_444)
4093 .expect("cpu decoder")
4094 .decode_region_scaled(
4095 PixelFormat::Rgb8,
4096 j2k_jpeg::Rect {
4097 x: roi.x,
4098 y: roi.y,
4099 w: roi.w,
4100 h: roi.h,
4101 },
4102 scale,
4103 )
4104 .expect("cpu region scaled decode");
4105
4106 let surfaces = decode_rgb8_region_scaled_batch_into_metal_buffer_with_session(
4107 &inputs, roi, scale, &output, &session,
4108 )
4109 .expect("decode region scaled into reusable output");
4110
4111 assert_eq!(surfaces.len(), 2);
4112 for (index, result) in surfaces.into_iter().enumerate() {
4113 let surface = result.expect("surface");
4114 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
4115 assert_eq!(surface.dimensions(), (scaled.w, scaled.h));
4116 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
4117 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
4118 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
4119 assert_eq!(offset, index * output.tile_stride_bytes());
4120 assert_eq!(surface.as_bytes(), expected.as_slice());
4121 }
4122 }
4123
4124 #[cfg(target_os = "macos")]
4125 #[test]
4126 fn rgb8_region_scaled_batch_decode_resizes_reusable_metal_output_buffer() {
4127 let session = MetalBackendSession::system_default().expect("Metal backend session");
4128 let roi = Rect {
4129 x: 1,
4130 y: 2,
4131 w: 5,
4132 h: 4,
4133 };
4134 let scale = Downscale::Quarter;
4135 let scaled = roi.scaled_covering(scale);
4136 let mut output =
4137 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (1, 1), 1).expect("output buffer");
4138 let inputs = [BASELINE_444, BASELINE_444];
4139 let (expected, _) = CpuDecoder::new(BASELINE_444)
4140 .expect("cpu decoder")
4141 .decode_region_scaled(
4142 PixelFormat::Rgb8,
4143 j2k_jpeg::Rect {
4144 x: roi.x,
4145 y: roi.y,
4146 w: roi.w,
4147 h: roi.h,
4148 },
4149 scale,
4150 )
4151 .expect("cpu region scaled decode");
4152
4153 let surfaces =
4154 Codec::decode_rgb8_region_scaled_batch_into_resizable_metal_buffer_with_session(
4155 &inputs,
4156 roi,
4157 scale,
4158 &mut output,
4159 &session,
4160 )
4161 .expect("decode region scaled into resizable reusable output");
4162
4163 assert_eq!(output.dimensions(), (scaled.w, scaled.h));
4164 assert_eq!(output.tile_capacity(), 2);
4165 assert_eq!(surfaces.len(), 2);
4166 for (index, result) in surfaces.into_iter().enumerate() {
4167 let surface = result.expect("surface");
4168 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
4169 assert_eq!(surface.dimensions(), (scaled.w, scaled.h));
4170 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
4171 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
4172 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
4173 assert_eq!(offset, index * output.tile_stride_bytes());
4174 assert_eq!(surface.as_bytes(), expected.as_slice());
4175 }
4176 }
4177
4178 #[cfg(target_os = "macos")]
4179 #[test]
4180 fn rgb8_decoder_region_scaled_batch_resizes_reusable_metal_output_buffer() {
4181 let session = MetalBackendSession::system_default().expect("Metal backend session");
4182 let roi = Rect {
4183 x: 1,
4184 y: 2,
4185 w: 10,
4186 h: 9,
4187 };
4188 let scale = Downscale::Quarter;
4189 let scaled = roi.scaled_covering(scale);
4190 let mut output =
4191 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (1, 1), 1).expect("output buffer");
4192 let first = Decoder::new(BASELINE_420).expect("first decoder");
4193 let second = Decoder::new(BASELINE_420).expect("second decoder");
4194 let decoders = [&first, &second];
4195 let (expected, _) = CpuDecoder::new(BASELINE_420)
4196 .expect("cpu decoder")
4197 .decode_region_scaled(
4198 PixelFormat::Rgb8,
4199 j2k_jpeg::Rect {
4200 x: roi.x,
4201 y: roi.y,
4202 w: roi.w,
4203 h: roi.h,
4204 },
4205 scale,
4206 )
4207 .expect("cpu region scaled decode");
4208
4209 let surfaces =
4210 decode_rgb8_decoder_region_scaled_batch_into_resizable_metal_buffer_with_session(
4211 &decoders,
4212 roi,
4213 scale,
4214 &mut output,
4215 &session,
4216 )
4217 .expect("decode cached decoder batch into resizable reusable output");
4218
4219 assert_eq!(output.dimensions(), (scaled.w, scaled.h));
4220 assert_eq!(output.tile_capacity(), 2);
4221 assert_eq!(surfaces.len(), 2);
4222 for (index, result) in surfaces.into_iter().enumerate() {
4223 let surface = result.expect("surface");
4224 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
4225 assert_eq!(surface.dimensions(), (scaled.w, scaled.h));
4226 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
4227 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
4228 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
4229 assert_eq!(offset, index * output.tile_stride_bytes());
4230 assert_eq!(surface.as_bytes(), expected.as_slice());
4231 }
4232 }
4233
4234 #[cfg(target_os = "macos")]
4235 #[test]
4236 fn rgb8_decoder_region_scaled_batch_can_write_into_fixed_metal_output_buffer() {
4237 let session = MetalBackendSession::system_default().expect("Metal backend session");
4238 let roi = Rect {
4239 x: 1,
4240 y: 2,
4241 w: 10,
4242 h: 9,
4243 };
4244 let scale = Downscale::Quarter;
4245 let scaled = roi.scaled_covering(scale);
4246 let output = MetalBatchOutputBuffer::new_rgb8_tiles(&session, (scaled.w, scaled.h), 2)
4247 .expect("output buffer");
4248 let first = Decoder::new(BASELINE_420).expect("first decoder");
4249 let second = Decoder::new(BASELINE_420).expect("second decoder");
4250 let decoders = [&first, &second];
4251 let (expected, _) = CpuDecoder::new(BASELINE_420)
4252 .expect("cpu decoder")
4253 .decode_region_scaled(
4254 PixelFormat::Rgb8,
4255 j2k_jpeg::Rect {
4256 x: roi.x,
4257 y: roi.y,
4258 w: roi.w,
4259 h: roi.h,
4260 },
4261 scale,
4262 )
4263 .expect("cpu region scaled decode");
4264
4265 let surfaces = decode_rgb8_decoder_region_scaled_batch_into_metal_buffer_with_session(
4266 &decoders, roi, scale, &output, &session,
4267 )
4268 .expect("decode cached decoder region-scaled batch into fixed reusable output");
4269
4270 assert_eq!(surfaces.len(), 2);
4271 assert_eq!(output.dimensions(), (scaled.w, scaled.h));
4272 assert_eq!(output.tile_capacity(), 2);
4273 for (index, result) in surfaces.into_iter().enumerate() {
4274 let surface = result.expect("surface");
4275 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
4276 assert_eq!(surface.dimensions(), (scaled.w, scaled.h));
4277 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
4278 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
4279 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
4280 assert_eq!(offset, index * output.tile_stride_bytes());
4281 assert_eq!(surface.as_bytes(), expected.as_slice());
4282 }
4283 }
4284
4285 #[cfg(target_os = "macos")]
4286 #[test]
4287 fn rgb8_restart_fast420_region_scaled_batch_decode_writes_reusable_metal_output_buffer() {
4288 let session = MetalBackendSession::system_default().expect("Metal backend session");
4289 let dimensions = (128, 128);
4290 let roi = Rect {
4291 x: 9,
4292 y: 11,
4293 w: 73,
4294 h: 67,
4295 };
4296 let scale = Downscale::Half;
4297 let scaled = roi.scaled_covering(scale);
4298 let rgb = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
4299 let jpeg = encode_jpeg_baseline(
4300 JpegSamples::Rgb8 {
4301 data: &rgb,
4302 width: dimensions.0,
4303 height: dimensions.1,
4304 },
4305 JpegEncodeOptions {
4306 quality: 90,
4307 subsampling: JpegSubsampling::Ybr420,
4308 restart_interval: Some(4),
4309 backend: JpegBackend::Cpu,
4310 },
4311 )
4312 .expect("encode restart-coded fast420 region-scaled jpeg");
4313 let packet = build_fast420_packet(&jpeg.data).expect("restart fast420 packet");
4314 assert_ne!(packet.restart_interval_mcus, 0);
4315 assert!(!packet.restart_offsets.is_empty());
4316
4317 let output = MetalBatchOutputBuffer::new_rgb8_tiles(&session, (scaled.w, scaled.h), 2)
4318 .expect("output buffer");
4319 let inputs = [jpeg.data.as_slice(), jpeg.data.as_slice()];
4320 let (expected, _) = CpuDecoder::new(&jpeg.data)
4321 .expect("cpu decoder")
4322 .decode_region_scaled(
4323 PixelFormat::Rgb8,
4324 j2k_jpeg::Rect {
4325 x: roi.x,
4326 y: roi.y,
4327 w: roi.w,
4328 h: roi.h,
4329 },
4330 scale,
4331 )
4332 .expect("cpu region-scaled decode");
4333
4334 let surfaces = decode_rgb8_region_scaled_batch_into_metal_buffer_with_session(
4335 &inputs, roi, scale, &output, &session,
4336 )
4337 .expect("decode restart-coded region-scaled tiles into reusable output buffer");
4338
4339 assert_eq!(surfaces.len(), 2);
4340 for (index, surface) in surfaces.into_iter().enumerate() {
4341 let surface = surface.expect("surface");
4342 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
4343 assert_eq!(surface.dimensions(), (scaled.w, scaled.h));
4344 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
4345 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
4346 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
4347 assert_eq!(offset, index * output.tile_stride_bytes());
4348 assert_eq!(surface.as_bytes(), expected.as_slice());
4349 }
4350 }
4351
4352 #[cfg(target_os = "macos")]
4353 fn assert_restart_region_scaled_buffer_batch_writes_reusable_metal_output(
4354 subsampling: JpegSubsampling,
4355 dimensions: (u32, u32),
4356 ) {
4357 let session = MetalBackendSession::system_default().expect("Metal backend session");
4358 let roi = Rect {
4359 x: 0,
4360 y: 0,
4361 w: dimensions.0,
4362 h: dimensions.1,
4363 };
4364 let scale = Downscale::Half;
4365 let scaled = roi.scaled_covering(scale);
4366 let rgb = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
4367 let jpeg = encode_jpeg_baseline(
4368 JpegSamples::Rgb8 {
4369 data: &rgb,
4370 width: dimensions.0,
4371 height: dimensions.1,
4372 },
4373 JpegEncodeOptions {
4374 quality: 90,
4375 subsampling,
4376 restart_interval: Some(256),
4377 backend: JpegBackend::Cpu,
4378 },
4379 )
4380 .expect("encode restart-coded region-scaled jpeg");
4381 match subsampling {
4382 JpegSubsampling::Ybr422 => {
4383 let packet = build_fast422_packet(&jpeg.data).expect("restart fast422 packet");
4384 assert_ne!(packet.restart_interval_mcus, 0);
4385 assert!(!packet.restart_offsets.is_empty());
4386 }
4387 JpegSubsampling::Ybr444 => {
4388 let packet = build_fast444_packet(&jpeg.data).expect("restart fast444 packet");
4389 assert_ne!(packet.restart_interval_mcus, 0);
4390 assert!(!packet.restart_offsets.is_empty());
4391 }
4392 _ => panic!("restart region-scaled buffer helper expects fast422 or fast444"),
4393 }
4394
4395 let output = MetalBatchOutputBuffer::new_rgb8_tiles(&session, (scaled.w, scaled.h), 2)
4396 .expect("output buffer");
4397 let inputs = [jpeg.data.as_slice(), jpeg.data.as_slice()];
4398 let (expected, _) = CpuDecoder::new(&jpeg.data)
4399 .expect("cpu decoder")
4400 .decode_region_scaled(
4401 PixelFormat::Rgb8,
4402 j2k_jpeg::Rect {
4403 x: roi.x,
4404 y: roi.y,
4405 w: roi.w,
4406 h: roi.h,
4407 },
4408 scale,
4409 )
4410 .expect("cpu region-scaled decode");
4411
4412 let surfaces = decode_rgb8_region_scaled_batch_into_metal_buffer_with_session(
4413 &inputs, roi, scale, &output, &session,
4414 )
4415 .expect("decode restart-coded region-scaled tiles into reusable output buffer");
4416
4417 assert_eq!(surfaces.len(), 2);
4418 for (index, surface) in surfaces.into_iter().enumerate() {
4419 let surface = surface.expect("surface");
4420 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
4421 assert_eq!(surface.dimensions(), (scaled.w, scaled.h));
4422 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
4423 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
4424 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
4425 assert_eq!(offset, index * output.tile_stride_bytes());
4426 assert_eq!(surface.as_bytes(), expected.as_slice());
4427 }
4428 }
4429
4430 #[cfg(target_os = "macos")]
4431 #[test]
4432 fn rgb8_restart_fast422_region_scaled_batch_decode_writes_reusable_metal_output_buffer() {
4433 assert_restart_region_scaled_buffer_batch_writes_reusable_metal_output(
4434 JpegSubsampling::Ybr422,
4435 (128, 96),
4436 );
4437 }
4438
4439 #[cfg(target_os = "macos")]
4440 #[test]
4441 fn rgb8_restart_fast444_region_scaled_batch_decode_writes_reusable_metal_output_buffer() {
4442 assert_restart_region_scaled_buffer_batch_writes_reusable_metal_output(
4443 JpegSubsampling::Ybr444,
4444 (96, 96),
4445 );
4446 }
4447
4448 #[cfg(target_os = "macos")]
4449 fn assert_table_mixed_region_scaled_buffer_groups_resident(
4450 subsampling: JpegSubsampling,
4451 dimensions: (u32, u32),
4452 first_quality: u8,
4453 second_quality: u8,
4454 ) {
4455 let session = MetalBackendSession::system_default().expect("Metal backend session");
4456 let roi = Rect {
4457 x: 0,
4458 y: 0,
4459 w: dimensions.0,
4460 h: dimensions.1,
4461 };
4462 let scale = Downscale::Half;
4463 let scaled = roi.scaled_covering(scale);
4464 let rgb_a = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
4465 let mut rgb_b = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
4466 let mut rgb_c = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
4467 for (index, pixel) in rgb_b.chunks_exact_mut(3).enumerate() {
4468 let delta = patterned_index_byte(index)
4469 .wrapping_mul(37)
4470 .wrapping_add(19);
4471 pixel[0] = pixel[0].wrapping_add(delta.rotate_left(1));
4472 pixel[1] ^= delta;
4473 pixel[2] = pixel[2].wrapping_sub(delta.rotate_right(2));
4474 }
4475 for (index, pixel) in rgb_c.chunks_exact_mut(3).enumerate() {
4476 let delta = patterned_index_byte(index)
4477 .wrapping_mul(53)
4478 .wrapping_add(11);
4479 pixel[0] ^= delta.rotate_right(1);
4480 pixel[1] = pixel[1].wrapping_sub(delta.rotate_left(2));
4481 pixel[2] = pixel[2].wrapping_add(delta);
4482 }
4483
4484 let jpeg_a = encode_jpeg_baseline(
4485 JpegSamples::Rgb8 {
4486 data: &rgb_a,
4487 width: dimensions.0,
4488 height: dimensions.1,
4489 },
4490 JpegEncodeOptions {
4491 quality: first_quality,
4492 subsampling,
4493 restart_interval: None,
4494 backend: JpegBackend::Cpu,
4495 },
4496 )
4497 .expect("encode first table-mixed region-scaled buffer jpeg");
4498 let jpeg_b = encode_jpeg_baseline(
4499 JpegSamples::Rgb8 {
4500 data: &rgb_b,
4501 width: dimensions.0,
4502 height: dimensions.1,
4503 },
4504 JpegEncodeOptions {
4505 quality: second_quality,
4506 subsampling,
4507 restart_interval: None,
4508 backend: JpegBackend::Cpu,
4509 },
4510 )
4511 .expect("encode second table-mixed region-scaled buffer jpeg");
4512 let jpeg_c = encode_jpeg_baseline(
4513 JpegSamples::Rgb8 {
4514 data: &rgb_c,
4515 width: dimensions.0,
4516 height: dimensions.1,
4517 },
4518 JpegEncodeOptions {
4519 quality: first_quality,
4520 subsampling,
4521 restart_interval: None,
4522 backend: JpegBackend::Cpu,
4523 },
4524 )
4525 .expect("encode third table-mixed region-scaled buffer jpeg");
4526
4527 match subsampling {
4528 JpegSubsampling::Ybr420 => {
4529 let packet_a = build_fast420_packet(&jpeg_a.data).expect("first packet");
4530 let packet_b = build_fast420_packet(&jpeg_b.data).expect("second packet");
4531 let packet_c = build_fast420_packet(&jpeg_c.data).expect("third packet");
4532 assert_eq!(packet_a.y_quant, packet_c.y_quant);
4533 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
4534 assert_eq!(
4535 packet_a.entropy_checkpoints.len(),
4536 packet_c.entropy_checkpoints.len()
4537 );
4538 assert_ne!(packet_a.y_quant, packet_b.y_quant);
4539 }
4540 JpegSubsampling::Ybr422 => {
4541 let packet_a = build_fast422_packet(&jpeg_a.data).expect("first packet");
4542 let packet_b = build_fast422_packet(&jpeg_b.data).expect("second packet");
4543 let packet_c = build_fast422_packet(&jpeg_c.data).expect("third packet");
4544 assert_eq!(packet_a.y_quant, packet_c.y_quant);
4545 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
4546 assert_eq!(
4547 packet_a.entropy_checkpoints.len(),
4548 packet_c.entropy_checkpoints.len()
4549 );
4550 assert_ne!(packet_a.y_quant, packet_b.y_quant);
4551 }
4552 JpegSubsampling::Ybr444 => {
4553 let packet_a = build_fast444_packet(&jpeg_a.data).expect("first packet");
4554 let packet_b = build_fast444_packet(&jpeg_b.data).expect("second packet");
4555 let packet_c = build_fast444_packet(&jpeg_c.data).expect("third packet");
4556 assert_eq!(packet_a.y_quant, packet_c.y_quant);
4557 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
4558 assert_eq!(
4559 packet_a.entropy_checkpoints.len(),
4560 packet_c.entropy_checkpoints.len()
4561 );
4562 assert_ne!(packet_a.y_quant, packet_b.y_quant);
4563 }
4564 JpegSubsampling::Gray => panic!("table-mixed buffer helper expects YCbCr sampling"),
4565 }
4566
4567 let output = MetalBatchOutputBuffer::new_rgb8_tiles(&session, (scaled.w, scaled.h), 3)
4568 .expect("output buffer");
4569 let inputs = [
4570 jpeg_a.data.as_slice(),
4571 jpeg_b.data.as_slice(),
4572 jpeg_c.data.as_slice(),
4573 ];
4574 let expected_tiles = inputs
4575 .iter()
4576 .map(|input| {
4577 CpuDecoder::new(input)
4578 .expect("cpu decoder")
4579 .decode_region_scaled(
4580 PixelFormat::Rgb8,
4581 j2k_jpeg::Rect {
4582 x: roi.x,
4583 y: roi.y,
4584 w: roi.w,
4585 h: roi.h,
4586 },
4587 scale,
4588 )
4589 .expect("cpu region-scaled decode")
4590 .0
4591 })
4592 .collect::<Vec<_>>();
4593 assert_ne!(expected_tiles[0], expected_tiles[1]);
4594 assert_ne!(expected_tiles[0], expected_tiles[2]);
4595 assert_ne!(expected_tiles[1], expected_tiles[2]);
4596
4597 let surfaces = decode_rgb8_region_scaled_batch_into_metal_buffer_with_session(
4598 &inputs, roi, scale, &output, &session,
4599 )
4600 .expect("decode table-mixed region-scaled tiles into reusable output buffer");
4601
4602 assert_eq!(surfaces.len(), 3);
4603 for (index, surface) in surfaces.into_iter().enumerate() {
4604 let surface = surface.expect("surface");
4605 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
4606 assert_eq!(surface.dimensions(), (scaled.w, scaled.h));
4607 assert_eq!(surface.pixel_format(), PixelFormat::Rgb8);
4608 let (buffer, offset) = surface.metal_buffer().expect("metal buffer");
4609 assert!(std::ptr::eq(buffer.as_ref(), output.buffer()));
4610 assert_eq!(offset, index * output.tile_stride_bytes());
4611 assert_eq!(surface.as_bytes(), expected_tiles[index].as_slice());
4612 }
4613 }
4614
4615 #[cfg(target_os = "macos")]
4616 #[test]
4617 fn rgb8_table_mixed_fast420_region_scaled_buffer_batch_groups_resident_dispatches() {
4618 assert_table_mixed_region_scaled_buffer_groups_resident(
4619 JpegSubsampling::Ybr420,
4620 (128, 96),
4621 90,
4622 72,
4623 );
4624 }
4625
4626 #[cfg(target_os = "macos")]
4627 #[test]
4628 fn rgb8_table_mixed_fast422_region_scaled_buffer_batch_groups_resident_dispatches() {
4629 assert_table_mixed_region_scaled_buffer_groups_resident(
4630 JpegSubsampling::Ybr422,
4631 (128, 96),
4632 91,
4633 73,
4634 );
4635 }
4636
4637 #[cfg(target_os = "macos")]
4638 #[test]
4639 fn rgb8_table_mixed_fast444_region_scaled_buffer_batch_groups_resident_dispatches() {
4640 assert_table_mixed_region_scaled_buffer_groups_resident(
4641 JpegSubsampling::Ybr444,
4642 (96, 96),
4643 92,
4644 74,
4645 );
4646 }
4647
4648 #[cfg(target_os = "macos")]
4649 #[test]
4650 fn rgb8_fast444_region_scaled_batch_decode_can_write_into_reusable_metal_textures() {
4651 let session = MetalBackendSession::system_default().expect("Metal backend session");
4652 let roi = Rect {
4653 x: 1,
4654 y: 2,
4655 w: 5,
4656 h: 4,
4657 };
4658 let scale = Downscale::Quarter;
4659 let scaled = roi.scaled_covering(scale);
4660 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (scaled.w, scaled.h), 2)
4661 .expect("texture output");
4662 let inputs = [BASELINE_444, BASELINE_444];
4663 let (expected_rgb, _) = CpuDecoder::new(BASELINE_444)
4664 .expect("cpu decoder")
4665 .decode_region_scaled(
4666 PixelFormat::Rgb8,
4667 j2k_jpeg::Rect {
4668 x: roi.x,
4669 y: roi.y,
4670 w: roi.w,
4671 h: roi.h,
4672 },
4673 scale,
4674 )
4675 .expect("cpu region scaled decode");
4676 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
4677
4678 let tiles = decode_rgb8_region_scaled_batch_into_metal_textures_with_session(
4679 &inputs, roi, scale, &output, &session,
4680 )
4681 .expect("decode region scaled into reusable textures");
4682
4683 assert_eq!(tiles.len(), 2);
4684 assert_eq!(output.tile_capacity(), 2);
4685 assert_eq!(output.dimensions(), (scaled.w, scaled.h));
4686 assert_eq!(output.pixel_format(), PixelFormat::Rgba8);
4687 for (index, tile) in tiles.into_iter().enumerate() {
4688 let tile = tile.expect("texture tile");
4689 assert_eq!(tile.dimensions(), (scaled.w, scaled.h));
4690 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
4691 assert!(std::ptr::eq(
4692 tile.texture(),
4693 output.texture(index).expect("output texture")
4694 ));
4695 assert_eq!(
4696 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
4697 expected_rgba
4698 );
4699 }
4700 }
4701
4702 #[cfg(target_os = "macos")]
4703 #[test]
4704 fn metal_batch_output_buffer_ensure_reuses_matching_allocation_and_grows_capacity() {
4705 use metal::foreign_types::ForeignTypeRef;
4706
4707 let session = MetalBackendSession::system_default().expect("Metal backend session");
4708 let mut output =
4709 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (16, 16), 2).expect("output buffer");
4710 let original_buffer = output.buffer().as_ptr();
4711
4712 output
4713 .ensure_rgb8_tiles(&session, (16, 16), 1)
4714 .expect("ensure smaller matching output");
4715 assert_eq!(output.buffer().as_ptr(), original_buffer);
4716 assert_eq!(output.dimensions(), (16, 16));
4717 assert_eq!(output.tile_capacity(), 2);
4718
4719 output
4720 .ensure_rgb8_tiles(&session, (16, 16), 3)
4721 .expect("ensure larger output");
4722 assert_ne!(output.buffer().as_ptr(), original_buffer);
4723 assert_eq!(output.dimensions(), (16, 16));
4724 assert_eq!(output.tile_capacity(), 3);
4725 assert_eq!(
4726 output.byte_len(),
4727 16 * 16 * PixelFormat::Rgb8.bytes_per_pixel() * 3
4728 );
4729 }
4730
4731 #[cfg(target_os = "macos")]
4732 #[test]
4733 fn metal_batch_texture_output_ensure_reuses_matching_textures_and_grows_capacity() {
4734 use metal::foreign_types::ForeignTypeRef;
4735
4736 let session = MetalBackendSession::system_default().expect("Metal backend session");
4737 let mut output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (16, 16), 2)
4738 .expect("texture output");
4739 let original_texture = output.texture(0).expect("texture").as_ptr();
4740
4741 output
4742 .ensure_rgba8_tiles(&session, (16, 16), 1)
4743 .expect("ensure smaller matching texture output");
4744 assert_eq!(
4745 output.texture(0).expect("texture").as_ptr(),
4746 original_texture
4747 );
4748 assert_eq!(output.dimensions(), (16, 16));
4749 assert_eq!(output.tile_capacity(), 2);
4750
4751 output
4752 .ensure_rgba8_tiles(&session, (16, 16), 3)
4753 .expect("ensure larger texture output");
4754 assert_ne!(
4755 output.texture(0).expect("texture").as_ptr(),
4756 original_texture
4757 );
4758 assert_eq!(output.dimensions(), (16, 16));
4759 assert_eq!(output.tile_capacity(), 3);
4760 assert_eq!(output.pixel_format(), PixelFormat::Rgba8);
4761 }
4762
4763 #[cfg(target_os = "macos")]
4764 #[test]
4765 fn metal_batch_output_buffer_ensure_region_scaled_tiles_uses_scaled_roi_shape() {
4766 let session = MetalBackendSession::system_default().expect("Metal backend session");
4767 let roi = Rect {
4768 x: 4,
4769 y: 4,
4770 w: 10,
4771 h: 10,
4772 };
4773 let scaled = roi.scaled_covering(Downscale::Quarter);
4774 let mut output =
4775 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (1, 1), 1).expect("output buffer");
4776
4777 output
4778 .ensure_rgb8_region_scaled_tiles(&session, roi, Downscale::Quarter, 2)
4779 .expect("ensure region-scaled output");
4780
4781 assert_eq!(output.dimensions(), (scaled.w, scaled.h));
4782 assert_eq!(output.tile_capacity(), 2);
4783 assert_eq!(
4784 output.tile_stride_bytes(),
4785 scaled.w as usize * scaled.h as usize * PixelFormat::Rgb8.bytes_per_pixel()
4786 );
4787 }
4788
4789 #[cfg(target_os = "macos")]
4790 #[test]
4791 fn metal_batch_texture_output_ensure_scaled_tiles_uses_scaled_full_shape() {
4792 let session = MetalBackendSession::system_default().expect("Metal backend session");
4793 let mut output =
4794 MetalBatchTextureOutput::new_rgba8_tiles(&session, (1, 1), 1).expect("texture output");
4795
4796 output
4797 .ensure_rgba8_scaled_tiles(&session, (16, 16), Downscale::Quarter, 2)
4798 .expect("ensure scaled texture output");
4799
4800 assert_eq!(output.dimensions(), (4, 4));
4801 assert_eq!(output.tile_capacity(), 2);
4802 assert_eq!(output.pixel_format(), PixelFormat::Rgba8);
4803 }
4804
4805 #[cfg(target_os = "macos")]
4806 #[test]
4807 fn metal_batch_outputs_can_ensure_from_resident_batch_report() {
4808 let session = MetalBackendSession::system_default().expect("Metal backend session");
4809 let first = Decoder::new(BASELINE_420).expect("first decoder");
4810 let second = Decoder::new(BASELINE_420).expect("second decoder");
4811 let decoders = [&first, &second];
4812 let report = Codec::inspect_rgb8_decoder_batch_metal_output(
4813 &decoders,
4814 j2k_jpeg::JpegDecodeOp::Scaled(Downscale::Quarter),
4815 );
4816 assert!(report.eligibility.eligible);
4817
4818 let mut buffer =
4819 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (1, 1), 1).expect("output buffer");
4820 let mut textures =
4821 MetalBatchTextureOutput::new_rgba8_tiles(&session, (1, 1), 1).expect("texture output");
4822
4823 buffer
4824 .ensure_rgb8_batch_report(&session, &report)
4825 .expect("ensure buffer from report");
4826 textures
4827 .ensure_rgba8_batch_report(&session, &report)
4828 .expect("ensure textures from report");
4829
4830 assert_eq!(buffer.dimensions(), (4, 4));
4831 assert_eq!(buffer.tile_capacity(), 2);
4832 assert_eq!(
4833 buffer.tile_stride_bytes(),
4834 4 * 4 * PixelFormat::Rgb8.bytes_per_pixel()
4835 );
4836 assert_eq!(textures.dimensions(), (4, 4));
4837 assert_eq!(textures.tile_capacity(), 2);
4838 assert_eq!(textures.pixel_format(), PixelFormat::Rgba8);
4839 }
4840
4841 #[cfg(target_os = "macos")]
4842 #[test]
4843 fn metal_batch_outputs_reject_ineligible_report_without_resizing() {
4844 let session = MetalBackendSession::system_default().expect("Metal backend session");
4845 let first = Decoder::new(BASELINE_420).expect("first decoder");
4846 let second = Decoder::new(BASELINE_444).expect("second decoder");
4847 let decoders = [&first, &second];
4848 let report =
4849 Codec::inspect_rgb8_decoder_batch_metal_output(&decoders, j2k_jpeg::JpegDecodeOp::Full);
4850 assert!(!report.eligibility.eligible);
4851
4852 let mut buffer =
4853 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (1, 1), 1).expect("output buffer");
4854 let mut textures =
4855 MetalBatchTextureOutput::new_rgba8_tiles(&session, (1, 1), 1).expect("texture output");
4856
4857 let buffer_err = buffer
4858 .ensure_rgb8_batch_report(&session, &report)
4859 .expect_err("ineligible report should reject buffer ensure");
4860 let texture_err = textures
4861 .ensure_rgba8_batch_report(&session, &report)
4862 .expect_err("ineligible report should reject texture ensure");
4863
4864 assert!(matches!(
4865 buffer_err,
4866 Error::UnsupportedMetalRequest { reason }
4867 if reason.contains("matching output dimensions")
4868 ));
4869 assert!(matches!(
4870 texture_err,
4871 Error::UnsupportedMetalRequest { reason }
4872 if reason.contains("matching output dimensions")
4873 ));
4874 assert_eq!(buffer.dimensions(), (1, 1));
4875 assert_eq!(buffer.tile_capacity(), 1);
4876 assert_eq!(textures.dimensions(), (1, 1));
4877 assert_eq!(textures.tile_capacity(), 1);
4878 }
4879
4880 #[cfg(target_os = "macos")]
4881 #[test]
4882 fn warm_session_reuses_private_intermediate_buffers_for_reusable_output_batches() {
4883 let session = MetalBackendSession::system_default().expect("Metal backend session");
4884 let output =
4885 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (16, 16), 2).expect("output buffer");
4886 let inputs = [BASELINE_420, BASELINE_420];
4887
4888 compute::reset_jpeg_private_buffer_allocations_for_test();
4889 let first = decode_rgb8_batch_into_metal_buffer_with_session(&inputs, &output, &session)
4890 .expect("first decode");
4891 for surface in first {
4892 assert_eq!(
4893 surface.expect("surface").residency(),
4894 SurfaceResidency::MetalResidentDecode
4895 );
4896 }
4897 let allocations_after_first = compute::jpeg_private_buffer_allocations_for_test();
4898
4899 let second = decode_rgb8_batch_into_metal_buffer_with_session(&inputs, &output, &session)
4900 .expect("second decode");
4901 for surface in second {
4902 assert_eq!(
4903 surface.expect("surface").residency(),
4904 SurfaceResidency::MetalResidentDecode
4905 );
4906 }
4907
4908 assert!(
4909 allocations_after_first > 0,
4910 "first batch should allocate private intermediate buffers"
4911 );
4912 assert_eq!(
4913 compute::jpeg_private_buffer_allocations_for_test(),
4914 allocations_after_first,
4915 "warm session batch should reuse private intermediate buffers"
4916 );
4917 }
4918
4919 #[cfg(target_os = "macos")]
4920 #[test]
4921 fn warm_session_reuses_shared_upload_buffers_for_reusable_output_batches() {
4922 let session = MetalBackendSession::system_default().expect("Metal backend session");
4923 let output =
4924 MetalBatchOutputBuffer::new_rgb8_tiles(&session, (16, 16), 2).expect("output buffer");
4925 let inputs = [BASELINE_420, BASELINE_420];
4926
4927 compute::reset_jpeg_shared_buffer_allocations_for_test();
4928 decode_rgb8_batch_into_metal_buffer_with_session(&inputs, &output, &session)
4929 .expect("first decode");
4930 let allocations_after_first = compute::jpeg_shared_buffer_allocations_for_test();
4931
4932 decode_rgb8_batch_into_metal_buffer_with_session(&inputs, &output, &session)
4933 .expect("second decode");
4934
4935 assert!(
4936 allocations_after_first > 0,
4937 "first batch should allocate shared upload/status buffers"
4938 );
4939 assert_eq!(
4940 compute::jpeg_shared_buffer_allocations_for_test(),
4941 allocations_after_first,
4942 "warm session batch should reuse shared upload/status buffers"
4943 );
4944 }
4945
4946 #[cfg(target_os = "macos")]
4947 fn patterned_index_byte(index: usize) -> u8 {
4948 u8::try_from(index % 256).expect("modulo 256 fits in u8")
4949 }
4950
4951 #[cfg(target_os = "macos")]
4952 fn rgb_to_rgba_opaque(rgb: &[u8]) -> Vec<u8> {
4953 let mut rgba = Vec::with_capacity(rgb.len() / 3 * 4);
4954 for pixel in rgb.chunks_exact(3) {
4955 rgba.extend_from_slice(pixel);
4956 rgba.push(u8::MAX);
4957 }
4958 rgba
4959 }
4960
4961 #[cfg(target_os = "macos")]
4962 fn download_rgba8_texture(
4963 session: &MetalBackendSession,
4964 texture: &metal::TextureRef,
4965 dimensions: (u32, u32),
4966 ) -> Vec<u8> {
4967 let row_bytes = dimensions.0 as usize * PixelFormat::Rgba8.bytes_per_pixel();
4968 let byte_len = row_bytes * dimensions.1 as usize;
4969 let buffer = session.device().new_buffer(
4970 byte_len as u64,
4971 metal::MTLResourceOptions::StorageModeShared,
4972 );
4973 let queue = session.device().new_command_queue();
4974 let command_buffer = queue.new_command_buffer();
4975 let blit = command_buffer.new_blit_command_encoder();
4976 blit.copy_from_texture_to_buffer(
4977 texture,
4978 0,
4979 0,
4980 metal::MTLOrigin { x: 0, y: 0, z: 0 },
4981 metal::MTLSize::new(u64::from(dimensions.0), u64::from(dimensions.1), 1),
4982 &buffer,
4983 0,
4984 row_bytes as u64,
4985 byte_len as u64,
4986 metal::MTLBlitOption::None,
4987 );
4988 blit.end_encoding();
4989 command_buffer.commit();
4990 command_buffer.wait_until_completed();
4991
4992 unsafe { core::slice::from_raw_parts(buffer.contents().cast::<u8>(), byte_len).to_vec() }
4994 }
4995
4996 #[cfg(target_os = "macos")]
4997 #[test]
4998 fn rgb8_fast444_batch_decode_can_write_into_reusable_metal_textures() {
4999 let session = MetalBackendSession::system_default().expect("Metal backend session");
5000 let output =
5001 MetalBatchTextureOutput::new_rgba8_tiles(&session, (8, 8), 2).expect("texture output");
5002 let inputs = [BASELINE_444, BASELINE_444];
5003 let (expected_rgb, _) = CpuDecoder::new(BASELINE_444)
5004 .expect("cpu decoder")
5005 .decode(PixelFormat::Rgb8)
5006 .expect("cpu decode");
5007 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5008
5009 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
5010 .expect("decode into reusable textures");
5011
5012 assert_eq!(tiles.len(), 2);
5013 assert_eq!(output.tile_capacity(), 2);
5014 assert_eq!(output.dimensions(), (8, 8));
5015 assert_eq!(output.pixel_format(), PixelFormat::Rgba8);
5016 for (index, tile) in tiles.into_iter().enumerate() {
5017 let tile = tile.expect("texture tile");
5018 assert_eq!(tile.dimensions(), (8, 8));
5019 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5020 assert!(std::ptr::eq(
5021 tile.texture(),
5022 output.texture(index).expect("output texture")
5023 ));
5024 assert_eq!(
5025 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5026 expected_rgba
5027 );
5028 }
5029 }
5030
5031 #[cfg(target_os = "macos")]
5032 #[test]
5033 fn rgb8_decoder_batch_resizes_reusable_metal_textures() {
5034 let session = MetalBackendSession::system_default().expect("Metal backend session");
5035 let mut output =
5036 MetalBatchTextureOutput::new_rgba8_tiles(&session, (1, 1), 1).expect("texture output");
5037 let first = Decoder::new(BASELINE_420).expect("first decoder");
5038 let second = Decoder::new(BASELINE_420).expect("second decoder");
5039 let decoders = [&first, &second];
5040 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
5041 .expect("cpu decoder")
5042 .decode(PixelFormat::Rgb8)
5043 .expect("cpu decode");
5044 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5045
5046 let tiles = Codec::decode_rgb8_decoder_batch_into_resizable_metal_textures_with_session(
5047 &decoders,
5048 &mut output,
5049 &session,
5050 )
5051 .expect("decode cached decoder batch into resizable reusable textures");
5052
5053 assert_eq!(output.dimensions(), (16, 16));
5054 assert_eq!(output.tile_capacity(), 2);
5055 assert_eq!(tiles.len(), 2);
5056 for (index, tile) in tiles.into_iter().enumerate() {
5057 let tile = tile.expect("texture tile");
5058 assert_eq!(tile.dimensions(), (16, 16));
5059 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5060 assert!(std::ptr::eq(
5061 tile.texture(),
5062 output.texture(index).expect("output texture")
5063 ));
5064 assert_eq!(
5065 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5066 expected_rgba
5067 );
5068 }
5069 }
5070
5071 #[cfg(target_os = "macos")]
5072 #[test]
5073 fn rgb8_decoder_batch_can_write_into_fixed_metal_textures() {
5074 let session = MetalBackendSession::system_default().expect("Metal backend session");
5075 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (16, 16), 2)
5076 .expect("texture output");
5077 let first = Decoder::new(BASELINE_420).expect("first decoder");
5078 let second = Decoder::new(BASELINE_420).expect("second decoder");
5079 let decoders = [&first, &second];
5080 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
5081 .expect("cpu decoder")
5082 .decode(PixelFormat::Rgb8)
5083 .expect("cpu decode");
5084 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5085
5086 let tiles = decode_rgb8_decoder_batch_into_metal_textures_with_session(
5087 &decoders, &output, &session,
5088 )
5089 .expect("decode cached decoder batch into fixed reusable textures");
5090
5091 assert_eq!(tiles.len(), 2);
5092 assert_eq!(output.dimensions(), (16, 16));
5093 assert_eq!(output.tile_capacity(), 2);
5094 for (index, tile) in tiles.into_iter().enumerate() {
5095 let tile = tile.expect("texture tile");
5096 assert_eq!(tile.dimensions(), (16, 16));
5097 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5098 assert!(std::ptr::eq(
5099 tile.texture(),
5100 output.texture(index).expect("output texture")
5101 ));
5102 assert_eq!(
5103 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5104 expected_rgba
5105 );
5106 }
5107 }
5108
5109 #[cfg(target_os = "macos")]
5110 #[test]
5111 fn rgb8_decoder_batch_rejects_mixed_output_dimensions_without_resizing_textures() {
5112 let session = MetalBackendSession::system_default().expect("Metal backend session");
5113 let mut output =
5114 MetalBatchTextureOutput::new_rgba8_tiles(&session, (1, 1), 1).expect("texture output");
5115 let first = Decoder::new(BASELINE_420).expect("first decoder");
5116 let second = Decoder::new(BASELINE_444).expect("second decoder");
5117 let decoders = [&first, &second];
5118
5119 let Err(err) = Codec::decode_rgb8_decoder_batch_into_resizable_metal_textures_with_session(
5120 &decoders,
5121 &mut output,
5122 &session,
5123 ) else {
5124 panic!("mixed output dimensions should be rejected");
5125 };
5126
5127 assert!(matches!(err, Error::UnsupportedMetalRequest { .. }));
5128 assert_eq!(output.dimensions(), (1, 1));
5129 assert_eq!(output.tile_capacity(), 1);
5130 }
5131
5132 #[cfg(target_os = "macos")]
5133 #[test]
5134 fn rgb8_decoder_batch_rejects_mixed_sampling_without_resizing_textures() {
5135 let session = MetalBackendSession::system_default().expect("Metal backend session");
5136 let mut output =
5137 MetalBatchTextureOutput::new_rgba8_tiles(&session, (1, 1), 1).expect("texture output");
5138 let rgb = j2k_test_support::patterned_rgb8(16, 16);
5139 let fast420 = encode_jpeg_baseline(
5140 JpegSamples::Rgb8 {
5141 data: &rgb,
5142 width: 16,
5143 height: 16,
5144 },
5145 JpegEncodeOptions {
5146 quality: 90,
5147 subsampling: JpegSubsampling::Ybr420,
5148 restart_interval: None,
5149 backend: JpegBackend::Cpu,
5150 },
5151 )
5152 .expect("encode fast420 jpeg");
5153 let fast444 = encode_jpeg_baseline(
5154 JpegSamples::Rgb8 {
5155 data: &rgb,
5156 width: 16,
5157 height: 16,
5158 },
5159 JpegEncodeOptions {
5160 quality: 90,
5161 subsampling: JpegSubsampling::Ybr444,
5162 restart_interval: None,
5163 backend: JpegBackend::Cpu,
5164 },
5165 )
5166 .expect("encode fast444 jpeg");
5167 let first = Decoder::new(&fast420.data).expect("first decoder");
5168 let second = Decoder::new(&fast444.data).expect("second decoder");
5169 let decoders = [&first, &second];
5170
5171 let Err(err) = Codec::decode_rgb8_decoder_batch_into_resizable_metal_textures_with_session(
5172 &decoders,
5173 &mut output,
5174 &session,
5175 ) else {
5176 panic!("mixed sampling should be rejected");
5177 };
5178
5179 assert!(matches!(
5180 err,
5181 Error::UnsupportedMetalRequest { reason }
5182 if reason.contains("same fast-packet sampling family")
5183 ));
5184 assert_eq!(output.dimensions(), (1, 1));
5185 assert_eq!(output.tile_capacity(), 1);
5186 }
5187
5188 #[cfg(target_os = "macos")]
5189 #[test]
5190 fn rgb8_scaled_batch_decode_can_write_into_reusable_metal_textures() {
5191 let session = MetalBackendSession::system_default().expect("Metal backend session");
5192 let scale = Downscale::Quarter;
5193 let output =
5194 MetalBatchTextureOutput::new_rgba8_tiles(&session, (4, 4), 2).expect("texture output");
5195 let inputs = [BASELINE_420, BASELINE_420];
5196 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
5197 .expect("cpu decoder")
5198 .decode_scaled(PixelFormat::Rgb8, scale)
5199 .expect("cpu scaled decode");
5200 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5201
5202 let tiles = decode_rgb8_scaled_batch_into_metal_textures_with_session(
5203 &inputs, scale, &output, &session,
5204 )
5205 .expect("decode scaled into reusable textures");
5206
5207 assert_eq!(tiles.len(), 2);
5208 assert_eq!(output.tile_capacity(), 2);
5209 assert_eq!(output.dimensions(), (4, 4));
5210 assert_eq!(output.pixel_format(), PixelFormat::Rgba8);
5211 for (index, tile) in tiles.into_iter().enumerate() {
5212 let tile = tile.expect("texture tile");
5213 assert_eq!(tile.dimensions(), (4, 4));
5214 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5215 assert!(std::ptr::eq(
5216 tile.texture(),
5217 output.texture(index).expect("output texture")
5218 ));
5219 assert_eq!(
5220 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5221 expected_rgba
5222 );
5223 }
5224 }
5225
5226 #[cfg(target_os = "macos")]
5227 #[test]
5228 fn rgb8_scaled_batch_decode_resizes_reusable_metal_textures() {
5229 let session = MetalBackendSession::system_default().expect("Metal backend session");
5230 let scale = Downscale::Quarter;
5231 let mut output =
5232 MetalBatchTextureOutput::new_rgba8_tiles(&session, (1, 1), 1).expect("texture output");
5233 let inputs = [BASELINE_420, BASELINE_420];
5234 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
5235 .expect("cpu decoder")
5236 .decode_scaled(PixelFormat::Rgb8, scale)
5237 .expect("cpu scaled decode");
5238 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5239
5240 let tiles = decode_rgb8_scaled_batch_into_resizable_metal_textures_with_session(
5241 &inputs,
5242 scale,
5243 &mut output,
5244 &session,
5245 )
5246 .expect("decode scaled into resizable reusable textures");
5247
5248 assert_eq!(output.dimensions(), (4, 4));
5249 assert_eq!(output.tile_capacity(), 2);
5250 assert_eq!(tiles.len(), 2);
5251 for (index, tile) in tiles.into_iter().enumerate() {
5252 let tile = tile.expect("texture tile");
5253 assert_eq!(tile.dimensions(), (4, 4));
5254 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5255 assert!(std::ptr::eq(
5256 tile.texture(),
5257 output.texture(index).expect("output texture")
5258 ));
5259 assert_eq!(
5260 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5261 expected_rgba
5262 );
5263 }
5264 }
5265
5266 #[cfg(target_os = "macos")]
5267 #[test]
5268 fn rgb8_decoder_scaled_batch_resizes_reusable_metal_textures() {
5269 let session = MetalBackendSession::system_default().expect("Metal backend session");
5270 let scale = Downscale::Quarter;
5271 let mut output =
5272 MetalBatchTextureOutput::new_rgba8_tiles(&session, (1, 1), 1).expect("texture output");
5273 let first = Decoder::new(BASELINE_420).expect("first decoder");
5274 let second = Decoder::new(BASELINE_420).expect("second decoder");
5275 let decoders = [&first, &second];
5276 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
5277 .expect("cpu decoder")
5278 .decode_scaled(PixelFormat::Rgb8, scale)
5279 .expect("cpu scaled decode");
5280 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5281
5282 let tiles = decode_rgb8_decoder_scaled_batch_into_resizable_metal_textures_with_session(
5283 &decoders,
5284 scale,
5285 &mut output,
5286 &session,
5287 )
5288 .expect("decode cached decoder scaled batch into resizable reusable textures");
5289
5290 assert_eq!(output.dimensions(), (4, 4));
5291 assert_eq!(output.tile_capacity(), 2);
5292 assert_eq!(tiles.len(), 2);
5293 for (index, tile) in tiles.into_iter().enumerate() {
5294 let tile = tile.expect("texture tile");
5295 assert_eq!(tile.dimensions(), (4, 4));
5296 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5297 assert!(std::ptr::eq(
5298 tile.texture(),
5299 output.texture(index).expect("output texture")
5300 ));
5301 assert_eq!(
5302 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5303 expected_rgba
5304 );
5305 }
5306 }
5307
5308 #[cfg(target_os = "macos")]
5309 #[test]
5310 fn rgb8_decoder_scaled_batch_can_write_into_fixed_metal_textures() {
5311 let session = MetalBackendSession::system_default().expect("Metal backend session");
5312 let scale = Downscale::Quarter;
5313 let output =
5314 MetalBatchTextureOutput::new_rgba8_tiles(&session, (4, 4), 2).expect("texture output");
5315 let first = Decoder::new(BASELINE_420).expect("first decoder");
5316 let second = Decoder::new(BASELINE_420).expect("second decoder");
5317 let decoders = [&first, &second];
5318 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
5319 .expect("cpu decoder")
5320 .decode_scaled(PixelFormat::Rgb8, scale)
5321 .expect("cpu scaled decode");
5322 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5323
5324 let tiles = decode_rgb8_decoder_scaled_batch_into_metal_textures_with_session(
5325 &decoders, scale, &output, &session,
5326 )
5327 .expect("decode cached decoder scaled batch into fixed reusable textures");
5328
5329 assert_eq!(tiles.len(), 2);
5330 assert_eq!(output.dimensions(), (4, 4));
5331 assert_eq!(output.tile_capacity(), 2);
5332 for (index, tile) in tiles.into_iter().enumerate() {
5333 let tile = tile.expect("texture tile");
5334 assert_eq!(tile.dimensions(), (4, 4));
5335 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5336 assert!(std::ptr::eq(
5337 tile.texture(),
5338 output.texture(index).expect("output texture")
5339 ));
5340 assert_eq!(
5341 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5342 expected_rgba
5343 );
5344 }
5345 }
5346
5347 #[cfg(target_os = "macos")]
5348 #[test]
5349 fn rgb8_fast422_region_scaled_batch_decode_can_write_into_reusable_metal_textures() {
5350 let session = MetalBackendSession::system_default().expect("Metal backend session");
5351 let roi = Rect {
5352 x: 1,
5353 y: 1,
5354 w: 9,
5355 h: 6,
5356 };
5357 let scale = Downscale::Half;
5358 let scaled = roi.scaled_covering(scale);
5359 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (scaled.w, scaled.h), 2)
5360 .expect("texture output");
5361 let inputs = [BASELINE_422, BASELINE_422];
5362 let (expected_rgb, _) = CpuDecoder::new(BASELINE_422)
5363 .expect("cpu decoder")
5364 .decode_region_scaled(
5365 PixelFormat::Rgb8,
5366 j2k_jpeg::Rect {
5367 x: roi.x,
5368 y: roi.y,
5369 w: roi.w,
5370 h: roi.h,
5371 },
5372 scale,
5373 )
5374 .expect("cpu region scaled decode");
5375 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5376
5377 let tiles = decode_rgb8_region_scaled_batch_into_metal_textures_with_session(
5378 &inputs, roi, scale, &output, &session,
5379 )
5380 .expect("decode region scaled into reusable textures");
5381
5382 assert_eq!(tiles.len(), 2);
5383 assert_eq!(output.tile_capacity(), 2);
5384 assert_eq!(output.dimensions(), (scaled.w, scaled.h));
5385 assert_eq!(output.pixel_format(), PixelFormat::Rgba8);
5386 for (index, tile) in tiles.into_iter().enumerate() {
5387 let tile = tile.expect("texture tile");
5388 assert_eq!(tile.dimensions(), (scaled.w, scaled.h));
5389 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5390 assert!(std::ptr::eq(
5391 tile.texture(),
5392 output.texture(index).expect("output texture")
5393 ));
5394 assert_eq!(
5395 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5396 expected_rgba
5397 );
5398 }
5399 }
5400
5401 #[cfg(target_os = "macos")]
5402 #[test]
5403 fn rgb8_table_mixed_fast422_region_scaled_texture_batch_groups_resident_dispatches() {
5404 let session = MetalBackendSession::system_default().expect("Metal backend session");
5405 let dimensions = (128, 96);
5406 let roi = Rect {
5407 x: 0,
5408 y: 0,
5409 w: dimensions.0,
5410 h: dimensions.1,
5411 };
5412 let scale = Downscale::Half;
5413 let scaled = roi.scaled_covering(scale);
5414 let rgb_a = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
5415 let mut rgb_b = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
5416 let mut rgb_c = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
5417 for (index, pixel) in rgb_b.chunks_exact_mut(3).enumerate() {
5418 let delta = patterned_index_byte(index)
5419 .wrapping_mul(41)
5420 .wrapping_add(29);
5421 pixel[0] ^= delta.rotate_left(1);
5422 pixel[1] = pixel[1].wrapping_add(delta);
5423 pixel[2] = pixel[2].wrapping_sub(delta.rotate_right(2));
5424 }
5425 for (index, pixel) in rgb_c.chunks_exact_mut(3).enumerate() {
5426 let delta = patterned_index_byte(index).wrapping_mul(59).wrapping_add(3);
5427 pixel[0] = pixel[0].wrapping_sub(delta.rotate_left(2));
5428 pixel[1] ^= delta.rotate_right(1);
5429 pixel[2] = pixel[2].wrapping_add(delta);
5430 }
5431
5432 let jpeg_a = encode_jpeg_baseline(
5433 JpegSamples::Rgb8 {
5434 data: &rgb_a,
5435 width: dimensions.0,
5436 height: dimensions.1,
5437 },
5438 JpegEncodeOptions {
5439 quality: 90,
5440 subsampling: JpegSubsampling::Ybr422,
5441 restart_interval: None,
5442 backend: JpegBackend::Cpu,
5443 },
5444 )
5445 .expect("encode first fast422 region-scaled table group jpeg");
5446 let jpeg_b = encode_jpeg_baseline(
5447 JpegSamples::Rgb8 {
5448 data: &rgb_b,
5449 width: dimensions.0,
5450 height: dimensions.1,
5451 },
5452 JpegEncodeOptions {
5453 quality: 71,
5454 subsampling: JpegSubsampling::Ybr422,
5455 restart_interval: None,
5456 backend: JpegBackend::Cpu,
5457 },
5458 )
5459 .expect("encode second fast422 region-scaled table group jpeg");
5460 let jpeg_c = encode_jpeg_baseline(
5461 JpegSamples::Rgb8 {
5462 data: &rgb_c,
5463 width: dimensions.0,
5464 height: dimensions.1,
5465 },
5466 JpegEncodeOptions {
5467 quality: 90,
5468 subsampling: JpegSubsampling::Ybr422,
5469 restart_interval: None,
5470 backend: JpegBackend::Cpu,
5471 },
5472 )
5473 .expect("encode third fast422 region-scaled table group jpeg");
5474 let packet_a = build_fast422_packet(&jpeg_a.data).expect("first fast422 packet");
5475 let packet_b = build_fast422_packet(&jpeg_b.data).expect("second fast422 packet");
5476 let packet_c = build_fast422_packet(&jpeg_c.data).expect("third fast422 packet");
5477 assert_eq!(packet_a.y_quant, packet_c.y_quant);
5478 assert_eq!(packet_a.cb_quant, packet_c.cb_quant);
5479 assert_eq!(packet_a.cr_quant, packet_c.cr_quant);
5480 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
5481 assert_eq!(packet_a.y_ac_table, packet_c.y_ac_table);
5482 assert_eq!(
5483 packet_a.entropy_checkpoints.len(),
5484 packet_c.entropy_checkpoints.len()
5485 );
5486 assert_ne!(packet_a.y_quant, packet_b.y_quant);
5487
5488 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (scaled.w, scaled.h), 3)
5489 .expect("texture output");
5490 let inputs = [
5491 jpeg_a.data.as_slice(),
5492 jpeg_b.data.as_slice(),
5493 jpeg_c.data.as_slice(),
5494 ];
5495 let (expected_rgb_a, _) = CpuDecoder::new(&jpeg_a.data)
5496 .expect("first cpu decoder")
5497 .decode_region_scaled(
5498 PixelFormat::Rgb8,
5499 j2k_jpeg::Rect {
5500 x: roi.x,
5501 y: roi.y,
5502 w: roi.w,
5503 h: roi.h,
5504 },
5505 scale,
5506 )
5507 .expect("first cpu region scaled decode");
5508 let (expected_rgb_b, _) = CpuDecoder::new(&jpeg_b.data)
5509 .expect("second cpu decoder")
5510 .decode_region_scaled(
5511 PixelFormat::Rgb8,
5512 j2k_jpeg::Rect {
5513 x: roi.x,
5514 y: roi.y,
5515 w: roi.w,
5516 h: roi.h,
5517 },
5518 scale,
5519 )
5520 .expect("second cpu region scaled decode");
5521 let (expected_rgb_c, _) = CpuDecoder::new(&jpeg_c.data)
5522 .expect("third cpu decoder")
5523 .decode_region_scaled(
5524 PixelFormat::Rgb8,
5525 j2k_jpeg::Rect {
5526 x: roi.x,
5527 y: roi.y,
5528 w: roi.w,
5529 h: roi.h,
5530 },
5531 scale,
5532 )
5533 .expect("third cpu region scaled decode");
5534 let expected_tiles = [
5535 rgb_to_rgba_opaque(&expected_rgb_a),
5536 rgb_to_rgba_opaque(&expected_rgb_b),
5537 rgb_to_rgba_opaque(&expected_rgb_c),
5538 ];
5539 assert_ne!(expected_tiles[0], expected_tiles[1]);
5540 assert_ne!(expected_tiles[0], expected_tiles[2]);
5541 assert_ne!(expected_tiles[1], expected_tiles[2]);
5542
5543 let tiles = decode_rgb8_region_scaled_batch_into_metal_textures_with_session(
5544 &inputs, roi, scale, &output, &session,
5545 )
5546 .expect("decode table-mixed fast422 region-scaled tiles into reusable textures");
5547
5548 assert_eq!(tiles.len(), 3);
5549 for (index, tile) in tiles.into_iter().enumerate() {
5550 let tile = tile.expect("texture tile");
5551 assert_eq!(tile.dimensions(), (scaled.w, scaled.h));
5552 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5553 assert!(std::ptr::eq(
5554 tile.texture(),
5555 output.texture(index).expect("output texture")
5556 ));
5557 let actual_rgba = download_rgba8_texture(&session, tile.texture(), tile.dimensions());
5558 assert_eq!(actual_rgba.as_slice(), expected_tiles[index].as_slice());
5559 }
5560 }
5561
5562 #[cfg(target_os = "macos")]
5563 #[test]
5564 fn rgb8_table_mixed_fast444_region_scaled_texture_batch_groups_resident_dispatches() {
5565 let session = MetalBackendSession::system_default().expect("Metal backend session");
5566 let dimensions = (96, 96);
5567 let roi = Rect {
5568 x: 0,
5569 y: 0,
5570 w: dimensions.0,
5571 h: dimensions.1,
5572 };
5573 let scale = Downscale::Half;
5574 let scaled = roi.scaled_covering(scale);
5575 let rgb_a = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
5576 let mut rgb_b = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
5577 let mut rgb_c = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
5578 for (index, pixel) in rgb_b.chunks_exact_mut(3).enumerate() {
5579 let delta = patterned_index_byte(index)
5580 .wrapping_mul(61)
5581 .wrapping_add(13);
5582 pixel[0] = pixel[0].wrapping_add(delta);
5583 pixel[1] ^= delta.rotate_left(1);
5584 pixel[2] = pixel[2].wrapping_sub(delta.rotate_right(2));
5585 }
5586 for (index, pixel) in rgb_c.chunks_exact_mut(3).enumerate() {
5587 let delta = patterned_index_byte(index)
5588 .wrapping_mul(67)
5589 .wrapping_add(31);
5590 pixel[0] = pixel[0].wrapping_sub(delta.rotate_left(2));
5591 pixel[1] = pixel[1].wrapping_add(delta.rotate_right(1));
5592 pixel[2] ^= delta;
5593 }
5594
5595 let jpeg_a = encode_jpeg_baseline(
5596 JpegSamples::Rgb8 {
5597 data: &rgb_a,
5598 width: dimensions.0,
5599 height: dimensions.1,
5600 },
5601 JpegEncodeOptions {
5602 quality: 91,
5603 subsampling: JpegSubsampling::Ybr444,
5604 restart_interval: None,
5605 backend: JpegBackend::Cpu,
5606 },
5607 )
5608 .expect("encode first fast444 region-scaled table group jpeg");
5609 let jpeg_b = encode_jpeg_baseline(
5610 JpegSamples::Rgb8 {
5611 data: &rgb_b,
5612 width: dimensions.0,
5613 height: dimensions.1,
5614 },
5615 JpegEncodeOptions {
5616 quality: 70,
5617 subsampling: JpegSubsampling::Ybr444,
5618 restart_interval: None,
5619 backend: JpegBackend::Cpu,
5620 },
5621 )
5622 .expect("encode second fast444 region-scaled table group jpeg");
5623 let jpeg_c = encode_jpeg_baseline(
5624 JpegSamples::Rgb8 {
5625 data: &rgb_c,
5626 width: dimensions.0,
5627 height: dimensions.1,
5628 },
5629 JpegEncodeOptions {
5630 quality: 91,
5631 subsampling: JpegSubsampling::Ybr444,
5632 restart_interval: None,
5633 backend: JpegBackend::Cpu,
5634 },
5635 )
5636 .expect("encode third fast444 region-scaled table group jpeg");
5637 let packet_a = build_fast444_packet(&jpeg_a.data).expect("first fast444 packet");
5638 let packet_b = build_fast444_packet(&jpeg_b.data).expect("second fast444 packet");
5639 let packet_c = build_fast444_packet(&jpeg_c.data).expect("third fast444 packet");
5640 assert_eq!(packet_a.y_quant, packet_c.y_quant);
5641 assert_eq!(packet_a.cb_quant, packet_c.cb_quant);
5642 assert_eq!(packet_a.cr_quant, packet_c.cr_quant);
5643 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
5644 assert_eq!(packet_a.y_ac_table, packet_c.y_ac_table);
5645 assert_eq!(
5646 packet_a.entropy_checkpoints.len(),
5647 packet_c.entropy_checkpoints.len()
5648 );
5649 assert_ne!(packet_a.y_quant, packet_b.y_quant);
5650
5651 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (scaled.w, scaled.h), 3)
5652 .expect("texture output");
5653 let inputs = [
5654 jpeg_a.data.as_slice(),
5655 jpeg_b.data.as_slice(),
5656 jpeg_c.data.as_slice(),
5657 ];
5658 let (expected_rgb_a, _) = CpuDecoder::new(&jpeg_a.data)
5659 .expect("first cpu decoder")
5660 .decode_region_scaled(
5661 PixelFormat::Rgb8,
5662 j2k_jpeg::Rect {
5663 x: roi.x,
5664 y: roi.y,
5665 w: roi.w,
5666 h: roi.h,
5667 },
5668 scale,
5669 )
5670 .expect("first cpu region scaled decode");
5671 let (expected_rgb_b, _) = CpuDecoder::new(&jpeg_b.data)
5672 .expect("second cpu decoder")
5673 .decode_region_scaled(
5674 PixelFormat::Rgb8,
5675 j2k_jpeg::Rect {
5676 x: roi.x,
5677 y: roi.y,
5678 w: roi.w,
5679 h: roi.h,
5680 },
5681 scale,
5682 )
5683 .expect("second cpu region scaled decode");
5684 let (expected_rgb_c, _) = CpuDecoder::new(&jpeg_c.data)
5685 .expect("third cpu decoder")
5686 .decode_region_scaled(
5687 PixelFormat::Rgb8,
5688 j2k_jpeg::Rect {
5689 x: roi.x,
5690 y: roi.y,
5691 w: roi.w,
5692 h: roi.h,
5693 },
5694 scale,
5695 )
5696 .expect("third cpu region scaled decode");
5697 let expected_tiles = [
5698 rgb_to_rgba_opaque(&expected_rgb_a),
5699 rgb_to_rgba_opaque(&expected_rgb_b),
5700 rgb_to_rgba_opaque(&expected_rgb_c),
5701 ];
5702 assert_ne!(expected_tiles[0], expected_tiles[1]);
5703 assert_ne!(expected_tiles[0], expected_tiles[2]);
5704 assert_ne!(expected_tiles[1], expected_tiles[2]);
5705
5706 let tiles = decode_rgb8_region_scaled_batch_into_metal_textures_with_session(
5707 &inputs, roi, scale, &output, &session,
5708 )
5709 .expect("decode table-mixed fast444 region-scaled tiles into reusable textures");
5710
5711 assert_eq!(tiles.len(), 3);
5712 for (index, tile) in tiles.into_iter().enumerate() {
5713 let tile = tile.expect("texture tile");
5714 assert_eq!(tile.dimensions(), (scaled.w, scaled.h));
5715 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5716 assert!(std::ptr::eq(
5717 tile.texture(),
5718 output.texture(index).expect("output texture")
5719 ));
5720 let actual_rgba = download_rgba8_texture(&session, tile.texture(), tile.dimensions());
5721 assert_eq!(actual_rgba.as_slice(), expected_tiles[index].as_slice());
5722 }
5723 }
5724
5725 #[cfg(target_os = "macos")]
5726 #[test]
5727 fn rgb8_fast420_region_scaled_batch_decode_can_write_into_reusable_metal_textures() {
5728 let session = MetalBackendSession::system_default().expect("Metal backend session");
5729 let roi = Rect {
5730 x: 1,
5731 y: 2,
5732 w: 10,
5733 h: 9,
5734 };
5735 let scale = Downscale::Quarter;
5736 let scaled = roi.scaled_covering(scale);
5737 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (scaled.w, scaled.h), 2)
5738 .expect("texture output");
5739 let inputs = [BASELINE_420, BASELINE_420];
5740 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
5741 .expect("cpu decoder")
5742 .decode_region_scaled(
5743 PixelFormat::Rgb8,
5744 j2k_jpeg::Rect {
5745 x: roi.x,
5746 y: roi.y,
5747 w: roi.w,
5748 h: roi.h,
5749 },
5750 scale,
5751 )
5752 .expect("cpu region scaled decode");
5753 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5754
5755 let tiles = decode_rgb8_region_scaled_batch_into_metal_textures_with_session(
5756 &inputs, roi, scale, &output, &session,
5757 )
5758 .expect("decode region scaled into reusable textures");
5759
5760 assert_eq!(tiles.len(), 2);
5761 assert_eq!(output.tile_capacity(), 2);
5762 assert_eq!(output.dimensions(), (scaled.w, scaled.h));
5763 assert_eq!(output.pixel_format(), PixelFormat::Rgba8);
5764 for (index, tile) in tiles.into_iter().enumerate() {
5765 let tile = tile.expect("texture tile");
5766 assert_eq!(tile.dimensions(), (scaled.w, scaled.h));
5767 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5768 assert!(std::ptr::eq(
5769 tile.texture(),
5770 output.texture(index).expect("output texture")
5771 ));
5772 assert_eq!(
5773 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5774 expected_rgba
5775 );
5776 }
5777 }
5778
5779 #[cfg(target_os = "macos")]
5780 #[test]
5781 fn rgb8_decoder_region_scaled_batch_resizes_reusable_metal_textures() {
5782 let session = MetalBackendSession::system_default().expect("Metal backend session");
5783 let roi = Rect {
5784 x: 1,
5785 y: 2,
5786 w: 10,
5787 h: 9,
5788 };
5789 let scale = Downscale::Quarter;
5790 let scaled = roi.scaled_covering(scale);
5791 let mut output =
5792 MetalBatchTextureOutput::new_rgba8_tiles(&session, (1, 1), 1).expect("texture output");
5793 let first = Decoder::new(BASELINE_420).expect("first decoder");
5794 let second = Decoder::new(BASELINE_420).expect("second decoder");
5795 let decoders = [&first, &second];
5796 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
5797 .expect("cpu decoder")
5798 .decode_region_scaled(
5799 PixelFormat::Rgb8,
5800 j2k_jpeg::Rect {
5801 x: roi.x,
5802 y: roi.y,
5803 w: roi.w,
5804 h: roi.h,
5805 },
5806 scale,
5807 )
5808 .expect("cpu region scaled decode");
5809 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5810
5811 let tiles =
5812 decode_rgb8_decoder_region_scaled_batch_into_resizable_metal_textures_with_session(
5813 &decoders,
5814 roi,
5815 scale,
5816 &mut output,
5817 &session,
5818 )
5819 .expect("decode cached decoder batch into resizable reusable textures");
5820
5821 assert_eq!(output.dimensions(), (scaled.w, scaled.h));
5822 assert_eq!(output.tile_capacity(), 2);
5823 assert_eq!(tiles.len(), 2);
5824 for (index, tile) in tiles.into_iter().enumerate() {
5825 let tile = tile.expect("texture tile");
5826 assert_eq!(tile.dimensions(), (scaled.w, scaled.h));
5827 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5828 assert!(std::ptr::eq(
5829 tile.texture(),
5830 output.texture(index).expect("output texture")
5831 ));
5832 assert_eq!(
5833 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5834 expected_rgba
5835 );
5836 }
5837 }
5838
5839 #[cfg(target_os = "macos")]
5840 #[test]
5841 fn rgb8_decoder_region_scaled_batch_can_write_into_fixed_metal_textures() {
5842 let session = MetalBackendSession::system_default().expect("Metal backend session");
5843 let roi = Rect {
5844 x: 1,
5845 y: 2,
5846 w: 10,
5847 h: 9,
5848 };
5849 let scale = Downscale::Quarter;
5850 let scaled = roi.scaled_covering(scale);
5851 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (scaled.w, scaled.h), 2)
5852 .expect("texture output");
5853 let first = Decoder::new(BASELINE_420).expect("first decoder");
5854 let second = Decoder::new(BASELINE_420).expect("second decoder");
5855 let decoders = [&first, &second];
5856 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
5857 .expect("cpu decoder")
5858 .decode_region_scaled(
5859 PixelFormat::Rgb8,
5860 j2k_jpeg::Rect {
5861 x: roi.x,
5862 y: roi.y,
5863 w: roi.w,
5864 h: roi.h,
5865 },
5866 scale,
5867 )
5868 .expect("cpu region scaled decode");
5869 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5870
5871 let tiles = decode_rgb8_decoder_region_scaled_batch_into_metal_textures_with_session(
5872 &decoders, roi, scale, &output, &session,
5873 )
5874 .expect("decode cached decoder region-scaled batch into fixed reusable textures");
5875
5876 assert_eq!(tiles.len(), 2);
5877 assert_eq!(output.dimensions(), (scaled.w, scaled.h));
5878 assert_eq!(output.tile_capacity(), 2);
5879 for (index, tile) in tiles.into_iter().enumerate() {
5880 let tile = tile.expect("texture tile");
5881 assert_eq!(tile.dimensions(), (scaled.w, scaled.h));
5882 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5883 assert!(std::ptr::eq(
5884 tile.texture(),
5885 output.texture(index).expect("output texture")
5886 ));
5887 assert_eq!(
5888 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5889 expected_rgba
5890 );
5891 }
5892 }
5893
5894 #[cfg(target_os = "macos")]
5895 #[test]
5896 fn rgb8_restart_fast420_region_scaled_batch_decode_writes_reusable_metal_textures() {
5897 let session = MetalBackendSession::system_default().expect("Metal backend session");
5898 let dimensions = (128, 128);
5899 let roi = Rect {
5900 x: 9,
5901 y: 11,
5902 w: 73,
5903 h: 67,
5904 };
5905 let scale = Downscale::Half;
5906 let scaled = roi.scaled_covering(scale);
5907 let rgb = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
5908 let jpeg = encode_jpeg_baseline(
5909 JpegSamples::Rgb8 {
5910 data: &rgb,
5911 width: dimensions.0,
5912 height: dimensions.1,
5913 },
5914 JpegEncodeOptions {
5915 quality: 90,
5916 subsampling: JpegSubsampling::Ybr420,
5917 restart_interval: Some(4),
5918 backend: JpegBackend::Cpu,
5919 },
5920 )
5921 .expect("encode restart-coded fast420 region-scaled texture jpeg");
5922 let packet = build_fast420_packet(&jpeg.data).expect("restart fast420 packet");
5923 assert_ne!(packet.restart_interval_mcus, 0);
5924 assert!(!packet.restart_offsets.is_empty());
5925
5926 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (scaled.w, scaled.h), 2)
5927 .expect("texture output");
5928 let inputs = [jpeg.data.as_slice(), jpeg.data.as_slice()];
5929 let (expected_rgb, _) = CpuDecoder::new(&jpeg.data)
5930 .expect("cpu decoder")
5931 .decode_region_scaled(
5932 PixelFormat::Rgb8,
5933 j2k_jpeg::Rect {
5934 x: roi.x,
5935 y: roi.y,
5936 w: roi.w,
5937 h: roi.h,
5938 },
5939 scale,
5940 )
5941 .expect("cpu region-scaled decode");
5942 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
5943
5944 let tiles = decode_rgb8_region_scaled_batch_into_metal_textures_with_session(
5945 &inputs, roi, scale, &output, &session,
5946 )
5947 .expect("decode restart-coded region-scaled tiles into reusable textures");
5948
5949 assert_eq!(tiles.len(), 2);
5950 for (index, tile) in tiles.into_iter().enumerate() {
5951 let tile = tile.expect("texture tile");
5952 assert_eq!(tile.dimensions(), (scaled.w, scaled.h));
5953 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
5954 assert!(std::ptr::eq(
5955 tile.texture(),
5956 output.texture(index).expect("output texture")
5957 ));
5958 assert_eq!(
5959 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
5960 expected_rgba
5961 );
5962 }
5963 }
5964
5965 #[cfg(target_os = "macos")]
5966 fn assert_restart_region_scaled_texture_batch_writes_reusable_metal_output(
5967 subsampling: JpegSubsampling,
5968 dimensions: (u32, u32),
5969 ) {
5970 let session = MetalBackendSession::system_default().expect("Metal backend session");
5971 let roi = Rect {
5972 x: 0,
5973 y: 0,
5974 w: dimensions.0,
5975 h: dimensions.1,
5976 };
5977 let scale = Downscale::Half;
5978 let scaled = roi.scaled_covering(scale);
5979 let rgb = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
5980 let jpeg = encode_jpeg_baseline(
5981 JpegSamples::Rgb8 {
5982 data: &rgb,
5983 width: dimensions.0,
5984 height: dimensions.1,
5985 },
5986 JpegEncodeOptions {
5987 quality: 90,
5988 subsampling,
5989 restart_interval: Some(256),
5990 backend: JpegBackend::Cpu,
5991 },
5992 )
5993 .expect("encode restart-coded region-scaled texture jpeg");
5994 match subsampling {
5995 JpegSubsampling::Ybr422 => {
5996 let packet = build_fast422_packet(&jpeg.data).expect("restart fast422 packet");
5997 assert_ne!(packet.restart_interval_mcus, 0);
5998 assert!(!packet.restart_offsets.is_empty());
5999 }
6000 JpegSubsampling::Ybr444 => {
6001 let packet = build_fast444_packet(&jpeg.data).expect("restart fast444 packet");
6002 assert_ne!(packet.restart_interval_mcus, 0);
6003 assert!(!packet.restart_offsets.is_empty());
6004 }
6005 _ => panic!("restart region-scaled texture helper expects fast422 or fast444"),
6006 }
6007
6008 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (scaled.w, scaled.h), 2)
6009 .expect("texture output");
6010 let inputs = [jpeg.data.as_slice(), jpeg.data.as_slice()];
6011 let (expected_rgb, _) = CpuDecoder::new(&jpeg.data)
6012 .expect("cpu decoder")
6013 .decode_region_scaled(
6014 PixelFormat::Rgb8,
6015 j2k_jpeg::Rect {
6016 x: roi.x,
6017 y: roi.y,
6018 w: roi.w,
6019 h: roi.h,
6020 },
6021 scale,
6022 )
6023 .expect("cpu region-scaled decode");
6024 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6025
6026 let tiles = decode_rgb8_region_scaled_batch_into_metal_textures_with_session(
6027 &inputs, roi, scale, &output, &session,
6028 )
6029 .expect("decode restart-coded region-scaled tiles into reusable textures");
6030
6031 assert_eq!(tiles.len(), 2);
6032 for (index, tile) in tiles.into_iter().enumerate() {
6033 let tile = tile.expect("texture tile");
6034 assert_eq!(tile.dimensions(), (scaled.w, scaled.h));
6035 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6036 assert!(std::ptr::eq(
6037 tile.texture(),
6038 output.texture(index).expect("output texture")
6039 ));
6040 assert_eq!(
6041 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6042 expected_rgba
6043 );
6044 }
6045 }
6046
6047 #[cfg(target_os = "macos")]
6048 #[test]
6049 fn rgb8_restart_fast422_region_scaled_batch_decode_writes_reusable_metal_textures() {
6050 assert_restart_region_scaled_texture_batch_writes_reusable_metal_output(
6051 JpegSubsampling::Ybr422,
6052 (128, 96),
6053 );
6054 }
6055
6056 #[cfg(target_os = "macos")]
6057 #[test]
6058 fn rgb8_restart_fast444_region_scaled_batch_decode_writes_reusable_metal_textures() {
6059 assert_restart_region_scaled_texture_batch_writes_reusable_metal_output(
6060 JpegSubsampling::Ybr444,
6061 (96, 96),
6062 );
6063 }
6064
6065 #[cfg(target_os = "macos")]
6066 #[test]
6067 fn rgb8_table_mixed_fast420_region_scaled_texture_batch_groups_resident_dispatches() {
6068 let session = MetalBackendSession::system_default().expect("Metal backend session");
6069 let dimensions = (128, 128);
6070 let roi = Rect {
6071 x: 9,
6072 y: 11,
6073 w: 77,
6074 h: 65,
6075 };
6076 let scale = Downscale::Half;
6077 let scaled = roi.scaled_covering(scale);
6078 let rgb_a = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6079 let mut rgb_b = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6080 let mut rgb_c = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6081 for (index, pixel) in rgb_b.chunks_exact_mut(3).enumerate() {
6082 let delta = patterned_index_byte(index)
6083 .wrapping_mul(43)
6084 .wrapping_add(19);
6085 pixel[0] = pixel[0].wrapping_add(delta.rotate_left(1));
6086 pixel[1] = pixel[1].wrapping_sub(delta);
6087 pixel[2] ^= delta.rotate_right(2);
6088 }
6089 for (index, pixel) in rgb_c.chunks_exact_mut(3).enumerate() {
6090 let delta = patterned_index_byte(index)
6091 .wrapping_mul(47)
6092 .wrapping_add(23);
6093 pixel[0] ^= delta.rotate_left(2);
6094 pixel[1] = pixel[1].wrapping_add(delta.rotate_right(1));
6095 pixel[2] = pixel[2].wrapping_sub(delta);
6096 }
6097
6098 let jpeg_a = encode_jpeg_baseline(
6099 JpegSamples::Rgb8 {
6100 data: &rgb_a,
6101 width: dimensions.0,
6102 height: dimensions.1,
6103 },
6104 JpegEncodeOptions {
6105 quality: 90,
6106 subsampling: JpegSubsampling::Ybr420,
6107 restart_interval: None,
6108 backend: JpegBackend::Cpu,
6109 },
6110 )
6111 .expect("encode first fast420 region-scaled table group jpeg");
6112 let jpeg_b = encode_jpeg_baseline(
6113 JpegSamples::Rgb8 {
6114 data: &rgb_b,
6115 width: dimensions.0,
6116 height: dimensions.1,
6117 },
6118 JpegEncodeOptions {
6119 quality: 72,
6120 subsampling: JpegSubsampling::Ybr420,
6121 restart_interval: None,
6122 backend: JpegBackend::Cpu,
6123 },
6124 )
6125 .expect("encode second fast420 region-scaled table group jpeg");
6126 let jpeg_c = encode_jpeg_baseline(
6127 JpegSamples::Rgb8 {
6128 data: &rgb_c,
6129 width: dimensions.0,
6130 height: dimensions.1,
6131 },
6132 JpegEncodeOptions {
6133 quality: 90,
6134 subsampling: JpegSubsampling::Ybr420,
6135 restart_interval: None,
6136 backend: JpegBackend::Cpu,
6137 },
6138 )
6139 .expect("encode third fast420 region-scaled table group jpeg");
6140 let packet_a = build_fast420_packet(&jpeg_a.data).expect("first fast420 packet");
6141 let packet_b = build_fast420_packet(&jpeg_b.data).expect("second fast420 packet");
6142 let packet_c = build_fast420_packet(&jpeg_c.data).expect("third fast420 packet");
6143 assert_eq!(packet_a.y_quant, packet_c.y_quant);
6144 assert_eq!(packet_a.cb_quant, packet_c.cb_quant);
6145 assert_eq!(packet_a.cr_quant, packet_c.cr_quant);
6146 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
6147 assert_eq!(packet_a.y_ac_table, packet_c.y_ac_table);
6148 assert_eq!(
6149 packet_a.entropy_checkpoints.len(),
6150 packet_c.entropy_checkpoints.len()
6151 );
6152 assert_ne!(packet_a.y_quant, packet_b.y_quant);
6153
6154 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (scaled.w, scaled.h), 3)
6155 .expect("texture output");
6156 let inputs = [
6157 jpeg_a.data.as_slice(),
6158 jpeg_b.data.as_slice(),
6159 jpeg_c.data.as_slice(),
6160 ];
6161 let (expected_rgb_a, _) = CpuDecoder::new(&jpeg_a.data)
6162 .expect("first cpu decoder")
6163 .decode_region_scaled(
6164 PixelFormat::Rgb8,
6165 j2k_jpeg::Rect {
6166 x: roi.x,
6167 y: roi.y,
6168 w: roi.w,
6169 h: roi.h,
6170 },
6171 scale,
6172 )
6173 .expect("first cpu region scaled decode");
6174 let (expected_rgb_b, _) = CpuDecoder::new(&jpeg_b.data)
6175 .expect("second cpu decoder")
6176 .decode_region_scaled(
6177 PixelFormat::Rgb8,
6178 j2k_jpeg::Rect {
6179 x: roi.x,
6180 y: roi.y,
6181 w: roi.w,
6182 h: roi.h,
6183 },
6184 scale,
6185 )
6186 .expect("second cpu region scaled decode");
6187 let (expected_rgb_c, _) = CpuDecoder::new(&jpeg_c.data)
6188 .expect("third cpu decoder")
6189 .decode_region_scaled(
6190 PixelFormat::Rgb8,
6191 j2k_jpeg::Rect {
6192 x: roi.x,
6193 y: roi.y,
6194 w: roi.w,
6195 h: roi.h,
6196 },
6197 scale,
6198 )
6199 .expect("third cpu region scaled decode");
6200 let expected_tiles = [
6201 rgb_to_rgba_opaque(&expected_rgb_a),
6202 rgb_to_rgba_opaque(&expected_rgb_b),
6203 rgb_to_rgba_opaque(&expected_rgb_c),
6204 ];
6205 assert_ne!(expected_tiles[0], expected_tiles[1]);
6206 assert_ne!(expected_tiles[0], expected_tiles[2]);
6207 assert_ne!(expected_tiles[1], expected_tiles[2]);
6208
6209 let tiles = decode_rgb8_region_scaled_batch_into_metal_textures_with_session(
6210 &inputs, roi, scale, &output, &session,
6211 )
6212 .expect("decode table-mixed fast420 region-scaled tiles into reusable textures");
6213
6214 assert_eq!(tiles.len(), 3);
6215 for (index, tile) in tiles.into_iter().enumerate() {
6216 let tile = tile.expect("texture tile");
6217 assert_eq!(tile.dimensions(), (scaled.w, scaled.h));
6218 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6219 assert!(std::ptr::eq(
6220 tile.texture(),
6221 output.texture(index).expect("output texture")
6222 ));
6223 let actual_rgba = download_rgba8_texture(&session, tile.texture(), tile.dimensions());
6224 assert_eq!(actual_rgba.as_slice(), expected_tiles[index].as_slice());
6225 }
6226 }
6227
6228 #[cfg(target_os = "macos")]
6229 #[test]
6230 fn rgb8_fast420_batch_decode_can_write_into_reusable_metal_textures() {
6231 let session = MetalBackendSession::system_default().expect("Metal backend session");
6232 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (16, 16), 2)
6233 .expect("texture output");
6234 let inputs = [BASELINE_420, BASELINE_420];
6235 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
6236 .expect("cpu decoder")
6237 .decode(PixelFormat::Rgb8)
6238 .expect("cpu decode");
6239 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6240
6241 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6242 .expect("decode into reusable textures");
6243
6244 assert_eq!(tiles.len(), 2);
6245 assert_eq!(output.tile_capacity(), 2);
6246 assert_eq!(output.dimensions(), (16, 16));
6247 assert_eq!(output.pixel_format(), PixelFormat::Rgba8);
6248 for (index, tile) in tiles.into_iter().enumerate() {
6249 let tile = tile.expect("texture tile");
6250 assert_eq!(tile.dimensions(), (16, 16));
6251 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6252 assert!(std::ptr::eq(
6253 tile.texture(),
6254 output.texture(index).expect("output texture")
6255 ));
6256 assert_eq!(
6257 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6258 expected_rgba
6259 );
6260 }
6261 }
6262
6263 #[cfg(target_os = "macos")]
6264 #[test]
6265 fn rgb8_fast422_batch_decode_can_write_into_reusable_metal_textures() {
6266 let session = MetalBackendSession::system_default().expect("Metal backend session");
6267 let output =
6268 MetalBatchTextureOutput::new_rgba8_tiles(&session, (16, 8), 2).expect("texture output");
6269 let inputs = [BASELINE_422, BASELINE_422];
6270 let (expected_rgb, _) = CpuDecoder::new(BASELINE_422)
6271 .expect("cpu decoder")
6272 .decode(PixelFormat::Rgb8)
6273 .expect("cpu decode");
6274 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6275
6276 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6277 .expect("decode into reusable textures");
6278
6279 assert_eq!(tiles.len(), 2);
6280 assert_eq!(output.tile_capacity(), 2);
6281 assert_eq!(output.dimensions(), (16, 8));
6282 assert_eq!(output.pixel_format(), PixelFormat::Rgba8);
6283 for (index, tile) in tiles.into_iter().enumerate() {
6284 let tile = tile.expect("texture tile");
6285 assert_eq!(tile.dimensions(), (16, 8));
6286 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6287 assert!(std::ptr::eq(
6288 tile.texture(),
6289 output.texture(index).expect("output texture")
6290 ));
6291 assert_eq!(
6292 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6293 expected_rgba
6294 );
6295 }
6296 }
6297
6298 #[cfg(target_os = "macos")]
6299 #[test]
6300 fn rgb8_texture_batch_decode_avoids_private_rgba_staging_buffers() {
6301 let cases = [
6302 (BASELINE_420, (16, 16), 0),
6303 (BASELINE_422, (16, 8), 0),
6304 (BASELINE_444, (8, 8), 0),
6305 ];
6306
6307 for (input, dimensions, expected_private_allocations) in cases {
6308 let session = MetalBackendSession::system_default().expect("Metal backend session");
6309 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 2)
6310 .expect("texture output");
6311 let inputs = [input, input];
6312
6313 compute::reset_jpeg_private_buffer_allocations_for_test();
6314 let tiles =
6315 decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6316 .expect("decode into reusable textures");
6317 assert_eq!(tiles.len(), 2);
6318 for tile in tiles {
6319 assert_eq!(
6320 tile.expect("texture tile").pixel_format(),
6321 PixelFormat::Rgba8
6322 );
6323 }
6324
6325 assert_eq!(
6326 compute::jpeg_private_buffer_allocations_for_test(),
6327 expected_private_allocations,
6328 "texture batch decode should not allocate a private RGBA staging buffer for {dimensions:?}"
6329 );
6330 }
6331 }
6332
6333 #[cfg(target_os = "macos")]
6334 #[test]
6335 fn rgb8_fast444_texture_batch_decode_fuses_directly_into_reusable_metal_textures() {
6336 let session = MetalBackendSession::system_default().expect("Metal backend session");
6337 let output =
6338 MetalBatchTextureOutput::new_rgba8_tiles(&session, (8, 8), 2).expect("texture output");
6339 let inputs = [BASELINE_444, BASELINE_444];
6340 let (expected_rgb, _) = CpuDecoder::new(BASELINE_444)
6341 .expect("cpu decoder")
6342 .decode(PixelFormat::Rgb8)
6343 .expect("cpu decode");
6344 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6345
6346 compute::reset_jpeg_private_buffer_allocations_for_test();
6347 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6348 .expect("decode into reusable textures");
6349
6350 assert_eq!(tiles.len(), 2);
6351 for (index, tile) in tiles.into_iter().enumerate() {
6352 let tile = tile.expect("texture tile");
6353 assert_eq!(tile.dimensions(), (8, 8));
6354 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6355 assert!(std::ptr::eq(
6356 tile.texture(),
6357 output.texture(index).expect("output texture")
6358 ));
6359 assert_eq!(
6360 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6361 expected_rgba
6362 );
6363 }
6364 assert_eq!(
6365 compute::jpeg_private_buffer_allocations_for_test(),
6366 0,
6367 "fused 4:4:4 texture batch decode should not allocate private Y/Cb/Cr staging planes"
6368 );
6369 }
6370
6371 #[cfg(target_os = "macos")]
6372 #[test]
6373 fn rgb8_table_mixed_fast444_texture_batch_groups_resident_dispatches() {
6374 let session = MetalBackendSession::system_default().expect("Metal backend session");
6375 let dimensions = (64, 64);
6376 let rgb_a = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6377 let mut rgb_b = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6378 let mut rgb_c = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6379 for (index, pixel) in rgb_b.chunks_exact_mut(3).enumerate() {
6380 let delta = patterned_index_byte(index).wrapping_mul(31).wrapping_add(5);
6381 pixel[0] = pixel[0].wrapping_sub(delta);
6382 pixel[1] = pixel[1].wrapping_add(delta.rotate_left(1));
6383 pixel[2] ^= delta.rotate_right(2);
6384 }
6385 for (index, pixel) in rgb_c.chunks_exact_mut(3).enumerate() {
6386 let delta = patterned_index_byte(index)
6387 .wrapping_mul(37)
6388 .wrapping_add(17);
6389 pixel[0] ^= delta.rotate_left(3);
6390 pixel[1] = pixel[1].wrapping_sub(delta.rotate_right(1));
6391 pixel[2] = pixel[2].wrapping_add(delta);
6392 }
6393
6394 let jpeg_a = encode_jpeg_baseline(
6395 JpegSamples::Rgb8 {
6396 data: &rgb_a,
6397 width: dimensions.0,
6398 height: dimensions.1,
6399 },
6400 JpegEncodeOptions {
6401 quality: 92,
6402 subsampling: JpegSubsampling::Ybr444,
6403 restart_interval: None,
6404 backend: JpegBackend::Cpu,
6405 },
6406 )
6407 .expect("encode first fast444 table group jpeg");
6408 let jpeg_b = encode_jpeg_baseline(
6409 JpegSamples::Rgb8 {
6410 data: &rgb_b,
6411 width: dimensions.0,
6412 height: dimensions.1,
6413 },
6414 JpegEncodeOptions {
6415 quality: 71,
6416 subsampling: JpegSubsampling::Ybr444,
6417 restart_interval: None,
6418 backend: JpegBackend::Cpu,
6419 },
6420 )
6421 .expect("encode second fast444 table group jpeg");
6422 let jpeg_c = encode_jpeg_baseline(
6423 JpegSamples::Rgb8 {
6424 data: &rgb_c,
6425 width: dimensions.0,
6426 height: dimensions.1,
6427 },
6428 JpegEncodeOptions {
6429 quality: 92,
6430 subsampling: JpegSubsampling::Ybr444,
6431 restart_interval: None,
6432 backend: JpegBackend::Cpu,
6433 },
6434 )
6435 .expect("encode third fast444 table group jpeg");
6436 let packet_a = build_fast444_packet(&jpeg_a.data).expect("first fast444 packet");
6437 let packet_b = build_fast444_packet(&jpeg_b.data).expect("second fast444 packet");
6438 let packet_c = build_fast444_packet(&jpeg_c.data).expect("third fast444 packet");
6439 assert_eq!(packet_a.y_quant, packet_c.y_quant);
6440 assert_eq!(packet_a.cb_quant, packet_c.cb_quant);
6441 assert_eq!(packet_a.cr_quant, packet_c.cr_quant);
6442 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
6443 assert_eq!(packet_a.y_ac_table, packet_c.y_ac_table);
6444 assert_eq!(
6445 packet_a.entropy_checkpoints.len(),
6446 packet_c.entropy_checkpoints.len()
6447 );
6448 assert_ne!(packet_a.y_quant, packet_b.y_quant);
6449
6450 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 3)
6451 .expect("texture output");
6452 let inputs = [
6453 jpeg_a.data.as_slice(),
6454 jpeg_b.data.as_slice(),
6455 jpeg_c.data.as_slice(),
6456 ];
6457 let (expected_rgb_a, _) = CpuDecoder::new(&jpeg_a.data)
6458 .expect("first cpu decoder")
6459 .decode(PixelFormat::Rgb8)
6460 .expect("first cpu decode");
6461 let (expected_rgb_b, _) = CpuDecoder::new(&jpeg_b.data)
6462 .expect("second cpu decoder")
6463 .decode(PixelFormat::Rgb8)
6464 .expect("second cpu decode");
6465 let (expected_rgb_c, _) = CpuDecoder::new(&jpeg_c.data)
6466 .expect("third cpu decoder")
6467 .decode(PixelFormat::Rgb8)
6468 .expect("third cpu decode");
6469 let expected_tiles = [
6470 rgb_to_rgba_opaque(&expected_rgb_a),
6471 rgb_to_rgba_opaque(&expected_rgb_b),
6472 rgb_to_rgba_opaque(&expected_rgb_c),
6473 ];
6474 assert_ne!(expected_tiles[0], expected_tiles[1]);
6475 assert_ne!(expected_tiles[0], expected_tiles[2]);
6476 assert_ne!(expected_tiles[1], expected_tiles[2]);
6477
6478 compute::reset_jpeg_private_buffer_allocations_for_test();
6479 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6480 .expect("decode table-mixed fast444 tiles into reusable textures");
6481
6482 assert_eq!(tiles.len(), 3);
6483 for (index, tile) in tiles.into_iter().enumerate() {
6484 let tile = tile.expect("texture tile");
6485 assert_eq!(tile.dimensions(), dimensions);
6486 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6487 assert!(std::ptr::eq(
6488 tile.texture(),
6489 output.texture(index).expect("output texture")
6490 ));
6491 let actual_rgba = download_rgba8_texture(&session, tile.texture(), tile.dimensions());
6492 assert_eq!(actual_rgba.as_slice(), expected_tiles[index].as_slice());
6493 }
6494 assert_eq!(
6495 compute::jpeg_private_buffer_allocations_for_test(),
6496 0,
6497 "table-mixed resident 4:4:4 texture dispatches should not allocate private Y/Cb/Cr staging planes"
6498 );
6499 }
6500
6501 #[cfg(target_os = "macos")]
6502 #[test]
6503 fn rgb8_fast422_texture_batch_decode_fuses_directly_into_reusable_metal_textures() {
6504 let session = MetalBackendSession::system_default().expect("Metal backend session");
6505 let output =
6506 MetalBatchTextureOutput::new_rgba8_tiles(&session, (16, 8), 2).expect("texture output");
6507 let inputs = [BASELINE_422, BASELINE_422];
6508 let (expected_rgb, _) = CpuDecoder::new(BASELINE_422)
6509 .expect("cpu decoder")
6510 .decode(PixelFormat::Rgb8)
6511 .expect("cpu decode");
6512 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6513
6514 compute::reset_jpeg_private_buffer_allocations_for_test();
6515 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6516 .expect("decode into reusable textures");
6517
6518 assert_eq!(tiles.len(), 2);
6519 for (index, tile) in tiles.into_iter().enumerate() {
6520 let tile = tile.expect("texture tile");
6521 assert_eq!(tile.dimensions(), (16, 8));
6522 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6523 assert!(std::ptr::eq(
6524 tile.texture(),
6525 output.texture(index).expect("output texture")
6526 ));
6527 assert_eq!(
6528 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6529 expected_rgba
6530 );
6531 }
6532 assert_eq!(
6533 compute::jpeg_private_buffer_allocations_for_test(),
6534 0,
6535 "fused 4:2:2 texture batch decode should not allocate private Y/Cb/Cr staging planes"
6536 );
6537 }
6538
6539 #[cfg(target_os = "macos")]
6540 #[test]
6541 fn rgb8_wide_fast422_texture_batch_decode_fuses_directly_into_reusable_metal_textures() {
6542 let session = MetalBackendSession::system_default().expect("Metal backend session");
6543 let dimensions = (48, 16);
6544 let rgb = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6545 let jpeg = encode_jpeg_baseline(
6546 JpegSamples::Rgb8 {
6547 data: &rgb,
6548 width: dimensions.0,
6549 height: dimensions.1,
6550 },
6551 JpegEncodeOptions {
6552 quality: 92,
6553 subsampling: JpegSubsampling::Ybr422,
6554 restart_interval: None,
6555 backend: JpegBackend::Cpu,
6556 },
6557 )
6558 .expect("encode 4:2:2 source jpeg");
6559 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 2)
6560 .expect("texture output");
6561 let inputs = [jpeg.data.as_slice(), jpeg.data.as_slice()];
6562 let (expected_rgb, _) = CpuDecoder::new(&jpeg.data)
6563 .expect("cpu decoder")
6564 .decode(PixelFormat::Rgb8)
6565 .expect("cpu decode");
6566 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6567
6568 compute::reset_jpeg_private_buffer_allocations_for_test();
6569 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6570 .expect("decode into reusable textures");
6571
6572 assert_eq!(tiles.len(), 2);
6573 for (index, tile) in tiles.into_iter().enumerate() {
6574 let tile = tile.expect("texture tile");
6575 assert_eq!(tile.dimensions(), dimensions);
6576 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6577 assert!(std::ptr::eq(
6578 tile.texture(),
6579 output.texture(index).expect("output texture")
6580 ));
6581 assert_eq!(
6582 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6583 expected_rgba
6584 );
6585 }
6586 assert_eq!(
6587 compute::jpeg_private_buffer_allocations_for_test(),
6588 0,
6589 "wide fused 4:2:2 texture batch decode should not allocate private Y/Cb/Cr staging planes"
6590 );
6591 }
6592
6593 #[cfg(target_os = "macos")]
6594 #[test]
6595 fn rgb8_table_mixed_fast422_texture_batch_groups_resident_dispatches() {
6596 let session = MetalBackendSession::system_default().expect("Metal backend session");
6597 let dimensions = (96, 48);
6598 let rgb_a = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6599 let mut rgb_b = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6600 let mut rgb_c = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6601 for (index, pixel) in rgb_b.chunks_exact_mut(3).enumerate() {
6602 let delta = patterned_index_byte(index)
6603 .wrapping_mul(23)
6604 .wrapping_add(11);
6605 pixel[0] = pixel[0].wrapping_add(delta.rotate_left(1));
6606 pixel[1] ^= delta;
6607 pixel[2] = pixel[2].wrapping_sub(delta.rotate_right(2));
6608 }
6609 for (index, pixel) in rgb_c.chunks_exact_mut(3).enumerate() {
6610 let delta = patterned_index_byte(index)
6611 .wrapping_mul(19)
6612 .wrapping_add(53);
6613 pixel[0] ^= delta.rotate_left(2);
6614 pixel[1] = pixel[1].wrapping_sub(delta);
6615 pixel[2] = pixel[2].wrapping_add(delta.rotate_right(1));
6616 }
6617
6618 let jpeg_a = encode_jpeg_baseline(
6619 JpegSamples::Rgb8 {
6620 data: &rgb_a,
6621 width: dimensions.0,
6622 height: dimensions.1,
6623 },
6624 JpegEncodeOptions {
6625 quality: 91,
6626 subsampling: JpegSubsampling::Ybr422,
6627 restart_interval: None,
6628 backend: JpegBackend::Cpu,
6629 },
6630 )
6631 .expect("encode first fast422 table group jpeg");
6632 let jpeg_b = encode_jpeg_baseline(
6633 JpegSamples::Rgb8 {
6634 data: &rgb_b,
6635 width: dimensions.0,
6636 height: dimensions.1,
6637 },
6638 JpegEncodeOptions {
6639 quality: 73,
6640 subsampling: JpegSubsampling::Ybr422,
6641 restart_interval: None,
6642 backend: JpegBackend::Cpu,
6643 },
6644 )
6645 .expect("encode second fast422 table group jpeg");
6646 let jpeg_c = encode_jpeg_baseline(
6647 JpegSamples::Rgb8 {
6648 data: &rgb_c,
6649 width: dimensions.0,
6650 height: dimensions.1,
6651 },
6652 JpegEncodeOptions {
6653 quality: 91,
6654 subsampling: JpegSubsampling::Ybr422,
6655 restart_interval: None,
6656 backend: JpegBackend::Cpu,
6657 },
6658 )
6659 .expect("encode third fast422 table group jpeg");
6660 let packet_a = build_fast422_packet(&jpeg_a.data).expect("first fast422 packet");
6661 let packet_b = build_fast422_packet(&jpeg_b.data).expect("second fast422 packet");
6662 let packet_c = build_fast422_packet(&jpeg_c.data).expect("third fast422 packet");
6663 assert_eq!(packet_a.y_quant, packet_c.y_quant);
6664 assert_eq!(packet_a.cb_quant, packet_c.cb_quant);
6665 assert_eq!(packet_a.cr_quant, packet_c.cr_quant);
6666 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
6667 assert_eq!(packet_a.y_ac_table, packet_c.y_ac_table);
6668 assert_eq!(
6669 packet_a.entropy_checkpoints.len(),
6670 packet_c.entropy_checkpoints.len()
6671 );
6672 assert_ne!(packet_a.y_quant, packet_b.y_quant);
6673
6674 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 3)
6675 .expect("texture output");
6676 let inputs = [
6677 jpeg_a.data.as_slice(),
6678 jpeg_b.data.as_slice(),
6679 jpeg_c.data.as_slice(),
6680 ];
6681 let (expected_rgb_a, _) = CpuDecoder::new(&jpeg_a.data)
6682 .expect("first cpu decoder")
6683 .decode(PixelFormat::Rgb8)
6684 .expect("first cpu decode");
6685 let (expected_rgb_b, _) = CpuDecoder::new(&jpeg_b.data)
6686 .expect("second cpu decoder")
6687 .decode(PixelFormat::Rgb8)
6688 .expect("second cpu decode");
6689 let (expected_rgb_c, _) = CpuDecoder::new(&jpeg_c.data)
6690 .expect("third cpu decoder")
6691 .decode(PixelFormat::Rgb8)
6692 .expect("third cpu decode");
6693 let expected_tiles = [
6694 rgb_to_rgba_opaque(&expected_rgb_a),
6695 rgb_to_rgba_opaque(&expected_rgb_b),
6696 rgb_to_rgba_opaque(&expected_rgb_c),
6697 ];
6698 assert_ne!(expected_tiles[0], expected_tiles[1]);
6699 assert_ne!(expected_tiles[0], expected_tiles[2]);
6700 assert_ne!(expected_tiles[1], expected_tiles[2]);
6701
6702 compute::reset_jpeg_private_buffer_allocations_for_test();
6703 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6704 .expect("decode table-mixed fast422 tiles into reusable textures");
6705
6706 assert_eq!(tiles.len(), 3);
6707 for (index, tile) in tiles.into_iter().enumerate() {
6708 let tile = tile.expect("texture tile");
6709 assert_eq!(tile.dimensions(), dimensions);
6710 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6711 assert!(std::ptr::eq(
6712 tile.texture(),
6713 output.texture(index).expect("output texture")
6714 ));
6715 let actual_rgba = download_rgba8_texture(&session, tile.texture(), tile.dimensions());
6716 assert_eq!(actual_rgba.as_slice(), expected_tiles[index].as_slice());
6717 }
6718 assert_eq!(
6719 compute::jpeg_private_buffer_allocations_for_test(),
6720 0,
6721 "table-mixed resident 4:2:2 texture dispatches should not allocate private Y/Cb/Cr staging planes"
6722 );
6723 }
6724
6725 #[cfg(target_os = "macos")]
6726 #[test]
6727 fn rgb8_fast420_texture_batch_decode_fuses_directly_into_reusable_metal_textures() {
6728 let session = MetalBackendSession::system_default().expect("Metal backend session");
6729 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, (16, 16), 2)
6730 .expect("texture output");
6731 let inputs = [BASELINE_420, BASELINE_420];
6732 let (expected_rgb, _) = CpuDecoder::new(BASELINE_420)
6733 .expect("cpu decoder")
6734 .decode(PixelFormat::Rgb8)
6735 .expect("cpu decode");
6736 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6737
6738 compute::reset_jpeg_private_buffer_allocations_for_test();
6739 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6740 .expect("decode into reusable textures");
6741
6742 assert_eq!(tiles.len(), 2);
6743 for (index, tile) in tiles.into_iter().enumerate() {
6744 let tile = tile.expect("texture tile");
6745 assert_eq!(tile.dimensions(), (16, 16));
6746 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6747 assert!(std::ptr::eq(
6748 tile.texture(),
6749 output.texture(index).expect("output texture")
6750 ));
6751 assert_eq!(
6752 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6753 expected_rgba
6754 );
6755 }
6756 assert_eq!(
6757 compute::jpeg_private_buffer_allocations_for_test(),
6758 0,
6759 "fused 4:2:0 texture batch decode should not allocate private Y/Cb/Cr staging planes"
6760 );
6761 }
6762
6763 #[cfg(target_os = "macos")]
6764 #[test]
6765 fn rgb8_wide_row_fast420_texture_batch_decode_fuses_directly_into_reusable_metal_textures() {
6766 let session = MetalBackendSession::system_default().expect("Metal backend session");
6767 let dimensions = (32, 16);
6768 let rgb = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6769 let jpeg = encode_jpeg_baseline(
6770 JpegSamples::Rgb8 {
6771 data: &rgb,
6772 width: dimensions.0,
6773 height: dimensions.1,
6774 },
6775 JpegEncodeOptions {
6776 quality: 92,
6777 subsampling: JpegSubsampling::Ybr420,
6778 restart_interval: None,
6779 backend: JpegBackend::Cpu,
6780 },
6781 )
6782 .expect("encode 4:2:0 source jpeg");
6783 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 2)
6784 .expect("texture output");
6785 let inputs = [jpeg.data.as_slice(), jpeg.data.as_slice()];
6786 let (expected_rgb, _) = CpuDecoder::new(&jpeg.data)
6787 .expect("cpu decoder")
6788 .decode(PixelFormat::Rgb8)
6789 .expect("cpu decode");
6790 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6791
6792 compute::reset_jpeg_private_buffer_allocations_for_test();
6793 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6794 .expect("decode into reusable textures");
6795
6796 assert_eq!(tiles.len(), 2);
6797 for (index, tile) in tiles.into_iter().enumerate() {
6798 let tile = tile.expect("texture tile");
6799 assert_eq!(tile.dimensions(), dimensions);
6800 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6801 assert!(std::ptr::eq(
6802 tile.texture(),
6803 output.texture(index).expect("output texture")
6804 ));
6805 assert_eq!(
6806 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6807 expected_rgba
6808 );
6809 }
6810 assert_eq!(
6811 compute::jpeg_private_buffer_allocations_for_test(),
6812 0,
6813 "wide-row fused 4:2:0 texture batch decode should not allocate private Y/Cb/Cr staging planes"
6814 );
6815 }
6816
6817 #[cfg(target_os = "macos")]
6818 #[test]
6819 fn rgb8_multi_row_fast420_texture_batch_decode_fuses_directly_into_reusable_metal_textures() {
6820 let session = MetalBackendSession::system_default().expect("Metal backend session");
6821 let dimensions = (16, 32);
6822 let rgb = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6823 let jpeg = encode_jpeg_baseline(
6824 JpegSamples::Rgb8 {
6825 data: &rgb,
6826 width: dimensions.0,
6827 height: dimensions.1,
6828 },
6829 JpegEncodeOptions {
6830 quality: 92,
6831 subsampling: JpegSubsampling::Ybr420,
6832 restart_interval: None,
6833 backend: JpegBackend::Cpu,
6834 },
6835 )
6836 .expect("encode 4:2:0 source jpeg");
6837 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 2)
6838 .expect("texture output");
6839 let inputs = [jpeg.data.as_slice(), jpeg.data.as_slice()];
6840 let (expected_rgb, _) = CpuDecoder::new(&jpeg.data)
6841 .expect("cpu decoder")
6842 .decode(PixelFormat::Rgb8)
6843 .expect("cpu decode");
6844 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6845
6846 compute::reset_jpeg_private_buffer_allocations_for_test();
6847 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6848 .expect("decode into reusable textures");
6849
6850 assert_eq!(tiles.len(), 2);
6851 for (index, tile) in tiles.into_iter().enumerate() {
6852 let tile = tile.expect("texture tile");
6853 assert_eq!(tile.dimensions(), dimensions);
6854 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6855 assert!(std::ptr::eq(
6856 tile.texture(),
6857 output.texture(index).expect("output texture")
6858 ));
6859 assert_eq!(
6860 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6861 expected_rgba
6862 );
6863 }
6864 assert_eq!(
6865 compute::jpeg_private_buffer_allocations_for_test(),
6866 0,
6867 "multi-row fused 4:2:0 texture batch decode should not allocate private Y/Cb/Cr staging planes"
6868 );
6869 }
6870
6871 #[cfg(target_os = "macos")]
6872 #[test]
6873 fn rgb8_multi_axis_fast420_texture_batch_decode_fuses_directly_into_reusable_metal_textures() {
6874 let session = MetalBackendSession::system_default().expect("Metal backend session");
6875 for dimensions in [(32, 32), (48, 48)] {
6876 let rgb = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6877 let jpeg = encode_jpeg_baseline(
6878 JpegSamples::Rgb8 {
6879 data: &rgb,
6880 width: dimensions.0,
6881 height: dimensions.1,
6882 },
6883 JpegEncodeOptions {
6884 quality: 92,
6885 subsampling: JpegSubsampling::Ybr420,
6886 restart_interval: None,
6887 backend: JpegBackend::Cpu,
6888 },
6889 )
6890 .expect("encode 4:2:0 source jpeg");
6891 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 2)
6892 .expect("texture output");
6893 let inputs = [jpeg.data.as_slice(), jpeg.data.as_slice()];
6894 let (expected_rgb, _) = CpuDecoder::new(&jpeg.data)
6895 .expect("cpu decoder")
6896 .decode(PixelFormat::Rgb8)
6897 .expect("cpu decode");
6898 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6899
6900 compute::reset_jpeg_private_buffer_allocations_for_test();
6901 let tiles =
6902 decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6903 .expect("decode into reusable textures");
6904
6905 assert_eq!(tiles.len(), 2);
6906 for (index, tile) in tiles.into_iter().enumerate() {
6907 let tile = tile.expect("texture tile");
6908 assert_eq!(tile.dimensions(), dimensions);
6909 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6910 assert!(std::ptr::eq(
6911 tile.texture(),
6912 output.texture(index).expect("output texture")
6913 ));
6914 assert_eq!(
6915 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6916 expected_rgba
6917 );
6918 }
6919 assert_eq!(
6920 compute::jpeg_private_buffer_allocations_for_test(),
6921 0,
6922 "multi-axis fused 4:2:0 texture batch decode should not allocate private Y/Cb/Cr staging planes for {dimensions:?}"
6923 );
6924 }
6925 }
6926
6927 #[cfg(target_os = "macos")]
6928 #[test]
6929 fn rgb8_chunked_multi_axis_fast420_texture_batch_decode_fuses_directly_into_reusable_metal_textures(
6930 ) {
6931 let session = MetalBackendSession::system_default().expect("Metal backend session");
6932 let dimensions = (736, 720);
6933 let rgb = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6934 let jpeg = encode_jpeg_baseline(
6935 JpegSamples::Rgb8 {
6936 data: &rgb,
6937 width: dimensions.0,
6938 height: dimensions.1,
6939 },
6940 JpegEncodeOptions {
6941 quality: 90,
6942 subsampling: JpegSubsampling::Ybr420,
6943 restart_interval: None,
6944 backend: JpegBackend::Cpu,
6945 },
6946 )
6947 .expect("encode chunked 4:2:0 source jpeg");
6948 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 2)
6949 .expect("texture output");
6950 let inputs = [jpeg.data.as_slice(), jpeg.data.as_slice()];
6951 let (expected_rgb, _) = CpuDecoder::new(&jpeg.data)
6952 .expect("cpu decoder")
6953 .decode(PixelFormat::Rgb8)
6954 .expect("cpu decode");
6955 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
6956
6957 compute::reset_jpeg_private_buffer_allocations_for_test();
6958 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
6959 .expect("decode into reusable textures");
6960
6961 assert_eq!(tiles.len(), 2);
6962 for (index, tile) in tiles.into_iter().enumerate() {
6963 let tile = tile.expect("texture tile");
6964 assert_eq!(tile.dimensions(), dimensions);
6965 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
6966 assert!(std::ptr::eq(
6967 tile.texture(),
6968 output.texture(index).expect("output texture")
6969 ));
6970 assert_eq!(
6971 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
6972 expected_rgba
6973 );
6974 }
6975 assert_eq!(
6976 compute::jpeg_private_buffer_allocations_for_test(),
6977 0,
6978 "chunked multi-axis fused 4:2:0 texture batch decode should not allocate private Y/Cb/Cr staging planes"
6979 );
6980 }
6981
6982 #[cfg(target_os = "macos")]
6983 #[test]
6984 fn rgb8_restart_fast420_texture_batch_decode_fuses_directly_into_reusable_metal_textures() {
6985 let session = MetalBackendSession::system_default().expect("Metal backend session");
6986 let dimensions = (48, 48);
6987 let rgb = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
6988 let jpeg = encode_jpeg_baseline(
6989 JpegSamples::Rgb8 {
6990 data: &rgb,
6991 width: dimensions.0,
6992 height: dimensions.1,
6993 },
6994 JpegEncodeOptions {
6995 quality: 90,
6996 subsampling: JpegSubsampling::Ybr420,
6997 restart_interval: Some(2),
6998 backend: JpegBackend::Cpu,
6999 },
7000 )
7001 .expect("encode restart 4:2:0 source jpeg");
7002 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 2)
7003 .expect("texture output");
7004 let inputs = [jpeg.data.as_slice(), jpeg.data.as_slice()];
7005 let (expected_rgb, _) = CpuDecoder::new(&jpeg.data)
7006 .expect("cpu decoder")
7007 .decode(PixelFormat::Rgb8)
7008 .expect("cpu decode");
7009 let expected_rgba = rgb_to_rgba_opaque(&expected_rgb);
7010
7011 compute::reset_jpeg_private_buffer_allocations_for_test();
7012 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
7013 .expect("decode into reusable textures");
7014
7015 assert_eq!(tiles.len(), 2);
7016 for (index, tile) in tiles.into_iter().enumerate() {
7017 let tile = tile.expect("texture tile");
7018 assert_eq!(tile.dimensions(), dimensions);
7019 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
7020 assert!(std::ptr::eq(
7021 tile.texture(),
7022 output.texture(index).expect("output texture")
7023 ));
7024 assert_eq!(
7025 download_rgba8_texture(&session, tile.texture(), tile.dimensions()),
7026 expected_rgba
7027 );
7028 }
7029 assert_eq!(
7030 compute::jpeg_private_buffer_allocations_for_test(),
7031 0,
7032 "restart fused 4:2:0 texture batch decode should not allocate private Y/Cb/Cr staging planes"
7033 );
7034 }
7035
7036 #[cfg(target_os = "macos")]
7037 #[test]
7038 fn rgb8_distinct_restart_fast420_texture_batch_decode_fuses_directly_into_reusable_metal_textures(
7039 ) {
7040 let session = MetalBackendSession::system_default().expect("Metal backend session");
7041 let dimensions = (128, 128);
7042 let rgb_a = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
7043 let mut rgb_b = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
7044 for (index, pixel) in rgb_b.chunks_exact_mut(3).enumerate() {
7045 let delta = patterned_index_byte(index)
7046 .wrapping_mul(17)
7047 .wrapping_add(31);
7048 pixel[0] = pixel[0].wrapping_add(delta);
7049 pixel[1] = pixel[1].wrapping_sub(delta.rotate_left(1));
7050 pixel[2] ^= delta.rotate_right(1);
7051 }
7052 assert_ne!(rgb_a, rgb_b);
7053
7054 let jpeg_a = encode_jpeg_baseline(
7055 JpegSamples::Rgb8 {
7056 data: &rgb_a,
7057 width: dimensions.0,
7058 height: dimensions.1,
7059 },
7060 JpegEncodeOptions {
7061 quality: 90,
7062 subsampling: JpegSubsampling::Ybr420,
7063 restart_interval: Some(4),
7064 backend: JpegBackend::Cpu,
7065 },
7066 )
7067 .expect("encode first restart 4:2:0 source jpeg");
7068 let jpeg_b = encode_jpeg_baseline(
7069 JpegSamples::Rgb8 {
7070 data: &rgb_b,
7071 width: dimensions.0,
7072 height: dimensions.1,
7073 },
7074 JpegEncodeOptions {
7075 quality: 90,
7076 subsampling: JpegSubsampling::Ybr420,
7077 restart_interval: Some(4),
7078 backend: JpegBackend::Cpu,
7079 },
7080 )
7081 .expect("encode second restart 4:2:0 source jpeg");
7082 assert_ne!(jpeg_a.data, jpeg_b.data);
7083
7084 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 2)
7085 .expect("texture output");
7086 let inputs = [jpeg_a.data.as_slice(), jpeg_b.data.as_slice()];
7087 let (expected_rgb_a, _) = CpuDecoder::new(&jpeg_a.data)
7088 .expect("first cpu decoder")
7089 .decode(PixelFormat::Rgb8)
7090 .expect("first cpu decode");
7091 let (expected_rgb_b, _) = CpuDecoder::new(&jpeg_b.data)
7092 .expect("second cpu decoder")
7093 .decode(PixelFormat::Rgb8)
7094 .expect("second cpu decode");
7095 let expected_tiles = [
7096 rgb_to_rgba_opaque(&expected_rgb_a),
7097 rgb_to_rgba_opaque(&expected_rgb_b),
7098 ];
7099 assert_ne!(expected_tiles[0], expected_tiles[1]);
7100
7101 compute::reset_jpeg_private_buffer_allocations_for_test();
7102 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
7103 .expect("decode distinct restart tiles into reusable textures");
7104
7105 assert_eq!(tiles.len(), 2);
7106 for (index, tile) in tiles.into_iter().enumerate() {
7107 let tile = tile.expect("texture tile");
7108 assert_eq!(tile.dimensions(), dimensions);
7109 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
7110 assert!(std::ptr::eq(
7111 tile.texture(),
7112 output.texture(index).expect("output texture")
7113 ));
7114 let actual_rgba = download_rgba8_texture(&session, tile.texture(), tile.dimensions());
7115 assert_eq!(actual_rgba.as_slice(), expected_tiles[index].as_slice());
7116 }
7117 assert_eq!(
7118 compute::jpeg_private_buffer_allocations_for_test(),
7119 0,
7120 "distinct restart fused 4:2:0 texture batch decode should not allocate private Y/Cb/Cr staging planes"
7121 );
7122 }
7123
7124 #[cfg(target_os = "macos")]
7125 #[test]
7126 fn rgb8_table_mixed_restart_fast420_texture_batch_groups_resident_dispatches() {
7127 let session = MetalBackendSession::system_default().expect("Metal backend session");
7128 let dimensions = (128, 128);
7129 let rgb_a = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
7130 let mut rgb_b = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
7131 let mut rgb_c = j2k_test_support::patterned_rgb8(dimensions.0, dimensions.1);
7132 for (index, pixel) in rgb_b.chunks_exact_mut(3).enumerate() {
7133 let delta = patterned_index_byte(index).wrapping_mul(29).wrapping_add(7);
7134 pixel[0] ^= delta;
7135 pixel[1] = pixel[1].wrapping_add(delta.rotate_left(2));
7136 pixel[2] = pixel[2].wrapping_sub(delta.rotate_right(2));
7137 }
7138 for (index, pixel) in rgb_c.chunks_exact_mut(3).enumerate() {
7139 let delta = patterned_index_byte(index)
7140 .wrapping_mul(13)
7141 .wrapping_add(41);
7142 pixel[0] = pixel[0].wrapping_sub(delta.rotate_left(1));
7143 pixel[1] ^= delta.rotate_right(3);
7144 pixel[2] = pixel[2].wrapping_add(delta);
7145 }
7146
7147 let jpeg_a = encode_jpeg_baseline(
7148 JpegSamples::Rgb8 {
7149 data: &rgb_a,
7150 width: dimensions.0,
7151 height: dimensions.1,
7152 },
7153 JpegEncodeOptions {
7154 quality: 90,
7155 subsampling: JpegSubsampling::Ybr420,
7156 restart_interval: Some(4),
7157 backend: JpegBackend::Cpu,
7158 },
7159 )
7160 .expect("encode first table group jpeg");
7161 let jpeg_b = encode_jpeg_baseline(
7162 JpegSamples::Rgb8 {
7163 data: &rgb_b,
7164 width: dimensions.0,
7165 height: dimensions.1,
7166 },
7167 JpegEncodeOptions {
7168 quality: 74,
7169 subsampling: JpegSubsampling::Ybr420,
7170 restart_interval: Some(4),
7171 backend: JpegBackend::Cpu,
7172 },
7173 )
7174 .expect("encode second table group jpeg");
7175 let jpeg_c = encode_jpeg_baseline(
7176 JpegSamples::Rgb8 {
7177 data: &rgb_c,
7178 width: dimensions.0,
7179 height: dimensions.1,
7180 },
7181 JpegEncodeOptions {
7182 quality: 90,
7183 subsampling: JpegSubsampling::Ybr420,
7184 restart_interval: Some(4),
7185 backend: JpegBackend::Cpu,
7186 },
7187 )
7188 .expect("encode third table group jpeg");
7189 let packet_a = build_fast420_packet(&jpeg_a.data).expect("first fast420 packet");
7190 let packet_b = build_fast420_packet(&jpeg_b.data).expect("second fast420 packet");
7191 let packet_c = build_fast420_packet(&jpeg_c.data).expect("third fast420 packet");
7192 assert_eq!(packet_a.y_quant, packet_c.y_quant);
7193 assert_eq!(packet_a.cb_quant, packet_c.cb_quant);
7194 assert_eq!(packet_a.cr_quant, packet_c.cr_quant);
7195 assert_eq!(packet_a.y_dc_table, packet_c.y_dc_table);
7196 assert_eq!(packet_a.y_ac_table, packet_c.y_ac_table);
7197 assert_eq!(
7198 packet_a.entropy_checkpoints.len(),
7199 packet_c.entropy_checkpoints.len()
7200 );
7201 assert_ne!(packet_a.y_quant, packet_b.y_quant);
7202
7203 let output = MetalBatchTextureOutput::new_rgba8_tiles(&session, dimensions, 3)
7204 .expect("texture output");
7205 let inputs = [
7206 jpeg_a.data.as_slice(),
7207 jpeg_b.data.as_slice(),
7208 jpeg_c.data.as_slice(),
7209 ];
7210 let (expected_rgb_a, _) = CpuDecoder::new(&jpeg_a.data)
7211 .expect("first cpu decoder")
7212 .decode(PixelFormat::Rgb8)
7213 .expect("first cpu decode");
7214 let (expected_rgb_b, _) = CpuDecoder::new(&jpeg_b.data)
7215 .expect("second cpu decoder")
7216 .decode(PixelFormat::Rgb8)
7217 .expect("second cpu decode");
7218 let (expected_rgb_c, _) = CpuDecoder::new(&jpeg_c.data)
7219 .expect("third cpu decoder")
7220 .decode(PixelFormat::Rgb8)
7221 .expect("third cpu decode");
7222 let expected_tiles = [
7223 rgb_to_rgba_opaque(&expected_rgb_a),
7224 rgb_to_rgba_opaque(&expected_rgb_b),
7225 rgb_to_rgba_opaque(&expected_rgb_c),
7226 ];
7227 assert_ne!(expected_tiles[0], expected_tiles[1]);
7228 assert_ne!(expected_tiles[0], expected_tiles[2]);
7229 assert_ne!(expected_tiles[1], expected_tiles[2]);
7230
7231 compute::reset_jpeg_private_buffer_allocations_for_test();
7232 let tiles = decode_rgb8_batch_into_metal_textures_with_session(&inputs, &output, &session)
7233 .expect("decode table-mixed restart tiles into reusable textures");
7234
7235 assert_eq!(tiles.len(), 3);
7236 for (index, tile) in tiles.into_iter().enumerate() {
7237 let tile = tile.expect("texture tile");
7238 assert_eq!(tile.dimensions(), dimensions);
7239 assert_eq!(tile.pixel_format(), PixelFormat::Rgba8);
7240 assert!(std::ptr::eq(
7241 tile.texture(),
7242 output.texture(index).expect("output texture")
7243 ));
7244 let actual_rgba = download_rgba8_texture(&session, tile.texture(), tile.dimensions());
7245 assert_eq!(actual_rgba.as_slice(), expected_tiles[index].as_slice());
7246 }
7247 assert_eq!(
7248 compute::jpeg_private_buffer_allocations_for_test(),
7249 0,
7250 "table-mixed resident 4:2:0 texture dispatches should not allocate private Y/Cb/Cr staging planes"
7251 );
7252 }
7253
7254 #[cfg(target_os = "macos")]
7255 #[test]
7256 fn jpeg_device_decode_uses_private_internal_planes() {
7257 let session = MetalBackendSession::system_default().expect("Metal backend session");
7258 let mut decoder = Decoder::new(BASELINE_420).expect("decoder");
7259
7260 compute::reset_jpeg_private_buffer_allocations_for_test();
7261 let surface = decoder
7262 .decode_to_device_with_session(PixelFormat::Rgb8, &session)
7263 .expect("resident JPEG Metal decode");
7264 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
7265 assert!(
7266 compute::jpeg_private_buffer_allocations_for_test() > 0,
7267 "resident JPEG Metal decode should use Private internal planes"
7268 );
7269 let _ = surface.as_bytes();
7270 }
7271
7272 #[cfg(target_os = "macos")]
7273 #[test]
7274 fn jpeg_private_rgb8_tile_uses_private_output_buffer() {
7275 let session = MetalBackendSession::system_default().expect("Metal backend session");
7276 let mut decoder = Decoder::new(BASELINE_420).expect("decoder");
7277
7278 let tile = decoder
7279 .decode_private_rgb8_tile_with_session(&session)
7280 .expect("resident private JPEG Metal decode");
7281
7282 assert_eq!(tile.dimensions, (16, 16));
7283 assert_eq!(tile.pixel_format, PixelFormat::Rgb8);
7284 assert_eq!(tile.pitch_bytes, 16 * PixelFormat::Rgb8.bytes_per_pixel());
7285 assert_eq!(tile.byte_offset, 0);
7286 assert_eq!(tile.buffer.storage_mode(), metal::MTLStorageMode::Private);
7287 assert!(tile.status_buffer.length() > 0);
7288 }
7289
7290 #[cfg(target_os = "macos")]
7291 #[test]
7292 fn jpeg_gray_region_decode_uses_private_internal_planes() {
7293 let roi = Rect {
7294 x: 4,
7295 y: 4,
7296 w: 8,
7297 h: 8,
7298 };
7299 let mut expected_decoder = Decoder::new(BASELINE_420).expect("expected decoder");
7300 let mut expected = vec![0; roi.w as usize * roi.h as usize];
7301 expected_decoder
7302 .decode_region_into(
7303 &mut CpuScratchPool::new(),
7304 &mut expected,
7305 roi.w as usize,
7306 PixelFormat::Gray8,
7307 roi,
7308 )
7309 .expect("expected CPU region decode");
7310
7311 let mut decoder = Decoder::new(BASELINE_420).expect("decoder");
7312 compute::reset_jpeg_private_buffer_allocations_for_test();
7313 let surface = decoder
7314 .decode_region_to_device(PixelFormat::Gray8, roi, BackendRequest::Metal)
7315 .expect("resident JPEG Metal region decode");
7316 assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
7317 assert!(
7318 compute::jpeg_private_buffer_allocations_for_test() >= 3,
7319 "resident Gray8 region decode should keep decoded Y/Cb/Cr planes Private"
7320 );
7321 assert_eq!(surface.as_bytes(), expected.as_slice());
7322 }
7323
7324 #[cfg(target_os = "macos")]
7325 #[test]
7326 fn uploaded_metal_surface_is_marked_cpu_staged() {
7327 let surface = upload_surface(
7328 vec![1, 2, 3],
7329 (1, 1),
7330 PixelFormat::Rgb8,
7331 BackendRequest::Metal,
7332 )
7333 .expect("CPU staged Metal upload");
7334
7335 assert_eq!(surface.residency(), SurfaceResidency::CpuStagedMetalUpload);
7336 }
7337
7338 #[test]
7339 fn auto_route_prefers_cpu_host_for_region_scaled_even_with_restart_packets() {
7340 let decoder = CpuDecoder::new(BASELINE_420_RESTART).expect("restart decoder");
7341 let packet = build_fast420_packet(BASELINE_420_RESTART).expect("restart packet");
7342
7343 assert_eq!(
7344 choose_route(
7345 &decoder,
7346 BackendRequest::Auto,
7347 PixelFormat::Rgb8,
7348 batch::BatchOp::RegionScaled {
7349 roi: Rect {
7350 x: 0,
7351 y: 0,
7352 w: 16,
7353 h: 16,
7354 },
7355 scale: Downscale::Quarter,
7356 },
7357 None,
7358 None,
7359 Some(&packet),
7360 ),
7361 routing::RouteDecision::CpuHost
7362 );
7363 }
7364
7365 #[cfg(not(target_os = "macos"))]
7366 #[test]
7367 fn session_decode_rejects_unsupported_shape_before_host_unavailability() {
7368 let mut decoder = Decoder::new(GRAYSCALE).expect("decoder");
7369 let session = MetalBackendSession::default();
7370
7371 assert!(matches!(
7372 decoder.decode_to_device_with_session(PixelFormat::Gray8, &session),
7373 Err(Error::UnsupportedMetalRequest { .. })
7374 ));
7375 }
7376}