diff --git a/crates/j2k-metal/src/engine.rs b/crates/j2k-metal/src/engine.rs index 522b27ae..f5b881b7 100644 --- a/crates/j2k-metal/src/engine.rs +++ b/crates/j2k-metal/src/engine.rs @@ -64,9 +64,10 @@ pub(crate) use self::direct_buffers::{ }; #[cfg(target_os = "macos")] use self::direct_buffers::{ - copied_recyclable_shared_slice_buffer, copied_slice_buffer, new_private_buffer, - new_shared_buffer, new_shared_buffer_with_slice, take_classic_coefficients_scratch_buffer, - take_classic_states_scratch_buffer, zeroed_recyclable_shared_buffer, zeroed_shared_buffer, + checked_buffer_copy_into, copied_recyclable_shared_slice_buffer, copied_slice_buffer, + new_private_buffer, new_shared_buffer, new_shared_buffer_with_slice, + take_classic_coefficients_scratch_buffer, take_classic_states_scratch_buffer, + zeroed_recyclable_shared_buffer, zeroed_shared_buffer, }; #[cfg(target_os = "macos")] mod direct_commands; @@ -190,8 +191,9 @@ pub(crate) use self::test_counters::{ ht_batch_coefficient_copy_blits_for_test, hybrid_cpu_decode_inputs_for_test, hybrid_cpu_decode_worker_inits_for_test, hybrid_repeated_output_blits_for_test, hybrid_stacked_component_batches_for_test, idwt97_stage_sequences_for_test, - lossless_deinterleave_rct_fused_dispatches_for_test, metal_command_buffers_for_test, - metal_compute_encoders_for_test, reset_classic_gpu_token_pack_dispatches_for_test, + idwt_host_transfer_counters_for_test, lossless_deinterleave_rct_fused_dispatches_for_test, + metal_command_buffers_for_test, metal_compute_encoders_for_test, + reset_classic_gpu_token_pack_dispatches_for_test, reset_classic_split_mq_byte_gpu_token_pack_dispatches_for_test, reset_direct_destination_event_bridge_for_test, reset_direct_tier1_input_buffer_prepares_for_test, @@ -199,6 +201,7 @@ pub(crate) use self::test_counters::{ reset_ht_batch_coefficient_copy_blits_for_test, reset_hybrid_cpu_decode_inputs_for_test, reset_hybrid_cpu_decode_worker_inits_for_test, reset_hybrid_repeated_output_blits_for_test, reset_hybrid_stacked_component_batches_for_test, reset_idwt97_stage_sequences_for_test, + reset_idwt_host_transfer_counters_for_test, reset_lossless_deinterleave_rct_fused_dispatches_for_test, reset_metal_command_buffers_for_test, reset_metal_compute_encoders_for_test, reset_resident_codestream_command_buffer_waits_for_test, diff --git a/crates/j2k-metal/src/engine/decode_dispatch.rs b/crates/j2k-metal/src/engine/decode_dispatch.rs index f264cbf7..43d68c40 100644 --- a/crates/j2k-metal/src/engine/decode_dispatch.rs +++ b/crates/j2k-metal/src/engine/decode_dispatch.rs @@ -17,14 +17,15 @@ use super::abi::{ J2kStoreParams, J2K_CLASSIC_MAX_HEIGHT, J2K_CLASSIC_MAX_WIDTH, J2K_CLASSIC_STATUS_OK, }; use super::{ - checked_buffer_slice, commit_and_wait_metal, copied_slice_buffer, decode_classic_status_error, - j2k_u32_param, new_command_buffer, new_compute_command_encoder, new_shared_buffer, - take_classic_coefficients_scratch_buffer, take_classic_states_scratch_buffer, with_runtime, - zeroed_shared_buffer, Buffer, CommandBufferRef, ComputeCommandEncoderRef, - DirectIdwtCommandBuffers, DirectScratchBuffer, DirectStatusCheck, Error, HtCodeBlockDecodeJob, - J2kInverseMctJob, J2kSingleDecompositionIdwtJob, J2kStoreComponentJob, J2kWaveletTransform, - MetalRuntime, PixelFormat, PreparedClassicSubBand, PreparedClassicSubBandGroup, - PreparedHtSubBand, PreparedHtSubBandGroup, Surface, + checked_buffer_copy_into, checked_buffer_slice, commit_and_wait_metal, copied_slice_buffer, + decode_classic_status_error, j2k_u32_param, new_command_buffer, new_compute_command_encoder, + new_shared_buffer, take_classic_coefficients_scratch_buffer, + take_classic_states_scratch_buffer, with_runtime, zeroed_shared_buffer, Buffer, + CommandBufferRef, ComputeCommandEncoderRef, DirectIdwtCommandBuffers, DirectScratchBuffer, + DirectStatusCheck, Error, HtCodeBlockDecodeJob, J2kInverseMctJob, + J2kSingleDecompositionIdwtJob, J2kStoreComponentJob, J2kWaveletTransform, MetalRuntime, + PixelFormat, PreparedClassicSubBand, PreparedClassicSubBandGroup, PreparedHtSubBand, + PreparedHtSubBandGroup, Surface, }; mod classic_cleanup; diff --git a/crates/j2k-metal/src/engine/decode_dispatch/idwt.rs b/crates/j2k-metal/src/engine/decode_dispatch/idwt.rs index 3a1ae694..d608fb15 100644 --- a/crates/j2k-metal/src/engine/decode_dispatch/idwt.rs +++ b/crates/j2k-metal/src/engine/decode_dispatch/idwt.rs @@ -4,7 +4,7 @@ use crate::metal_types::prelude::*; use super::{ - checked_buffer_slice, commit_and_wait_metal, copied_slice_buffer, dispatch_2d_pipeline, + checked_buffer_copy_into, commit_and_wait_metal, copied_slice_buffer, dispatch_2d_pipeline, dispatch_3d_pipeline, hybrid_stage_signpost, label_compute_encoder, new_command_buffer, new_compute_command_encoder, with_runtime, Buffer, CommandBufferRef, ComputeCommandEncoderRef, DirectIdwtCommandBuffers, Error, J2kIdwtSingleDecompositionParams, @@ -76,6 +76,10 @@ pub(crate) fn decode_reversible53_single_decomposition_idwt( let lh = copied_slice_buffer(&runtime.device, job.lh.coefficients)?; let hh = copied_slice_buffer(&runtime.device, job.hh.coefficients)?; let decoded = copied_slice_buffer(&runtime.device, output)?; + #[cfg(test)] + crate::engine::test_counters::record_idwt_host_overwritten_output_upload( + std::mem::size_of_val(output), + ); let command_buffer = new_command_buffer(&runtime.queue)?; @@ -124,8 +128,7 @@ pub(crate) fn decode_reversible53_single_decomposition_idwt( ); encoder.endEncoding(); commit_and_wait_metal(&command_buffer)?; - let decoded_host = checked_buffer_slice::(&decoded, output.len(), "IDWT output")?; - output.copy_from_slice(&decoded_host); + checked_buffer_copy_into(&decoded, 0, output, "IDWT output")?; Ok(()) }) } diff --git a/crates/j2k-metal/src/engine/decode_dispatch/idwt/irreversible.rs b/crates/j2k-metal/src/engine/decode_dispatch/idwt/irreversible.rs index 24b8f946..7608efe9 100644 --- a/crates/j2k-metal/src/engine/decode_dispatch/idwt/irreversible.rs +++ b/crates/j2k-metal/src/engine/decode_dispatch/idwt/irreversible.rs @@ -4,7 +4,7 @@ use crate::metal_types::prelude::*; use super::super::{ - checked_buffer_slice, commit_and_wait_metal, copied_slice_buffer, dispatch_2d_pipeline, + checked_buffer_copy_into, commit_and_wait_metal, copied_slice_buffer, dispatch_2d_pipeline, dispatch_3d_pipeline, hybrid_stage_signpost, label_compute_encoder, new_command_buffer, new_compute_command_encoder, with_runtime, Buffer, CommandBufferRef, ComputeCommandEncoderRef, Error, J2kIdwt97StepParams, J2kIdwtSingleDecompositionParams, J2kSingleDecompositionIdwtJob, @@ -13,6 +13,9 @@ use super::super::{ use super::{IdwtSubBandBuffers, SingleIdwtDispatch}; use j2k_codec_math::dwt; +#[cfg(test)] +use super::super::checked_buffer_slice; + pub(crate) fn decode_irreversible97_single_decomposition_idwt( job: J2kSingleDecompositionIdwtJob<'_>, output: &mut [f32], @@ -85,6 +88,10 @@ fn decode_irreversible97_staged_single_decomposition_idwt_with_high_pass( let lh = copied_slice_buffer(&runtime.device, job.lh.coefficients)?; let hh = copied_slice_buffer(&runtime.device, job.hh.coefficients)?; let decoded = copied_slice_buffer(&runtime.device, output)?; + #[cfg(test)] + crate::engine::test_counters::record_idwt_host_overwritten_output_upload( + std::mem::size_of_val(output), + ); let command_buffer = new_command_buffer(&runtime.queue)?; let encoder = new_compute_command_encoder(&command_buffer)?; dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_high_pass( @@ -110,8 +117,7 @@ fn decode_irreversible97_staged_single_decomposition_idwt_with_high_pass( encoder.endEncoding(); commit_and_wait_metal(&command_buffer)?; - let decoded_host = checked_buffer_slice::(&decoded, output.len(), "IDWT output")?; - output.copy_from_slice(&decoded_host); + checked_buffer_copy_into(&decoded, 0, output, "IDWT output")?; Ok(()) }) } diff --git a/crates/j2k-metal/src/engine/direct_buffers.rs b/crates/j2k-metal/src/engine/direct_buffers.rs index 5e1afe7d..160b54bf 100644 --- a/crates/j2k-metal/src/engine/direct_buffers.rs +++ b/crates/j2k-metal/src/engine/direct_buffers.rs @@ -73,8 +73,28 @@ pub(crate) fn checked_buffer_slice_at( ) -> Result, Error> { // SAFETY: J2K readback helpers are called only for CPU-initialized buffers // or after the producing Metal command buffer has completed. - unsafe { checked_buffer_read_vec::(buffer, byte_offset, len) } - .map_err(|error| buffer_access_error(context, error)) + let result = unsafe { checked_buffer_read_vec::(buffer, byte_offset, len) } + .map_err(|error| buffer_access_error(context, error)); + #[cfg(test)] + if let Ok(values) = &result { + if !values.is_empty() { + crate::engine::test_counters::record_idwt_host_temporary_readback_vec(size_of_val( + values.as_slice(), + )); + } + } + result +} + +pub(crate) fn checked_buffer_copy_into( + buffer: &Buffer, + byte_offset: usize, + output: &mut [T], + context: &str, +) -> Result<(), Error> { + let values = checked_buffer_slice_at(buffer, byte_offset, output.len(), context)?; + output.copy_from_slice(&values); + Ok(()) } #[cfg(target_os = "macos")] @@ -204,11 +224,18 @@ pub(super) fn take_classic_states_scratch_buffer( }) } +#[cfg(test)] +#[path = "direct_buffers/performance.rs"] +mod performance; + #[cfg(test)] mod tests { use j2k_metal_support::MetalSupportError; - use super::buffer_access_error; + use super::{ + buffer_access_error, checked_buffer_copy_into, new_private_buffer, + new_shared_buffer_with_slice, + }; use crate::Error; #[test] @@ -227,4 +254,123 @@ mod tests { && message.contains("not aligned") )); } + + #[test] + fn checked_buffer_copy_into_preserves_destination_outside_the_selected_slice() { + if !j2k_test_support::metal_runtime_gate(module_path!()) { + return; + } + crate::engine::with_runtime(|runtime| { + let source = [7.0_f32, -1.25, 2.5, -3.75, 7.0]; + let buffer = new_shared_buffer_with_slice(&runtime.device, &source)?; + let mut output = [11.0_f32; 5]; + checked_buffer_copy_into( + &buffer, + core::mem::size_of::(), + &mut output[1..4], + "selected copy", + )?; + assert_eq!(output[0].to_bits(), 11.0_f32.to_bits()); + assert_eq!(output[4].to_bits(), 11.0_f32.to_bits()); + for (actual, expected) in output[1..4].iter().zip(&source[1..4]) { + assert_eq!(actual.to_bits(), expected.to_bits()); + } + Ok(()) + }) + .expect("copy selected shared Metal buffer range"); + } + + #[test] + fn checked_buffer_copy_into_rejects_invalid_ranges_without_changing_output() { + if !j2k_test_support::metal_runtime_gate(module_path!()) { + return; + } + crate::engine::with_runtime(|runtime| { + let buffer = new_shared_buffer_with_slice(&runtime.device, &[1.0_f32, 2.0])?; + let mut output = [13.0_f32; 2]; + let alignment = checked_buffer_copy_into(&buffer, 1, &mut output[..1], "alignment") + .expect_err("misaligned typed copy must fail"); + assert!(matches!( + alignment, + Error::MetalSupport { + source: MetalSupportError::BufferAlignment { .. }, + .. + } + )); + assert!(output + .iter() + .all(|value| value.to_bits() == 13.0_f32.to_bits())); + + let bounds = checked_buffer_copy_into( + &buffer, + core::mem::size_of::(), + &mut output, + "bounds", + ) + .expect_err("out-of-bounds typed copy must fail"); + assert!(matches!( + bounds, + Error::MetalSupport { + source: MetalSupportError::BufferBounds { .. }, + .. + } + )); + assert!(output + .iter() + .all(|value| value.to_bits() == 13.0_f32.to_bits())); + + checked_buffer_copy_into::( + &buffer, + 2 * core::mem::size_of::(), + &mut [], + "empty end", + )?; + let empty_unaligned = + checked_buffer_copy_into::(&buffer, 1, &mut [], "empty unaligned") + .expect_err("empty typed copy still validates alignment"); + assert!(matches!( + empty_unaligned, + Error::MetalSupport { + source: MetalSupportError::BufferAlignment { .. }, + .. + } + )); + let past_end = checked_buffer_copy_into::( + &buffer, + 3 * core::mem::size_of::(), + &mut [], + "empty past end", + ) + .expect_err("empty copy past the buffer must fail"); + assert!(matches!( + past_end, + Error::MetalSupport { + source: MetalSupportError::BufferBounds { .. }, + .. + } + )); + + let mut zero_sized_output = [[0_u8; 0]; 1]; + let zero_sized = + checked_buffer_copy_into(&buffer, 0, &mut zero_sized_output, "zero-sized ABI") + .expect_err("zero-sized GPU ABI element must fail"); + assert!(matches!( + zero_sized, + Error::MetalSupport { + source: MetalSupportError::BufferZeroSizedType { .. }, + .. + } + )); + + let private = new_private_buffer(&runtime.device, core::mem::size_of::())?; + let private_error = checked_buffer_copy_into(&private, 0, &mut output[..1], "private") + .expect_err("private Metal storage must not be copied on the CPU"); + assert!(matches!(private_error, Error::MetalSupport { .. })); + assert!(output + .iter() + .all(|value| value.to_bits() == 13.0_f32.to_bits())); + Ok(()) + }) + .expect("validate checked Metal buffer copy failures"); + } } diff --git a/crates/j2k-metal/src/engine/direct_buffers/performance.rs b/crates/j2k-metal/src/engine/direct_buffers/performance.rs new file mode 100644 index 00000000..b43ebcb0 --- /dev/null +++ b/crates/j2k-metal/src/engine/direct_buffers/performance.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use super::{checked_buffer_copy_into, copied_slice_buffer}; +use std::time::{Duration, Instant}; + +const CASES: [(u32, u32); 2] = [(512, 512), (509, 383)]; + +fn seeded_values(width: u32, height: u32) -> Vec { + let len = width as usize * height as usize; + (0..len) + .map(|index| { + let value = f32::from(u16::try_from(index % 257).expect("seed value fits u16")); + (value - 128.0) * 0.03125 + }) + .collect() +} + +#[test] +#[ignore = "host copy performance harness; run explicitly with --ignored --nocapture"] +fn metal_checked_buffer_copy_into_perf() { + if !j2k_test_support::metal_runtime_gate(module_path!()) { + return; + } + crate::engine::with_runtime(|runtime| { + for (width, height) in CASES { + let expected = seeded_values(width, height); + let buffer = copied_slice_buffer(&runtime.device, &expected).expect("copy source"); + let mut output = vec![0.0_f32; expected.len()]; + checked_buffer_copy_into(&buffer, 0, &mut output, "copy timing probe")?; + assert!(output + .iter() + .zip(&expected) + .all(|(actual, expected)| actual.to_bits() == expected.to_bits())); + + let warm_started = Instant::now(); + let mut warm_iterations = 0_u64; + while warm_started.elapsed() < Duration::from_secs(3) { + checked_buffer_copy_into(&buffer, 0, &mut output, "copy timing warmup")?; + std::hint::black_box(&output); + warm_iterations += 1; + } + let iterations = (warm_iterations / 15).max(1); + for sample in 0..50 { + crate::engine::reset_idwt_host_transfer_counters_for_test(); + let started = Instant::now(); + for _ in 0..iterations { + checked_buffer_copy_into(&buffer, 0, &mut output, "copy timing sample")?; + std::hint::black_box(&output); + } + let elapsed = started.elapsed(); + let (upload_bytes, readback_allocations, readback_bytes) = + crate::engine::idwt_host_transfer_counters_for_test(); + println!( + "metal_checked_buffer_copy_into width={width} height={height} sample={sample} iterations={iterations} elapsed_ns={} overwritten_output_upload_bytes={upload_bytes} temporary_readback_vec_allocations={readback_allocations} temporary_readback_vec_bytes={readback_bytes}", + elapsed.as_nanos(), + ); + } + } + Ok(()) + }) + .expect("run checked Metal buffer copy timing harness"); +} diff --git a/crates/j2k-metal/src/engine/test_counters.rs b/crates/j2k-metal/src/engine/test_counters.rs index 0365bbc1..2670c83a 100644 --- a/crates/j2k-metal/src/engine/test_counters.rs +++ b/crates/j2k-metal/src/engine/test_counters.rs @@ -276,6 +276,44 @@ std::thread_local! { static IDWT97_STAGE_SEQUENCES: Cell = const { Cell::new(0) }; static IDWT97_LOGICAL_REQUESTED_POSITIONS: Cell = const { Cell::new(0) }; static IDWT97_STAGE_DISPATCHES: Cell = const { Cell::new(0) }; + static IDWT_HOST_OVERWRITTEN_OUTPUT_UPLOAD_BYTES: Cell = const { Cell::new(0) }; + static IDWT_HOST_TEMPORARY_READBACK_VEC_ALLOCATIONS: Cell = const { Cell::new(0) }; + static IDWT_HOST_TEMPORARY_READBACK_VEC_BYTES: Cell = const { Cell::new(0) }; +} + +pub(crate) fn reset_idwt_host_transfer_counters_for_test() { + IDWT_HOST_OVERWRITTEN_OUTPUT_UPLOAD_BYTES.set(0); + IDWT_HOST_TEMPORARY_READBACK_VEC_ALLOCATIONS.set(0); + IDWT_HOST_TEMPORARY_READBACK_VEC_BYTES.set(0); +} + +pub(crate) fn idwt_host_transfer_counters_for_test() -> (usize, usize, usize) { + ( + IDWT_HOST_OVERWRITTEN_OUTPUT_UPLOAD_BYTES.get(), + IDWT_HOST_TEMPORARY_READBACK_VEC_ALLOCATIONS.get(), + IDWT_HOST_TEMPORARY_READBACK_VEC_BYTES.get(), + ) +} + +pub(crate) fn record_idwt_host_overwritten_output_upload(bytes: usize) { + IDWT_HOST_OVERWRITTEN_OUTPUT_UPLOAD_BYTES.set( + IDWT_HOST_OVERWRITTEN_OUTPUT_UPLOAD_BYTES + .get() + .saturating_add(bytes), + ); +} + +pub(crate) fn record_idwt_host_temporary_readback_vec(bytes: usize) { + IDWT_HOST_TEMPORARY_READBACK_VEC_ALLOCATIONS.set( + IDWT_HOST_TEMPORARY_READBACK_VEC_ALLOCATIONS + .get() + .saturating_add(1), + ); + IDWT_HOST_TEMPORARY_READBACK_VEC_BYTES.set( + IDWT_HOST_TEMPORARY_READBACK_VEC_BYTES + .get() + .saturating_add(bytes), + ); } pub(crate) fn reset_idwt97_stage_sequences_for_test() { diff --git a/crates/j2k-metal/src/idwt.rs b/crates/j2k-metal/src/idwt.rs index 28b2bcbc..f017d7bc 100644 --- a/crates/j2k-metal/src/idwt.rs +++ b/crates/j2k-metal/src/idwt.rs @@ -728,14 +728,17 @@ mod tests { // warmup. Raw batch durations permit external CI analysis. let iterations = (warm_iterations / 15).max(1); for sample in 0..50 { + crate::engine::reset_idwt_host_transfer_counters_for_test(); let started = Instant::now(); for _ in 0..iterations { decode(); } let elapsed = started.elapsed(); + let (upload_bytes, readback_allocations, readback_bytes) = + crate::engine::idwt_host_transfer_counters_for_test(); println!( - "metal_host_slice_idwt_decode reversible={reversible} width={width} height={height} sample={sample} iterations={iterations} elapsed_ns={}", - elapsed.as_nanos() + "metal_host_slice_idwt_decode reversible={reversible} width={width} height={height} sample={sample} iterations={iterations} elapsed_ns={} overwritten_output_upload_bytes={upload_bytes} temporary_readback_vec_allocations={readback_allocations} temporary_readback_vec_bytes={readback_bytes}", + elapsed.as_nanos(), ); } } @@ -758,3 +761,7 @@ mod tests { assert_eq!(image_components.dimensions(), (4, 4)); } } + +#[cfg(all(test, target_os = "macos"))] +#[path = "idwt_host_slice_tests.rs"] +mod host_slice_tests; diff --git a/crates/j2k-metal/src/idwt_host_slice_tests.rs b/crates/j2k-metal/src/idwt_host_slice_tests.rs new file mode 100644 index 00000000..5daac64c --- /dev/null +++ b/crates/j2k-metal/src/idwt_host_slice_tests.rs @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use super::MetalIdwtDecoder; +use crate::engine::{ + idwt_host_transfer_counters_for_test, reset_idwt_host_transfer_counters_for_test, +}; +use j2k_native::{ + HtCodeBlockDecoder, J2kIdwtBand, J2kIdwtNormalization, J2kRect, J2kSingleDecompositionIdwtJob, + J2kWaveletTransform, +}; + +struct Fixture { + rect: J2kRect, + sizes: [(u32, u32); 4], + coefficients: [Vec; 4], +} + +impl Fixture { + fn new(width: u32, height: u32, x0: u32, y0: u32) -> Self { + let low_width = (x0 + width).div_ceil(2) - x0.div_ceil(2); + let low_height = (y0 + height).div_ceil(2) - y0.div_ceil(2); + let sizes = [ + (low_width, low_height), + (width - low_width, low_height), + (low_width, height - low_height), + (width - low_width, height - low_height), + ]; + Self { + rect: J2kRect { + x0, + y0, + x1: x0 + width, + y1: y0 + height, + }, + sizes, + coefficients: sizes.map(|(width, height)| { + (0..width * height) + .map(|i| f32::from(i16::try_from(i % 31).unwrap()) - 15.0) + .collect() + }), + } + } + + fn job(&self, transform: J2kWaveletTransform) -> J2kSingleDecompositionIdwtJob<'_> { + let band = |index: usize| J2kIdwtBand { + rect: J2kRect { + x0: 0, + y0: 0, + x1: self.sizes[index].0, + y1: self.sizes[index].1, + }, + coefficients: &self.coefficients[index], + }; + J2kSingleDecompositionIdwtJob { + rect: self.rect, + transform, + ll: band(0), + hl: band(1), + lh: band(2), + hh: band(3), + } + } +} + +#[test] +fn host_slice_idwt_preserves_capacity_and_tail_contract() { + if !j2k_test_support::metal_runtime_gate(module_path!()) { + return; + } + let mut decoder = MetalIdwtDecoder::default(); + for (width, height, x0, y0) in [ + (1, 1, 0, 0), + (1, 7, 1, 0), + (9, 1, 0, 1), + (3, 2, 1, 1), + (9, 7, 0, 1), + (2, 3, 1, 0), + ] { + let fixture = Fixture::new(width, height, x0, y0); + let len = width as usize * height as usize; + for transform in [ + J2kWaveletTransform::Reversible53, + J2kWaveletTransform::Irreversible97, + ] { + for normalization in [ + J2kIdwtNormalization::Standard, + J2kIdwtNormalization::OpenJpegCodestream, + ] { + let mut expected = vec![0.0_f32; len]; + assert!(decoder + .decode_single_decomposition_idwt_with_normalization( + fixture.job(transform), + normalization, + &mut expected + ) + .unwrap()); + let mut oversized = vec![123.25_f32; len + 5]; + assert!(decoder + .decode_single_decomposition_idwt_with_normalization( + fixture.job(transform), + normalization, + &mut oversized + ) + .unwrap()); + for (actual, expected) in oversized[..len].iter().zip(&expected) { + assert_eq!(actual.to_bits(), expected.to_bits()); + } + assert!(oversized[len..] + .iter() + .all(|v| v.to_bits() == 123.25_f32.to_bits())); + let mut undersized = vec![123.25_f32; len - 1]; + reset_idwt_host_transfer_counters_for_test(); + assert!(decoder + .decode_single_decomposition_idwt_with_normalization( + fixture.job(transform), + normalization, + &mut undersized + ) + .is_err()); + assert!(undersized + .iter() + .all(|v| v.to_bits() == 123.25_f32.to_bits())); + assert_eq!(idwt_host_transfer_counters_for_test(), (0, 0, 0)); + } + } + } +} + +#[test] +fn host_slice_idwt_output_transfer_accounting() { + if !j2k_test_support::metal_runtime_gate(module_path!()) { + return; + } + let fixture = Fixture::new(9, 7, 1, 1); + let mut decoder = MetalIdwtDecoder::default(); + for transform in [ + J2kWaveletTransform::Reversible53, + J2kWaveletTransform::Irreversible97, + ] { + let mut output = vec![5.0_f32; 9 * 7 + 4]; + reset_idwt_host_transfer_counters_for_test(); + assert!(decoder + .decode_single_decomposition_idwt(fixture.job(transform), &mut output) + .unwrap()); + assert_eq!( + idwt_host_transfer_counters_for_test(), + ( + output.len() * size_of::(), + 1, + output.len() * size_of::() + ) + ); + } +} + +#[test] +fn empty_host_slice_job_is_not_dispatched() { + let fixture = Fixture::new(0, 7, 0, 0); + let mut decoder = MetalIdwtDecoder::default(); + for transform in [ + J2kWaveletTransform::Reversible53, + J2kWaveletTransform::Irreversible97, + ] { + let mut output = [99.0_f32; 3]; + reset_idwt_host_transfer_counters_for_test(); + assert!(!decoder + .decode_single_decomposition_idwt(fixture.job(transform), &mut output) + .unwrap()); + assert_eq!(output.map(f32::to_bits), [99.0_f32.to_bits(); 3]); + assert_eq!(idwt_host_transfer_counters_for_test(), (0, 0, 0)); + } +} + +#[test] +fn oversized_host_slice_geometry_rejects_before_transfers() { + if !j2k_test_support::metal_runtime_gate(module_path!()) { + return; + } + let mut fixture = Fixture::new(1, 1, 0, 0); + fixture.rect.x1 = u32::MAX; + fixture.rect.y1 = u32::MAX; + let mut decoder = MetalIdwtDecoder::default(); + for transform in [ + J2kWaveletTransform::Reversible53, + J2kWaveletTransform::Irreversible97, + ] { + let mut output = [99.0_f32; 1]; + reset_idwt_host_transfer_counters_for_test(); + assert!(decoder + .decode_single_decomposition_idwt(fixture.job(transform), &mut output) + .is_err()); + assert_eq!(output.map(f32::to_bits), [99.0_f32.to_bits()]); + assert_eq!(idwt_host_transfer_counters_for_test(), (0, 0, 0)); + } +} diff --git a/xtask/src/metal.rs b/xtask/src/metal.rs index ee823fff..c6653388 100644 --- a/xtask/src/metal.rs +++ b/xtask/src/metal.rs @@ -59,6 +59,7 @@ const METAL_OPTIONAL_IGNORED_TESTS: &[&str] = &[ "idwt::tests::metal_irreversible_idwt_gpu_capture", "engine::runtime::resource_profile_tests::benchmark_cold_and_repeated_session_kernel_initialization", "idwt::tests::metal_host_slice_idwt_decode_perf", + "engine::direct_buffers::performance::metal_checked_buffer_copy_into_perf", "engine::decode_dispatch::idwt::irreversible::performance::metal_irreversible97_stage_gpu_timing", ]; diff --git a/xtask/src/metal/tests.rs b/xtask/src/metal/tests.rs index 940f74f9..d8944222 100644 --- a/xtask/src/metal/tests.rs +++ b/xtask/src/metal/tests.rs @@ -180,8 +180,10 @@ fn ignored_inventory_is_unique_and_has_expected_size() { .copied() .collect::>(); assert_eq!(required.len(), 21); - assert_eq!(optional.len(), 5); + assert_eq!(optional.len(), 6); assert!(optional.contains("idwt::tests::metal_host_slice_idwt_decode_perf")); + assert!(optional + .contains("engine::direct_buffers::performance::metal_checked_buffer_copy_into_perf")); assert!(optional.contains("engine::decode_dispatch::idwt::irreversible::performance::metal_irreversible97_stage_gpu_timing")); assert!( optional.contains("decoder::tests::sampled::local_sampled_color_batch_characterization")