Skip to main content

j2k_jpeg/
capabilities.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Public JPEG capability introspection for backend routing.
4
5use crate::adapter::summarize_device_batch;
6use crate::decoder::{Decoder, JpegView};
7use crate::error::JpegError;
8use crate::info::{ColorSpace, Info, Rect, SofKind};
9use crate::DeviceBatchSummary;
10use j2k_core::{BackendRequest, Downscale, PixelFormat};
11
12/// JPEG decode operation shape for capability routing.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum JpegDecodeOp {
15    /// Full-image/tile decode.
16    Full,
17    /// Source-coordinate region decode.
18    Region(Rect),
19    /// Full-image/tile decode at reduced resolution.
20    Scaled(Downscale),
21    /// Source-coordinate region decode at reduced resolution.
22    RegionScaled {
23        /// Source-coordinate region.
24        roi: Rect,
25        /// Reduced-resolution factor.
26        scale: Downscale,
27    },
28}
29
30impl JpegDecodeOp {
31    fn scale(self) -> Downscale {
32        match self {
33            Self::Full | Self::Region(_) => Downscale::None,
34            Self::Scaled(scale) | Self::RegionScaled { scale, .. } => scale,
35        }
36    }
37}
38
39/// Capability request for a JPEG decode route.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct JpegCapabilityRequest {
42    /// Decode operation shape.
43    pub op: JpegDecodeOp,
44    /// Requested output pixel format.
45    pub fmt: PixelFormat,
46}
47
48/// Complete JPEG decode request used by backend path resolution.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct JpegDecodeRequest {
51    /// Requested backend policy.
52    pub backend: BackendRequest,
53    /// Requested output pixel format.
54    pub fmt: PixelFormat,
55    /// Decode operation shape.
56    pub op: JpegDecodeOp,
57}
58
59impl JpegDecodeRequest {
60    /// Return the capability-only portion of the request.
61    #[must_use]
62    pub const fn capability(self) -> JpegCapabilityRequest {
63        JpegCapabilityRequest {
64            op: self.op,
65            fmt: self.fmt,
66        }
67    }
68}
69
70/// Normalized JPEG decode path selected for a request.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum JpegResolvedDecodePath {
73    /// Portable CPU host decode.
74    CpuHost,
75    /// J2K-owned CUDA RGB8 decode path.
76    OwnedCudaRgb8,
77    /// J2K Metal fast-packet decode path.
78    MetalFast,
79    /// Request cannot be satisfied by this path resolver.
80    Rejected {
81        /// Backend requested by the caller.
82        backend: BackendRequest,
83        /// Stable rejection reason.
84        reason: &'static str,
85    },
86}
87
88/// Parsed JPEG metadata plus the selected backend path.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct JpegResolvedDecode {
91    /// Original decode request.
92    pub request: JpegDecodeRequest,
93    /// Capability report used for the decision.
94    pub capabilities: JpegCapabilityReport,
95    /// Output rectangle after ROI and scale are applied.
96    pub output_rect: Rect,
97    /// Selected backend path.
98    pub path: JpegResolvedDecodePath,
99}
100
101impl JpegResolvedDecode {
102    /// Inspect JPEG bytes and resolve the requested backend path.
103    ///
104    /// # Errors
105    ///
106    /// Returns a JPEG parse or capability error when `input` is malformed or
107    /// the requested operation cannot be described safely.
108    pub fn inspect(input: &[u8], request: JpegDecodeRequest) -> Result<Self, JpegError> {
109        let capabilities = JpegCapabilityReport::inspect(input, request.capability())?;
110        Ok(Self::from_capabilities(capabilities, request))
111    }
112
113    /// Resolve a path from an existing capability report.
114    #[must_use]
115    pub fn from_capabilities(
116        capabilities: JpegCapabilityReport,
117        request: JpegDecodeRequest,
118    ) -> Self {
119        let output_rect = output_rect_for_request(&capabilities.info, request.op);
120        let path = capabilities.resolve_path(request.backend);
121        Self {
122            request,
123            capabilities,
124            output_rect,
125            path,
126        }
127    }
128}
129
130/// Backend eligibility result with a stable rejection reason.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct JpegBackendEligibility {
133    /// Whether this backend can handle the requested decode shape.
134    pub eligible: bool,
135    /// Static rejection reason when `eligible` is false.
136    pub reason: Option<&'static str>,
137}
138
139impl JpegBackendEligibility {
140    const fn eligible() -> Self {
141        Self {
142            eligible: true,
143            reason: None,
144        }
145    }
146
147    const fn rejected(reason: &'static str) -> Self {
148        Self {
149            eligible: false,
150            reason: Some(reason),
151        }
152    }
153}
154
155/// Parsed JPEG metadata and backend eligibility for one request.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct JpegCapabilityReport {
158    /// Original capability request.
159    pub request: JpegCapabilityRequest,
160    /// Public JPEG metadata.
161    pub info: Info,
162    /// Device batch summary derived from J2K's parser/planner.
163    pub device: DeviceBatchSummary,
164    /// Portable CPU decode eligibility.
165    pub cpu: JpegBackendEligibility,
166    /// J2K-owned CUDA-kernel eligibility.
167    pub owned_cuda: JpegBackendEligibility,
168    /// Metal fast-packet shape eligibility.
169    pub metal_fast: JpegBackendEligibility,
170}
171
172impl JpegCapabilityReport {
173    /// Inspect JPEG bytes and report decode-route eligibility.
174    ///
175    /// # Errors
176    /// Returns [`JpegError`] when JPEG header parsing fails or planner
177    /// validation finds malformed decode-table state. Parseable JPEG classes
178    /// that J2K has not implemented yet still return a report with
179    /// rejected backend eligibility.
180    pub fn inspect(input: &[u8], request: JpegCapabilityRequest) -> Result<Self, JpegError> {
181        let view = JpegView::parse(input)?;
182        let info = view.info().clone();
183        let has_lossless_subsampled_color_capability_shape =
184            view.has_lossless_subsampled_color_capability_shape();
185        match Decoder::from_view(view) {
186            Ok(decoder) => Ok(Self::for_decoder(&decoder, request)),
187            Err(err)
188                if can_report_from_parsed_info(
189                    &err,
190                    has_lossless_subsampled_color_capability_shape,
191                ) =>
192            {
193                Ok(Self::for_planner_rejected_info(info, request, &err))
194            }
195            Err(err) => Err(err),
196        }
197    }
198
199    /// Build a capability report from an already parsed decoder.
200    #[must_use]
201    pub fn for_decoder(decoder: &Decoder<'_>, request: JpegCapabilityRequest) -> Self {
202        let info = decoder.info().clone();
203        let device = summarize_device_batch(decoder, 4);
204        Self {
205            request,
206            info: info.clone(),
207            device,
208            cpu: cpu_eligibility(&info, request),
209            owned_cuda: owned_cuda_eligibility(&info, device, request),
210            metal_fast: metal_fast_eligibility(&info, device, request),
211        }
212    }
213
214    #[expect(
215        clippy::needless_pass_by_value,
216        reason = "the small Copy capability request is stored by value in the resulting report"
217    )]
218    fn for_parsed_info(info: Info, request: JpegCapabilityRequest) -> Self {
219        let device = unavailable_device_summary(&info);
220        Self {
221            request,
222            info: info.clone(),
223            device,
224            cpu: cpu_eligibility(&info, request),
225            owned_cuda: owned_cuda_eligibility(&info, device, request),
226            metal_fast: metal_fast_eligibility(&info, device, request),
227        }
228    }
229
230    fn for_planner_rejected_info(
231        info: Info,
232        request: JpegCapabilityRequest,
233        err: &JpegError,
234    ) -> Self {
235        let mut report = Self::for_parsed_info(info, request);
236        if report.cpu.eligible && matches!(err, JpegError::NotImplemented { .. }) {
237            report.cpu = JpegBackendEligibility::rejected(
238                "JPEG CPU decode planner rejected this stream shape before decode",
239            );
240        }
241        report
242    }
243
244    /// Eligibility for explicit reusable RGB8 Metal batch outputs.
245    ///
246    /// This is narrower than [`Self::metal_fast`]: it describes the current
247    /// caller-owned Metal buffer/texture batch APIs, not every Metal-capable
248    /// surface decode shape.
249    #[must_use]
250    pub fn metal_resident_rgb8_batch_output(&self) -> JpegBackendEligibility {
251        metal_resident_rgb8_batch_output_eligibility(self.device, self.request)
252    }
253
254    /// Resolve a backend request using this report's eligibility results.
255    #[must_use]
256    pub fn resolve_path(&self, backend: BackendRequest) -> JpegResolvedDecodePath {
257        match backend {
258            BackendRequest::Cpu => {
259                if self.cpu.eligible {
260                    JpegResolvedDecodePath::CpuHost
261                } else {
262                    JpegResolvedDecodePath::Rejected {
263                        backend,
264                        reason: self
265                            .cpu
266                            .reason
267                            .unwrap_or("JPEG CPU decode rejected this request"),
268                    }
269                }
270            }
271            BackendRequest::Auto => JpegResolvedDecodePath::CpuHost,
272            BackendRequest::Cuda => {
273                if self.owned_cuda.eligible {
274                    JpegResolvedDecodePath::OwnedCudaRgb8
275                } else {
276                    JpegResolvedDecodePath::Rejected {
277                        backend,
278                        reason: self
279                            .owned_cuda
280                            .reason
281                            .unwrap_or("J2K-owned CUDA JPEG decode rejected this request"),
282                    }
283                }
284            }
285            BackendRequest::Metal => {
286                if self.metal_fast.eligible {
287                    JpegResolvedDecodePath::MetalFast
288                } else {
289                    JpegResolvedDecodePath::Rejected {
290                        backend,
291                        reason: self
292                            .metal_fast
293                            .reason
294                            .unwrap_or("JPEG Metal fast path rejected this request"),
295                    }
296                }
297            }
298        }
299    }
300}
301
302fn output_rect_for_request(info: &Info, op: JpegDecodeOp) -> Rect {
303    match op {
304        JpegDecodeOp::Full => Rect::full(info.dimensions),
305        JpegDecodeOp::Region(roi) => roi,
306        JpegDecodeOp::Scaled(scale) => scaled_rect(Rect::full(info.dimensions), scale),
307        JpegDecodeOp::RegionScaled { roi, scale } => scaled_rect(roi, scale),
308    }
309}
310
311fn scaled_rect(rect: Rect, scale: Downscale) -> Rect {
312    let denom = scale.denominator();
313    let x_end = rect.x.saturating_add(rect.w);
314    let y_end = rect.y.saturating_add(rect.h);
315    let x0 = rect.x / denom;
316    let y0 = rect.y / denom;
317    let x1 = x_end.div_ceil(denom);
318    let y1 = y_end.div_ceil(denom);
319    Rect {
320        x: x0,
321        y: y0,
322        w: x1.saturating_sub(x0),
323        h: y1.saturating_sub(y0),
324    }
325}
326
327fn cpu_eligibility(info: &Info, request: JpegCapabilityRequest) -> JpegBackendEligibility {
328    match info.sof_kind {
329        SofKind::Extended12 if is_twelve_bit_output_request(request.fmt) => {
330            return twelve_bit_eligibility(info, request.fmt, TwelveBitSof::Extended);
331        }
332        SofKind::Progressive12 if is_twelve_bit_output_request(request.fmt) => {
333            return twelve_bit_eligibility(info, request.fmt, TwelveBitSof::Progressive);
334        }
335        SofKind::Extended12 | SofKind::Progressive12 => {
336            return JpegBackendEligibility::rejected(
337                "JPEG CPU decode does not yet support this 12-bit JPEG output",
338            )
339        }
340        SofKind::Lossless
341            if matches!(
342                request.fmt,
343                PixelFormat::Gray8
344                    | PixelFormat::Gray16
345                    | PixelFormat::Rgb8
346                    | PixelFormat::Rgba8
347                    | PixelFormat::Rgb16
348                    | PixelFormat::Rgba16
349            ) =>
350        {
351            return match (info.color_space, info.bit_depth, request.fmt) {
352                (ColorSpace::Grayscale, 8, PixelFormat::Gray8)
353                | (ColorSpace::Grayscale, 16, PixelFormat::Gray16) => {
354                    JpegBackendEligibility::eligible()
355                }
356                (
357                    ColorSpace::Rgb | ColorSpace::YCbCr,
358                    8,
359                    PixelFormat::Rgb8 | PixelFormat::Rgba8,
360                )
361                | (
362                    ColorSpace::Rgb | ColorSpace::YCbCr,
363                    16,
364                    PixelFormat::Rgb16 | PixelFormat::Rgba16,
365                )
366                    if is_supported_lossless_color_sampling(info) =>
367                {
368                    JpegBackendEligibility::eligible()
369                }
370                (ColorSpace::Rgb, 8, PixelFormat::Rgb8 | PixelFormat::Rgba8)
371                | (ColorSpace::Rgb, 16, PixelFormat::Rgb16 | PixelFormat::Rgba16) => JpegBackendEligibility::rejected(
372                    "JPEG CPU lossless SOF3 APP14 RGB decode currently supports 4:4:4 sampling, even-width 8/16-bit 4:2:2 sampling, or even-dimension 8/16-bit 4:2:0 sampling",
373                ),
374                (ColorSpace::YCbCr, 8, PixelFormat::Rgb8 | PixelFormat::Rgba8)
375                | (ColorSpace::YCbCr, 16, PixelFormat::Rgb16 | PixelFormat::Rgba16) => JpegBackendEligibility::rejected(
376                    "JPEG CPU lossless SOF3 YCbCr decode currently supports 4:4:4 sampling, even-width 8/16-bit 4:2:2 sampling, or even-dimension 8/16-bit 4:2:0 sampling",
377                ),
378                _ => JpegBackendEligibility::rejected(
379                    "JPEG CPU lossless SOF3 decode currently supports 8-bit Gray8, 16-bit Gray16, 8-bit YCbCr Rgb8/Rgba8 including even-width 4:2:2 and even-dimension 4:2:0, 16-bit YCbCr Rgb16/Rgba16 including even-width 4:2:2 and even-dimension 4:2:0, 8-bit APP14 RGB Rgb8/Rgba8 including even-width 4:2:2 and even-dimension 4:2:0, or 16-bit APP14 RGB Rgb16/Rgba16 including even-width 4:2:2 and even-dimension 4:2:0 output only",
380                ),
381            };
382        }
383        SofKind::Lossless => {
384            return JpegBackendEligibility::rejected(
385                "JPEG CPU decode does not yet support lossless SOF3 JPEG",
386            )
387        }
388        SofKind::Baseline8 | SofKind::Extended8 | SofKind::Progressive8 => {}
389    }
390
391    match (request.fmt, request.op.scale()) {
392        (PixelFormat::Rgb8 | PixelFormat::Rgba8 | PixelFormat::Gray8, _) => {
393            JpegBackendEligibility::eligible()
394        }
395        (PixelFormat::Rgb16 | PixelFormat::Rgba16 | PixelFormat::Gray16, _) => {
396            JpegBackendEligibility::rejected("JPEG CPU decode does not support 16-bit output")
397        }
398        _ => JpegBackendEligibility::rejected("unsupported JPEG CPU output format"),
399    }
400}
401
402#[derive(Debug, Clone, Copy)]
403enum TwelveBitSof {
404    Extended,
405    Progressive,
406}
407
408impl TwelveBitSof {
409    const fn ycbcr_sampling_reason(self) -> &'static str {
410        match self {
411            Self::Extended => {
412                "JPEG CPU 12-bit extended YCbCr decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
413            }
414            Self::Progressive => {
415                "JPEG CPU 12-bit progressive YCbCr decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
416            }
417        }
418    }
419
420    const fn rgb_sampling_reason(self) -> &'static str {
421        match self {
422            Self::Extended => {
423                "JPEG CPU 12-bit extended RGB decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
424            }
425            Self::Progressive => {
426                "JPEG CPU 12-bit progressive RGB decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
427            }
428        }
429    }
430
431    const fn four_component_sampling_reason(self) -> &'static str {
432        match self {
433            Self::Extended => {
434                "JPEG CPU 12-bit extended four-component CMYK/YCCK decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
435            }
436            Self::Progressive => {
437                "JPEG CPU 12-bit progressive four-component CMYK/YCCK decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
438            }
439        }
440    }
441
442    const fn output_reason(self) -> &'static str {
443        match self {
444            Self::Extended => {
445                "JPEG CPU 12-bit extended decode currently supports grayscale Gray16/Rgb16/Rgba16, APP14 RGB 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16, YCbCr 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16, or CMYK/YCCK 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16 only"
446            }
447            Self::Progressive => {
448                "JPEG CPU 12-bit progressive decode currently supports grayscale Gray16/Rgb16/Rgba16, APP14 RGB 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16, YCbCr 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16, or CMYK/YCCK 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16 only"
449            }
450        }
451    }
452}
453
454fn is_twelve_bit_output_request(fmt: PixelFormat) -> bool {
455    matches!(
456        fmt,
457        PixelFormat::Gray16 | PixelFormat::Rgb16 | PixelFormat::Rgba16
458    )
459}
460
461fn twelve_bit_eligibility(
462    info: &Info,
463    fmt: PixelFormat,
464    sof: TwelveBitSof,
465) -> JpegBackendEligibility {
466    match (info.color_space, fmt) {
467        (ColorSpace::Grayscale, PixelFormat::Gray16 | PixelFormat::Rgb16 | PixelFormat::Rgba16) => {
468            JpegBackendEligibility::eligible()
469        }
470        (ColorSpace::Rgb | ColorSpace::YCbCr, PixelFormat::Rgb16 | PixelFormat::Rgba16)
471            if is_supported_12bit_three_component_sampling(info) =>
472        {
473            JpegBackendEligibility::eligible()
474        }
475        (ColorSpace::Cmyk | ColorSpace::Ycck, PixelFormat::Rgb16 | PixelFormat::Rgba16)
476            if is_supported_extended12_four_component_sampling(info) =>
477        {
478            JpegBackendEligibility::eligible()
479        }
480        (ColorSpace::YCbCr, PixelFormat::Rgb16 | PixelFormat::Rgba16) => {
481            JpegBackendEligibility::rejected(sof.ycbcr_sampling_reason())
482        }
483        (ColorSpace::Rgb, PixelFormat::Rgb16 | PixelFormat::Rgba16) => {
484            JpegBackendEligibility::rejected(sof.rgb_sampling_reason())
485        }
486        (ColorSpace::Cmyk | ColorSpace::Ycck, PixelFormat::Rgb16 | PixelFormat::Rgba16) => {
487            JpegBackendEligibility::rejected(sof.four_component_sampling_reason())
488        }
489        _ => JpegBackendEligibility::rejected(sof.output_reason()),
490    }
491}
492
493fn is_supported_extended12_four_component_sampling(info: &Info) -> bool {
494    info.sampling.len() == 4
495        && matches!(
496            (
497                info.sampling.max_h,
498                info.sampling.max_v,
499                info.sampling.components()
500            ),
501            (1, 1, [(1, 1), (1, 1), (1, 1), (1, 1)])
502                | (2, 1, [(2, 1), (1, 1), (1, 1), (1, 1)])
503                | (2, 2, [(2, 2), (1, 1), (1, 1), (1, 1)])
504        )
505}
506
507fn is_supported_12bit_three_component_sampling(info: &Info) -> bool {
508    info.sampling.len() == 3
509        && matches!(
510            (
511                info.sampling.max_h,
512                info.sampling.max_v,
513                info.sampling.components()
514            ),
515            (1, 1, [(1, 1), (1, 1), (1, 1)])
516                | (2, 1, [(2, 1), (1, 1), (1, 1)])
517                | (2, 2, [(2, 2), (1, 1), (1, 1)])
518        )
519}
520
521fn is_supported_lossless_color_sampling(info: &Info) -> bool {
522    info.sampling.len() == 3
523        && matches!(
524            (
525                info.bit_depth,
526                info.dimensions.0.is_multiple_of(2),
527                info.dimensions.1.is_multiple_of(2),
528                info.sampling.max_h,
529                info.sampling.max_v,
530                info.sampling.components()
531            ),
532            (_, _, _, 1, 1, [(1, 1), (1, 1), (1, 1)])
533                | (8 | 16, true, _, 2, 1, [(2, 1), (1, 1), (1, 1)])
534                | (8 | 16, true, true, 2, 2, [(2, 2), (1, 1), (1, 1)])
535        )
536}
537
538fn owned_cuda_eligibility(
539    info: &Info,
540    device: DeviceBatchSummary,
541    request: JpegCapabilityRequest,
542) -> JpegBackendEligibility {
543    if request.op != JpegDecodeOp::Full || request.fmt != PixelFormat::Rgb8 {
544        return JpegBackendEligibility::rejected(
545            "J2K-owned CUDA JPEG decode currently supports full-tile RGB8 fast 4:2:0, 4:2:2, or 4:4:4 only",
546        );
547    }
548    if !matches!(info.sof_kind, SofKind::Baseline8 | SofKind::Extended8) {
549        return JpegBackendEligibility::rejected(
550            "J2K-owned CUDA JPEG decode supports baseline/extended 8-bit sequential JPEG only",
551        );
552    }
553    if info.color_space != ColorSpace::YCbCr
554        || !(device.matches_fast_420 || device.matches_fast_422 || device.matches_fast_444)
555    {
556        return JpegBackendEligibility::rejected(
557            "J2K-owned CUDA JPEG decode currently requires a YCbCr 4:2:0, 4:2:2, or 4:4:4 fast packet shape",
558        );
559    }
560    if !owned_cuda_rgb8_output_is_addressable(info.dimensions) {
561        return JpegBackendEligibility::rejected(
562            "J2K-owned CUDA JPEG decode requires RGB8 output addressable by u32 byte offsets",
563        );
564    }
565    JpegBackendEligibility::eligible()
566}
567
568fn owned_cuda_rgb8_output_is_addressable((width, height): (u32, u32)) -> bool {
569    u64::from(width)
570        .checked_mul(u64::from(height))
571        .and_then(|pixels| pixels.checked_mul(3))
572        .is_some_and(|bytes| bytes <= u64::from(u32::MAX) + 1)
573}
574
575fn metal_fast_eligibility(
576    info: &Info,
577    device: DeviceBatchSummary,
578    request: JpegCapabilityRequest,
579) -> JpegBackendEligibility {
580    if !matches!(
581        request.fmt,
582        PixelFormat::Gray8 | PixelFormat::Rgb8 | PixelFormat::Rgba8
583    ) {
584        return JpegBackendEligibility::rejected(
585            "JPEG Metal fast path supports Gray8, Rgb8, or Rgba8 output formats",
586        );
587    }
588    if !matches!(info.sof_kind, SofKind::Baseline8 | SofKind::Extended8) {
589        return JpegBackendEligibility::rejected(
590            "JPEG Metal fast path currently supports baseline/extended 8-bit sequential JPEG only",
591        );
592    }
593    if !matches!(
594        info.color_space,
595        ColorSpace::Grayscale | ColorSpace::YCbCr | ColorSpace::Rgb
596    ) {
597        return JpegBackendEligibility::rejected(
598            "JPEG Metal fast path requires grayscale, YCbCr, or RGB input color",
599        );
600    }
601    if device.matches_fast_420 || device.matches_fast_422 || device.matches_fast_444 {
602        JpegBackendEligibility::eligible()
603    } else {
604        JpegBackendEligibility::rejected(
605            "JPEG Metal fast path requires a fast 4:2:0, 4:2:2, or 4:4:4 packet shape",
606        )
607    }
608}
609
610fn metal_resident_rgb8_batch_output_eligibility(
611    device: DeviceBatchSummary,
612    request: JpegCapabilityRequest,
613) -> JpegBackendEligibility {
614    if request.fmt != PixelFormat::Rgb8 {
615        return JpegBackendEligibility::rejected(
616            "JPEG Metal reusable resident batch output currently supports RGB8 output only",
617        );
618    }
619    if !(device.matches_fast_420 || device.matches_fast_422 || device.matches_fast_444) {
620        return JpegBackendEligibility::rejected(
621            "JPEG Metal reusable resident batch output requires a fast 4:2:0, 4:2:2, or 4:4:4 packet shape",
622        );
623    }
624
625    match request.op {
626        JpegDecodeOp::Full => JpegBackendEligibility::eligible(),
627        JpegDecodeOp::Scaled(scale) | JpegDecodeOp::RegionScaled { scale, .. }
628            if supports_metal_resident_batch_scale(scale) =>
629        {
630            JpegBackendEligibility::eligible()
631        }
632        JpegDecodeOp::Scaled(_) | JpegDecodeOp::RegionScaled { .. } => {
633            JpegBackendEligibility::rejected(
634                "JPEG Metal reusable resident batch output currently supports half, quarter, or eighth scaling",
635            )
636        }
637        JpegDecodeOp::Region(_) => JpegBackendEligibility::rejected(
638            "JPEG Metal reusable resident batch output currently supports full, scaled, or region-scaled decode shapes",
639        ),
640    }
641}
642
643fn supports_metal_resident_batch_scale(scale: Downscale) -> bool {
644    matches!(
645        scale,
646        Downscale::Half | Downscale::Quarter | Downscale::Eighth
647    )
648}
649
650fn can_report_from_parsed_info(
651    err: &JpegError,
652    has_lossless_subsampled_color_capability_shape: bool,
653) -> bool {
654    match err {
655        JpegError::UnsupportedColorSpace { .. } => true,
656        JpegError::NotImplemented { sof } if *sof != SofKind::Lossless => true,
657        JpegError::NotImplemented {
658            sof: SofKind::Lossless,
659        } => has_lossless_subsampled_color_capability_shape,
660        _ => false,
661    }
662}
663
664fn unavailable_device_summary(info: &Info) -> DeviceBatchSummary {
665    DeviceBatchSummary {
666        restart_interval: info.restart_interval,
667        checkpoint_count: 0,
668        matches_fast_420: false,
669        matches_fast_422: false,
670        matches_fast_444: false,
671    }
672}