Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ and stale roadmap entries have been removed from the public documentation set.
it out. This affected the CPU and Metal batches alike. On the 19 x 13
OpenJPH fixture in 11 x 7 tiles, 4,598 of the 17,290 half-scale regions
failed to prepare; all now match single-image decode.
- All-stage Metal lossy encodes seed code-blocks without coding passes with zero
in the zero-bitplane tag tree, as the CPU encoder does, so their packet headers
match the CPU's byte for byte. Earlier codestreams were valid but could differ
from the CPU's by a few header bytes.
- `j2k-tilecodec` LZW decode no longer reports valid TIFF strips and tiles
as truncated. weezl reads input ahead of decoding, so the call that
consumes the last byte can still hold codes up to the end code, often in
Expand Down
54 changes: 54 additions & 0 deletions crates/j2k-metal/src/encode/tests/kernels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,60 @@ fn metal_htj2k_quality_layers_use_native_candidates_and_match_scalar_output() {
assert!(accelerator.ht_code_block_dispatches() > 0);
}

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

// The left half is textured and the right half flat, so detail bands hold
// empty code-blocks beside coded ones in the same tag trees.
let (width, height) = (256_u32, 256_u32);
let pixels: Vec<u8> = (0..width * height * 3)
.map(|idx| {
let x = (idx / 3) % width;
let texture = if x < width / 2 {
(idx * 37 + idx / 7) & 0x07
} else {
0
};
u8::try_from((x / 4 + texture) & 0xFF).expect("masked pixel")
})
.collect();
let samples =
J2kLossySamples::new(&pixels, width, height, 3, 8, false).expect("valid RGB samples");
for (mode, qfactor) in [
(J2kBlockCodingMode::HighThroughput, Some(60)),
(J2kBlockCodingMode::HighThroughput, None),
(J2kBlockCodingMode::Classic, None),
] {
let options = J2kLossyEncodeOptions::default()
.with_block_coding_mode(mode)
.with_max_decomposition_levels(Some(2))
.with_qfactor(qfactor)
.with_validation(J2kEncodeValidation::External);
let cpu = j2k::encode_j2k_lossy(
samples,
&options
.clone()
.with_backend(EncodeBackendPreference::CpuOnly),
)
.expect("CPU lossy encode");
let mut accelerator = MetalEncodeStageAccelerator::default();
let metal = encode_j2k_lossy_with_accelerator(
samples,
&options.with_backend(EncodeBackendPreference::RequireDevice),
BackendKind::Metal,
&mut accelerator,
)
.expect("Metal all-stage lossy encode");

assert_eq!(metal.backend, BackendKind::Metal, "{mode:?} {qfactor:?}");
assert_eq!(metal.codestream, cpu.codestream, "{mode:?} {qfactor:?}");
}
}

#[cfg(target_os = "macos")]
#[test]
fn metal_htj2k_lossy_rgb_facade_reports_forward_ict_dispatch() {
Expand Down
14 changes: 10 additions & 4 deletions crates/j2k-metal/src/encode_bitstream_packetize.metal
Original file line number Diff line number Diff line change
Expand Up @@ -830,9 +830,12 @@ inline bool j2k_packet_prepare_tree(
}
for (uint idx = 0u; idx < block_count; ++idx) {
const J2kPacketBlock block = blocks[block_offset + idx];
// A block that is never included contributes zero to the zero-bitplane
// tree, as the CPU packetizer seeds it, so headers match byte for byte.
const bool included = block.num_coding_passes > 0u;
value[idx] = zero_bitplanes
? block.num_zero_bitplanes
: (block.num_coding_passes > 0u ? inclusion_layer : 0x7FFFFFFFu);
? (included ? block.num_zero_bitplanes : 0u)
: (included ? inclusion_layer : 0x7FFFFFFFu);
}
for (uint level = 1u; level < levels; ++level) {
const uint prev_w = level_widths[level - 1u];
Expand Down Expand Up @@ -910,9 +913,12 @@ inline bool j2k_packet_prepare_tree_resident_classic(
tier1_job_count,
block_offset + idx
);
// A block that is never included contributes zero to the zero-bitplane
// tree, as the CPU packetizer seeds it, so headers match byte for byte.
const bool included = block.num_coding_passes > 0u;
value[idx] = zero_bitplanes
? block.num_zero_bitplanes
: (block.num_coding_passes > 0u ? inclusion_layer : 0x7FFFFFFFu);
? (included ? block.num_zero_bitplanes : 0u)
: (included ? inclusion_layer : 0x7FFFFFFFu);
}
for (uint level = 1u; level < levels; ++level) {
const uint prev_w = level_widths[level - 1u];
Expand Down
Loading