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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ and stale roadmap entries have been removed from the public documentation set.
orchestration roots by pipeline ownership, with repository policies enforcing
dependency direction, source boundaries, unsafe inventory, clone ceilings,
benchmark registration, and generated routing evidence.
- Adds bounded HTJ2K lossy candidate generation with two consecutive HT sets,
exact cleanup/SigProp/MagRef byte boundaries, per-pass distortion scoring,
and dependency-aware slope allocation. CUDA and Metal can produce the exact
candidate sets on device; CUDA entropy writing remains serial within each
code block. Each selected HT set is emitted atomically in the layer of its
final selected pass so external decoders retain an unambiguous cleanup
boundary.
- Makes bounded classic JPEG 2000 and HTJ2K row decode through 24-bit component
precision parse the tile graph once per row operation and reuse it across
stripes while retaining the parsed metadata in the aggregate allocation
baseline. Higher-precision exact-integer rows keep their compatibility path.
- Promotes measured staged CUDA JPEG encoding and adaptive CUDA JPEG checkpoint
launch geometry. The staged encoder improves the representative 512 x 512
batch-8 cells by about 95%, while the checkpoint policy uses one-thread
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

**Docs & guides:** [Pure-Rust JPEG 2000 codec documentation](https://frames-sg.github.io/j2k/rust-jpeg2000-codec/)

**Release status:** `0.9.0` is published and security-supported. See the
**Release status:** `0.10.0` is published and security-supported. See the
[release notes](CHANGELOG.md), [release policy](docs/release.md), and
[security policy](SECURITY.md).

Expand Down Expand Up @@ -293,6 +293,11 @@ surfaces are available for supported paths; unsupported explicit CUDA requests
fail clearly. J2K-owned CUDA kernels are used for CUDA codec stages. NVIDIA
performance claims require recorded self-hosted benchmark output.

Lossy HTJ2K encoding can opt into the OpenHTJ2K-compatible visual Qfactor
profile with `J2kLossyEncodeOptions::with_qfactor(Some(quality))`, where
`quality` is `1..=100`. This profile is intentionally separate from byte,
bits-per-pixel, PSNR, quality-layer-target, and ROI controls.

## Public API and support policy

Stable APIs are `j2k`, `j2k-core` traits and value types, `j2k-jpeg`,
Expand All @@ -302,6 +307,10 @@ transcode crates, and backend encode-stage adapter SPI.
Codec contracts include `ImageDecode`, `decode_region_scaled_into`,
`decode_rows`, `TileBatchDecode`, `DeviceSurface`, `ScratchPool`, and
the concrete `J2kContext` and `j2k_jpeg::DecoderContext` types.
Bounded JPEG 2000/HTJ2K row decode through 24-bit component precision retains
one parsed tile graph for the operation and reuses it across stripes; stripe
output scratch remains bounded by `J2kRowDecodeOptions`. Higher-precision exact
integer output keeps the existing full-decode/crop compatibility path.
`BackendRequest::Auto` may return CPU output.
`BackendRequest::Metal` and `BackendRequest::Cuda` are strict and fail for
unsupported shapes.
Expand Down
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

| Version | Status |
| --- | --- |
| `0.9.0` | Latest published and security-supported release |
| `0.10.0` | Latest published and security-supported release |
| `0.9.0` | Previous published release line; security-supported |
| `0.8.1` | Previous published release line; security-supported |
| `0.8.0` | Previous published release line; security-supported |
| `0.7.5` | Previous published release line; security-supported, except for the documented `j2k-ml` CUDA and Metal packaging defect |
Expand Down
37 changes: 35 additions & 2 deletions crates/j2k-alloc-probe/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ use std::sync::{mpsc, OnceLock};
use j2k_alloc_probe::{assert_allocations, measure, Budget};
use j2k_native::{
decode_ht_code_block_scalar_with_workspace, encode,
encode_ht_code_block_scalar_with_passes_and_workspace,
encode_precomputed_htj2k_97_with_accelerator_and_max_host_bytes,
CpuOnlyJ2kEncodeStageAccelerator, DecodeSettings, DecoderContext, EncodeError, EncodeOptions,
HtCodeBlockDecodeJob, HtCodeBlockDecodeWorkspace, Image, J2kForwardDwt97Output,
PrecomputedHtj2k97Component, PrecomputedHtj2k97Image,
HtCodeBlockDecodeJob, HtCodeBlockDecodeWorkspace, HtCodeBlockEncodeWorkspace, Image,
J2kForwardDwt97Output, PrecomputedHtj2k97Component, PrecomputedHtj2k97Image,
};
use proptest::{
collection::vec,
Expand Down Expand Up @@ -51,6 +52,10 @@ fn main() {
"warmed_scalar_decode_workspace_reuses_without_allocating",
warmed_scalar_decode_workspace_reuses_without_allocating,
),
(
"warmed_ht_encode_workspace_avoids_fixed_reservoir_allocations",
warmed_ht_encode_workspace_avoids_fixed_reservoir_allocations,
),
(
"warmed_decoder_context_has_bounded_transients",
warmed_decoder_context_has_bounded_transients,
Expand Down Expand Up @@ -257,6 +262,34 @@ fn warmed_scalar_decode_workspace_reuses_without_allocating() {
);
}

fn warmed_ht_encode_workspace_avoids_fixed_reservoir_allocations() {
let coefficients = (0_i32..64 * 64)
.map(|index| if index % 13 == 0 { 0 } else { index & 255 })
.collect::<Vec<_>>();
let mut workspace = HtCodeBlockEncodeWorkspace::try_new().expect("HT encode workspace");

let (encoded, reused) = measure(|| {
encode_ht_code_block_scalar_with_passes_and_workspace(
&coefficients,
64,
64,
9,
1,
&mut workspace,
)
});
let encoded = encoded.expect("reused-workspace HT encode");
let (_, fresh) = measure(|| {
j2k_native::encode_ht_code_block_scalar_with_passes(&coefficients, 64, 64, 9, 1)
});

assert!(!encoded.data.is_empty());
assert!(
reused.allocation_calls() + 3 <= fresh.allocation_calls(),
"the reusable workspace must remove the MEL, VLC, and magnitude/sign reservoir allocations: reused={reused:?}, fresh={fresh:?}"
);
}

fn warmed_decoder_context_has_bounded_transients() {
let pool = probe_pool();
let pixels = (0_u8..64).collect::<Vec<_>>();
Expand Down
221 changes: 182 additions & 39 deletions crates/j2k-compare/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,195 @@ use std::{

fn main() {
println!("cargo:rustc-check-cfg=cfg(have_grok)");
println!("cargo:rustc-check-cfg=cfg(have_openhtj2k)");
println!("cargo:rustc-check-cfg=cfg(have_openjph)");
println!("cargo:rerun-if-env-changed=J2K_GROK_ROOT");
println!("cargo:rerun-if-env-changed=J2K_GROK_SOURCE");
println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");
println!("cargo:rerun-if-changed=src/grok_shim.c");
println!("cargo:rerun-if-env-changed=J2K_OPENHTJ2K_SOURCE_DIR");
println!("cargo:rerun-if-env-changed=J2K_OPENHTJ2K_LIB_DIR");
println!("cargo:rerun-if-changed=src/openhtj2k_shim.cpp");
println!("cargo:rerun-if-env-changed=J2K_OPENJPH_SOURCE_DIR");
println!("cargo:rerun-if-env-changed=J2K_OPENJPH_LIB_DIR");
println!("cargo:rerun-if-changed=src/openjph_shim.cpp");

if let Some(config) = grok_config() {
let staged_lib_dir = stage_grok_runtime(&config.lib_dir)
.unwrap_or_else(|err| panic!("failed to stage Grok runtime libraries: {err}"));
let grok_version = grok_version(&config);
println!("cargo:rustc-cfg=have_grok");
println!("cargo:rustc-env=J2K_GROK_VERSION={grok_version}");
println!(
"cargo:rustc-env=J2K_GROK_LIB_DIR={}",
config.lib_dir.display()
);
println!(
"cargo:rustc-link-search=native={}",
staged_lib_dir.display()
);
println!(
"cargo:rustc-link-search=native={}",
config.lib_dir.display()
);
println!("cargo:rustc-link-lib=dylib=grokj2k");
#[cfg(target_os = "macos")]
println!(
"cargo:rustc-link-arg=-Wl,-rpath,{}",
config.lib_dir.display()
);
#[cfg(target_os = "macos")]
println!(
"cargo:rustc-link-arg=-Wl,-rpath,{}",
staged_lib_dir.display()
);

cc::Build::new()
.file("src/grok_shim.c")
.include(config.source_include)
.include(config.build_include)
.warnings(true)
.extra_warnings(true)
.warnings_into_errors(true)
.flag_if_supported("-Wconversion")
.flag_if_supported("-Wsign-conversion")
.compile("j2k_grok_shim");
configure_grok(config);
}
if let Some(config) = openhtj2k_config() {
configure_openhtj2k(&config);
}
if let Some(config) = openjph_config() {
configure_openjph(&config);
}
}

fn configure_grok(config: GrokConfig) {
let staged_lib_dir = stage_grok_runtime(&config.lib_dir)
.unwrap_or_else(|err| panic!("failed to stage Grok runtime libraries: {err}"));
let grok_version = grok_version(&config);
println!("cargo:rustc-cfg=have_grok");
println!("cargo:rustc-env=J2K_GROK_VERSION={grok_version}");
println!(
"cargo:rustc-env=J2K_GROK_LIB_DIR={}",
config.lib_dir.display()
);
println!(
"cargo:rustc-link-search=native={}",
staged_lib_dir.display()
);
println!(
"cargo:rustc-link-search=native={}",
config.lib_dir.display()
);
println!("cargo:rustc-link-lib=dylib=grokj2k");
#[cfg(target_os = "macos")]
println!(
"cargo:rustc-link-arg=-Wl,-rpath,{}",
config.lib_dir.display()
);
#[cfg(target_os = "macos")]
println!(
"cargo:rustc-link-arg=-Wl,-rpath,{}",
staged_lib_dir.display()
);

cc::Build::new()
.file("src/grok_shim.c")
.include(config.source_include)
.include(config.build_include)
.warnings(true)
.extra_warnings(true)
.warnings_into_errors(true)
.flag_if_supported("-Wconversion")
.flag_if_supported("-Wsign-conversion")
.compile("j2k_grok_shim");
}

fn configure_openhtj2k(config: &StaticReferenceConfig) {
println!("cargo:rustc-cfg=have_openhtj2k");
println!(
"cargo:rustc-env=J2K_OPENHTJ2K_LIB_DIR={}",
config.lib_dir.display()
);
println!("cargo:rustc-env=J2K_OPENHTJ2K_VERSION={}", config.version);
println!(
"cargo:rustc-link-search=native={}",
config.lib_dir.display()
);
println!("cargo:rustc-link-lib=static=openhtj2k");

cc::Build::new()
.cpp(true)
.file("src/openhtj2k_shim.cpp")
.include(config.source_dir.join("source/core/interface"))
.include(config.source_dir.join("source/core/common"))
.warnings(true)
.extra_warnings(true)
.warnings_into_errors(true)
.flag_if_supported("-std=c++17")
.flag_if_supported("-Wconversion")
.flag_if_supported("-Wsign-conversion")
.compile("j2k_openhtj2k_shim");
}

fn configure_openjph(config: &StaticReferenceConfig) {
println!("cargo:rustc-cfg=have_openjph");
println!(
"cargo:rustc-env=J2K_OPENJPH_LIB_DIR={}",
config.lib_dir.display()
);
println!("cargo:rustc-env=J2K_OPENJPH_VERSION={}", config.version);
println!(
"cargo:rustc-link-search=native={}",
config.lib_dir.display()
);
println!("cargo:rustc-link-lib=static=openjph");

cc::Build::new()
.cpp(true)
.file("src/openjph_shim.cpp")
.include(config.source_dir.join("src/core"))
.warnings(true)
.extra_warnings(true)
.warnings_into_errors(true)
.flag_if_supported("-std=c++14")
.flag_if_supported("-Wconversion")
.flag_if_supported("-Wsign-conversion")
.compile("j2k_openjph_shim");
}

struct StaticReferenceConfig {
source_dir: PathBuf,
lib_dir: PathBuf,
version: String,
}

fn openjph_config() -> Option<StaticReferenceConfig> {
static_reference_config(
"J2K_OPENJPH_SOURCE_DIR",
"J2K_OPENJPH_LIB_DIR",
"build-reference/src/core",
"src/core/openjph/ojph_codestream.h",
&["libopenjph.a", "openjph.lib"],
None,
)
}

fn openhtj2k_config() -> Option<StaticReferenceConfig> {
static_reference_config(
"J2K_OPENHTJ2K_SOURCE_DIR",
"J2K_OPENHTJ2K_LIB_DIR",
"build-reference",
"source/core/interface/decoder.hpp",
&["libopenhtj2k.a", "openhtj2k.lib"],
Some("v"),
)
}

fn static_reference_config(
source_env: &str,
lib_env: &str,
default_lib_dir: &str,
required_header: &str,
library_names: &[&str],
version_prefix_to_strip: Option<&str>,
) -> Option<StaticReferenceConfig> {
let source_dir = PathBuf::from(std::env::var_os(source_env)?);
let lib_dir =
std::env::var_os(lib_env).map_or_else(|| source_dir.join(default_lib_dir), PathBuf::from);
if !source_dir.join(required_header).is_file()
|| !library_names
.iter()
.any(|name| lib_dir.join(name).is_file())
{
return None;
}
Some(StaticReferenceConfig {
version: exact_git_tag(&source_dir, version_prefix_to_strip),
source_dir,
lib_dir,
})
}

fn exact_git_tag(source_dir: &Path, prefix_to_strip: Option<&str>) -> String {
Command::new("git")
.args(["-C"])
.arg(source_dir)
.args(["describe", "--tags", "--exact-match", "HEAD"])
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|value| {
let value = value.trim();
prefix_to_strip
.map_or(value, |prefix| value.trim_start_matches(prefix))
.to_string()
})
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "unknown".to_string())
}

fn stage_grok_runtime(lib_dir: &Path) -> Result<PathBuf, String> {
Expand Down
Loading