Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions crates/j2k-metal/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -190,15 +191,17 @@ 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,
reset_flattened_hybrid_cpu_decode_batches_for_test,
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,
Expand Down
17 changes: 9 additions & 8 deletions crates/j2k-metal/src/engine/decode_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 6 additions & 3 deletions crates/j2k-metal/src/engine/decode_dispatch/idwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)?;

Expand Down Expand Up @@ -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::<f32>(&decoded, output.len(), "IDWT output")?;
output.copy_from_slice(&decoded_host);
checked_buffer_copy_into(&decoded, 0, output, "IDWT output")?;
Ok(())
})
}
Expand Down
12 changes: 9 additions & 3 deletions crates/j2k-metal/src/engine/decode_dispatch/idwt/irreversible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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],
Expand Down Expand Up @@ -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(
Expand All @@ -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::<f32>(&decoded, output.len(), "IDWT output")?;
output.copy_from_slice(&decoded_host);
checked_buffer_copy_into(&decoded, 0, output, "IDWT output")?;
Ok(())
})
}
Expand Down
152 changes: 149 additions & 3 deletions crates/j2k-metal/src/engine/direct_buffers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,28 @@ pub(crate) fn checked_buffer_slice_at<T: GpuAbi>(
) -> Result<Vec<T>, 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::<T>(buffer, byte_offset, len) }
.map_err(|error| buffer_access_error(context, error))
let result = unsafe { checked_buffer_read_vec::<T>(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<T: GpuAbi>(
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")]
Expand Down Expand Up @@ -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]
Expand All @@ -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::<f32>(),
&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::<f32>(),
&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::<f32>(
&buffer,
2 * core::mem::size_of::<f32>(),
&mut [],
"empty end",
)?;
let empty_unaligned =
checked_buffer_copy_into::<f32>(&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::<f32>(
&buffer,
3 * core::mem::size_of::<f32>(),
&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::<f32>())?;
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");
}
}
62 changes: 62 additions & 0 deletions crates/j2k-metal/src/engine/direct_buffers/performance.rs
Original file line number Diff line number Diff line change
@@ -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<f32> {
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");
}
Loading
Loading