j2k-metal 0.7.1

Metal decoder and encode-stage adapter for j2k
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0

#[cfg(target_os = "macos")]
use std::sync::Arc;

use j2k_core::{
    BackendKind, BackendRequest, CodecError, DeviceSurface, Downscale, PixelFormat, Rect,
};
#[cfg(target_os = "macos")]
use metal::Device;

#[cfg(target_os = "macos")]
use super::is_direct_runtime_fallback_error;
use super::surface::upload_surface;
#[cfg(target_os = "macos")]
use super::J2kDecoder;
use super::{DecodeOperation, MetalDecodeRequest};
use crate::{batch, Error, Storage, Surface, SurfaceResidency};
#[cfg(target_os = "macos")]
use crate::{hybrid, MetalBackendSession, MetalDirectFallbackReason};

#[cfg(target_os = "macos")]
fn should_run_metal_runtime() -> bool {
    j2k_test_support::metal_runtime_gate(module_path!())
}

#[cfg(target_os = "macos")]
#[test]
fn direct_runtime_fallback_classification_uses_structured_variant() {
    let fallback = Error::MetalDirectFallback {
        message: "arbitrary fallback text".to_string(),
        reason: MetalDirectFallbackReason::UnsupportedRuntimeInput,
    };
    assert!(is_direct_runtime_fallback_error(&fallback));

    let message_only = Error::MetalKernel {
        message: "unsupported classic kernel input in direct component plan".to_string(),
    };
    assert!(!is_direct_runtime_fallback_error(&message_only));
}

#[test]
fn metal_decode_request_maps_geometry_to_report_and_batch_ops() {
    let roi = Rect {
        x: 1,
        y: 2,
        w: 3,
        h: 4,
    };
    let requests = [
        (
            MetalDecodeRequest::full(PixelFormat::Gray8, BackendRequest::Auto),
            DecodeOperation::Full,
            batch::BatchOp::Full,
        ),
        (
            MetalDecodeRequest::region(PixelFormat::Gray8, roi, BackendRequest::Auto),
            DecodeOperation::Region,
            batch::BatchOp::Region(roi),
        ),
        (
            MetalDecodeRequest::scaled(PixelFormat::Gray8, Downscale::Half, BackendRequest::Auto),
            DecodeOperation::Scaled,
            batch::BatchOp::Scaled(Downscale::Half),
        ),
        (
            MetalDecodeRequest::region_scaled(
                PixelFormat::Gray8,
                roi,
                Downscale::Quarter,
                BackendRequest::Auto,
            ),
            DecodeOperation::RegionScaled,
            batch::BatchOp::RegionScaled {
                roi,
                scale: Downscale::Quarter,
            },
        ),
    ];

    for (request, report_operation, batch_op) in requests {
        assert_eq!(request.op.report_operation(), report_operation);
        assert_eq!(request.op.batch_op(), batch_op);
    }
}

#[test]
fn metal_runtime_failures_are_not_unsupported_errors() {
    for err in [
        Error::MetalRuntime {
            message: "runtime".to_string(),
        },
        Error::MetalKernel {
            message: "kernel".to_string(),
        },
        Error::MetalStatePoisoned {
            state: "J2K Metal session",
        },
    ] {
        assert!(!err.is_unsupported(), "{err:?}");
    }
}

#[test]
fn cpu_uploaded_surface_reports_host_residency() {
    let surface = upload_surface(
        vec![1, 2, 3],
        (1, 1),
        PixelFormat::Rgb8,
        BackendRequest::Cpu,
    )
    .expect("create CPU surface");

    assert_eq!(surface.backend_kind(), BackendKind::Cpu);
    assert_eq!(surface.residency(), SurfaceResidency::Host);
    #[cfg(target_os = "macos")]
    assert!(surface.metal_buffer_trusted().is_none());
}

#[test]
fn download_into_reports_inconsistent_surface_storage_range() {
    let surface = Surface {
        backend: BackendKind::Cpu,
        residency: SurfaceResidency::Host,
        dimensions: (2, 1),
        fmt: PixelFormat::Gray8,
        pitch_bytes: 2,
        byte_offset: 0,
        storage: Storage::from_host(vec![7]),
    };
    let mut out = [0_u8; 2];

    let err = surface
        .download_into(&mut out, 2)
        .expect_err("inconsistent surface storage should be reported");

    assert!(matches!(
        err,
        Error::MetalKernel { message }
            if message == "J2K Metal surface byte range 0..2 exceeds storage length 1"
    ));
}

#[cfg(target_os = "macos")]
#[test]
fn metal_backend_sessions_own_distinct_direct_plan_caches() {
    if !should_run_metal_runtime() {
        return;
    }

    let Some(device) = metal::Device::system_default() else {
        j2k_test_support::metal_device_unavailable_is_skip(module_path!());
        return;
    };

    let first = MetalBackendSession::new(device.clone());
    let second = MetalBackendSession::new(device);

    assert_ne!(
        first.direct_cache_ids_for_test(),
        second.direct_cache_ids_for_test()
    );
}

#[cfg(target_os = "macos")]
#[test]
fn repeated_session_hits_share_native_and_prepared_plan_owners() {
    if !should_run_metal_runtime() {
        return;
    }

    let Some(device) = Device::system_default() else {
        j2k_test_support::metal_device_unavailable_is_skip(module_path!());
        return;
    };
    let pixels = j2k_test_support::gradient_u8(32, 32, 3);
    let bytes = j2k_native::encode(
        &pixels,
        32,
        32,
        3,
        8,
        false,
        &j2k_native::EncodeOptions {
            reversible: true,
            num_decomposition_levels: 2,
            ..j2k_native::EncodeOptions::default()
        },
    )
    .expect("encode RGB cache fixture");
    let session = MetalBackendSession::new(device);
    let request = MetalDecodeRequest::full(PixelFormat::Rgb8, BackendRequest::Metal);

    let mut first = J2kDecoder::new(&bytes).expect("first decoder");
    first
        .decode_request_to_device_with_session(request, &session)
        .expect("first session decode");
    let first_native = first
        .native_direct_color_plan
        .as_ref()
        .expect("first native plan")
        .clone();
    let first_prepared = first
        .native_prepared_direct_color_plan
        .as_ref()
        .expect("first prepared plan")
        .clone();

    let mut second = J2kDecoder::new(&bytes).expect("second decoder");
    second
        .decode_request_to_device_with_session(request, &session)
        .expect("cached session decode");
    let second_native = second
        .native_direct_color_plan
        .as_ref()
        .expect("cached native plan");
    let second_prepared = second
        .native_prepared_direct_color_plan
        .as_ref()
        .expect("cached prepared plan");

    assert!(Arc::ptr_eq(&first_native, second_native));
    assert!(Arc::ptr_eq(&first_prepared, second_prepared));
}

#[cfg(target_os = "macos")]
#[test]
fn explicit_metal_request_does_not_stage_cpu_pixels() {
    if !should_run_metal_runtime() {
        return;
    }

    if Device::system_default().is_none() {
        j2k_test_support::metal_device_unavailable_is_skip(module_path!());
        return;
    }

    let result = upload_surface(
        vec![1, 2, 3],
        (1, 1),
        PixelFormat::Rgb8,
        BackendRequest::Metal,
    );

    assert!(matches!(
        result,
        Err(Error::UnsupportedMetalRequest { reason })
            if reason.contains("CPU-staged")
                && reason.contains("explicit")
                && reason.contains("Metal")
    ));
}

#[cfg(target_os = "macos")]
#[test]
fn repeated_region_scaled_color_batch_reuses_prepared_plan() {
    if !should_run_metal_runtime() {
        return;
    }

    if Device::system_default().is_none() {
        j2k_test_support::metal_device_unavailable_is_skip(module_path!());
        return;
    }

    let pixels = j2k_test_support::gradient_u8(64, 64, 3);
    let options = j2k_native::EncodeOptions {
        reversible: true,
        num_decomposition_levels: 2,
        ..j2k_native::EncodeOptions::default()
    };
    let input = Arc::<[u8]>::from(
        j2k_native::encode(&pixels, 64, 64, 3, 8, false, &options).expect("encode rgb8"),
    );
    let roi = Rect {
        x: 8,
        y: 8,
        w: 32,
        h: 32,
    };
    let scale = Downscale::Quarter;
    let requests = vec![(input.clone(), roi, scale); 4];
    let _guard = hybrid::region_scaled_color_plan_test_lock_for_test();
    hybrid::reset_region_scaled_color_plan_builds_for_test();

    let surfaces =
        hybrid::decode_region_scaled_color_batch_direct_to_device(&requests, PixelFormat::Rgb8)
            .expect("repeated RGB region-scaled batch");

    assert_eq!(surfaces.len(), requests.len());
    assert_eq!(
        hybrid::region_scaled_color_plan_builds_for_test(),
        1,
        "repeated RGB ROI+scaled batches should build and crop one prepared direct color plan"
    );
}

#[test]
fn decoder_modules_remain_focused_without_suppression_shortcuts() {
    const MODULES: [(&str, &str, usize); 6] = [
        ("adapters.rs", include_str!("adapters.rs"), 300),
        ("core.rs", include_str!("core.rs"), 400),
        ("direct_paths.rs", include_str!("direct_paths.rs"), 600),
        ("request.rs", include_str!("request.rs"), 220),
        ("routes.rs", include_str!("routes.rs"), 380),
        ("surface.rs", include_str!("surface.rs"), 120),
    ];

    let root = include_str!("../decoder.rs");
    assert!(
        root.lines().count() <= 40,
        "decoder.rs should remain a small explicit module facade"
    );

    for (name, source, cap) in MODULES {
        assert!(
            source.lines().count() <= cap,
            "{name} has {} lines, exceeding its {cap}-line focus cap",
            source.lines().count()
        );
        assert!(
            !source.contains("include!("),
            "{name} must be a real module"
        );
        assert!(
            !source.contains("#![allow"),
            "{name} must not use module-wide lint suppression"
        );
        assert!(
            !source.contains("allow(unused"),
            "{name} must not suppress unused-code findings"
        );
        assert!(
            !source.contains("use super::*") && !source.contains("use crate::*"),
            "{name} must use explicit imports"
        );
    }
}