Skip to main content

j2k_transcode/
htj2k97_codeblock_oracle.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Shared HTJ2K 9/7 code-block option validation and quantization metadata.
4
5use crate::{Htj2k97CodeBlockAxis, Htj2k97CodeBlockOptions, Htj2k97CodeBlockOptionsError};
6use j2k::J2kSubBandType;
7use j2k_native::irreversible_quantization_step_for_subband;
8
9/// Deadzone quantization step size `Δ` for a subband.
10///
11/// `Δ = 2^(range_bits − exponent) · (1 + mantissa/2048)`, with
12/// `range_bits = bit_depth + {LL:0, HL:1, LH:1, HH:2}` and the shared
13/// `(exponent, mantissa)` derived by this module's quantizer.
14#[must_use]
15pub fn htj2k97_subband_delta(
16    options: Htj2k97CodeBlockOptions,
17    sub_band_type: J2kSubBandType,
18) -> f64 {
19    let log_gain = match sub_band_type {
20        J2kSubBandType::LowLow => 0,
21        J2kSubBandType::HighLow | J2kSubBandType::LowHigh => 1,
22        J2kSubBandType::HighHigh => 2,
23    };
24    let range_bits = i32::from(options.bit_depth) + log_gain;
25    let (exponent, mantissa) = htj2k97_step(options, sub_band_type);
26    pow2i_f64(range_bits - i32::from(exponent)) * (1.0 + f64::from(mantissa) / 2048.0)
27}
28
29/// Total declared bitplanes for every code-block in a subband.
30///
31/// `saturating(guard_bits + exponent - 1)`. The exponent is derived from the
32/// effective global plus per-subband quantization profile, so callers must pass
33/// the actual subband kind.
34#[must_use]
35pub fn htj2k97_subband_total_bitplanes(
36    options: Htj2k97CodeBlockOptions,
37    sub_band_type: J2kSubBandType,
38) -> u8 {
39    let (exponent, _) = htj2k97_step(options, sub_band_type);
40    options
41        .guard_bits
42        .saturating_add(exponent)
43        .saturating_sub(1)
44}
45
46/// Validate 9/7 code-block options against the numeric limits both GPU
47/// backends must agree on, returning the decoded `(cb_width, cb_height)`.
48///
49/// One shared implementation keeps Metal and CUDA from drifting: the same
50/// options must be accepted or rejected identically by every backend. Errors
51/// use a backend-neutral typed taxonomy so adapters never need to parse prose.
52///
53/// # Errors
54/// Rejects zero/oversized bit depths and guard bits, non-finite or
55/// non-positive quantization scales, code-block dimensions beyond the HTJ2K
56/// limits (sides ≤ 1024, area ≤ 4096), and subband deltas or total bitplane
57/// counts outside the supported range.
58pub fn validate_htj2k97_codeblock_options(
59    options: Htj2k97CodeBlockOptions,
60) -> Result<(usize, usize), Htj2k97CodeBlockOptionsError> {
61    if options.bit_depth == 0
62        || options.bit_depth > 30
63        || options.guard_bits > 30
64        || !options.irreversible_quantization_scale.is_finite()
65        || options.irreversible_quantization_scale <= 0.0
66    {
67        return Err(Htj2k97CodeBlockOptionsError::NumericOptionsOutOfRange);
68    }
69    let subband_scales = options.irreversible_quantization_subband_scales;
70    if [
71        subband_scales.low_low,
72        subband_scales.high_low,
73        subband_scales.low_high,
74        subband_scales.high_high,
75    ]
76    .iter()
77    .any(|scale| !scale.is_finite() || *scale <= 0.0)
78    {
79        return Err(Htj2k97CodeBlockOptionsError::QuantizationOptionsOutOfRange);
80    }
81
82    let cb_width =
83        checked_code_block_dim(options.code_block_width_exp, Htj2k97CodeBlockAxis::Width)?;
84    let cb_height =
85        checked_code_block_dim(options.code_block_height_exp, Htj2k97CodeBlockAxis::Height)?;
86    if cb_width > 1024
87        || cb_height > 1024
88        || cb_width
89            .checked_mul(cb_height)
90            .is_none_or(|area| area > 4096)
91    {
92        return Err(Htj2k97CodeBlockOptionsError::DimensionsExceedLimits {
93            width: cb_width,
94            height: cb_height,
95        });
96    }
97
98    for subband in [
99        J2kSubBandType::LowLow,
100        J2kSubBandType::HighLow,
101        J2kSubBandType::LowHigh,
102        J2kSubBandType::HighHigh,
103    ] {
104        let delta = htj2k97_subband_delta(options, subband);
105        if !delta.is_finite()
106            || delta <= 0.0
107            || htj2k97_subband_total_bitplanes(options, subband) > 30
108        {
109            return Err(Htj2k97CodeBlockOptionsError::QuantizationOptionsOutOfRange);
110        }
111    }
112
113    Ok((cb_width, cb_height))
114}
115
116fn checked_code_block_dim(
117    exp_minus_two: u8,
118    axis: Htj2k97CodeBlockAxis,
119) -> Result<usize, Htj2k97CodeBlockOptionsError> {
120    1usize.checked_shl(u32::from(exp_minus_two) + 2).ok_or(
121        Htj2k97CodeBlockOptionsError::DimensionExponentUnsupported {
122            axis,
123            exponent_minus_two: exp_minus_two,
124        },
125    )
126}
127
128/// Shared `(exponent, mantissa)` for the irreversible 9/7 quantizer.
129fn htj2k97_step(options: Htj2k97CodeBlockOptions, sub_band_type: J2kSubBandType) -> (u8, u16) {
130    let step = irreversible_quantization_step_for_subband(
131        options.bit_depth,
132        options.guard_bits,
133        options.irreversible_quantization_scale,
134        options.irreversible_quantization_subband_scales,
135        sub_band_type,
136    );
137    (step.exponent, step.mantissa)
138}
139
140fn pow2i_f64(exp: i32) -> f64 {
141    2.0f64.powi(exp)
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use j2k::IrreversibleQuantizationSubbandScales;
148
149    #[test]
150    fn shared_validator_accepts_standard_options_and_returns_dims() {
151        let options = Htj2k97CodeBlockOptions {
152            bit_depth: 8,
153            guard_bits: 2,
154            code_block_width_exp: 4,
155            code_block_height_exp: 4,
156            irreversible_quantization_scale: 1.0,
157            irreversible_quantization_subband_scales:
158                IrreversibleQuantizationSubbandScales::default(),
159        };
160        assert_eq!(validate_htj2k97_codeblock_options(options), Ok((64, 64)));
161    }
162
163    #[test]
164    fn shared_validator_rejects_out_of_spec_options_on_every_backend() {
165        let valid = Htj2k97CodeBlockOptions {
166            bit_depth: 8,
167            guard_bits: 2,
168            code_block_width_exp: 4,
169            code_block_height_exp: 4,
170            irreversible_quantization_scale: 1.0,
171            irreversible_quantization_subband_scales:
172                IrreversibleQuantizationSubbandScales::default(),
173        };
174
175        // Each case was accepted by the old Metal-only validator.
176        let oversized_bit_depth = Htj2k97CodeBlockOptions {
177            bit_depth: 31,
178            ..valid
179        };
180        let oversized_guard_bits = Htj2k97CodeBlockOptions {
181            guard_bits: 31,
182            ..valid
183        };
184        // 1024x1024: each side passes the per-side cap, area breaks the
185        // HTJ2K 4096 limit.
186        let oversized_area = Htj2k97CodeBlockOptions {
187            code_block_width_exp: 8,
188            code_block_height_exp: 8,
189            ..valid
190        };
191        for options in [oversized_bit_depth, oversized_guard_bits, oversized_area] {
192            assert!(
193                validate_htj2k97_codeblock_options(options).is_err(),
194                "options must be rejected: {options:?}"
195            );
196        }
197
198        // guard_bits == 0 stays accepted (the old Metal validator rejected it,
199        // CUDA and the native encoder accept it).
200        let zero_guard_bits = Htj2k97CodeBlockOptions {
201            guard_bits: 0,
202            ..valid
203        };
204        assert!(validate_htj2k97_codeblock_options(zero_guard_bits).is_ok());
205    }
206
207    #[test]
208    fn shared_validator_returns_each_typed_failure_variant() {
209        let valid = Htj2k97CodeBlockOptions {
210            bit_depth: 8,
211            guard_bits: 2,
212            code_block_width_exp: 4,
213            code_block_height_exp: 4,
214            irreversible_quantization_scale: 1.0,
215            irreversible_quantization_subband_scales:
216                IrreversibleQuantizationSubbandScales::default(),
217        };
218
219        let numeric = Htj2k97CodeBlockOptions {
220            bit_depth: 31,
221            ..valid
222        };
223        assert_eq!(
224            validate_htj2k97_codeblock_options(numeric),
225            Err(Htj2k97CodeBlockOptionsError::NumericOptionsOutOfRange)
226        );
227
228        let mut quantization = valid;
229        quantization
230            .irreversible_quantization_subband_scales
231            .low_low = 0.0;
232        assert_eq!(
233            validate_htj2k97_codeblock_options(quantization),
234            Err(Htj2k97CodeBlockOptionsError::QuantizationOptionsOutOfRange)
235        );
236
237        let exponent = Htj2k97CodeBlockOptions {
238            code_block_width_exp: u8::MAX,
239            ..valid
240        };
241        assert_eq!(
242            validate_htj2k97_codeblock_options(exponent),
243            Err(Htj2k97CodeBlockOptionsError::DimensionExponentUnsupported {
244                axis: Htj2k97CodeBlockAxis::Width,
245                exponent_minus_two: u8::MAX,
246            })
247        );
248
249        let dimensions = Htj2k97CodeBlockOptions {
250            code_block_width_exp: 8,
251            code_block_height_exp: 8,
252            ..valid
253        };
254        assert_eq!(
255            validate_htj2k97_codeblock_options(dimensions),
256            Err(Htj2k97CodeBlockOptionsError::DimensionsExceedLimits {
257                width: 1024,
258                height: 1024,
259            })
260        );
261    }
262
263    #[test]
264    fn oracle_subband_profile_changes_only_selected_delta_and_bitplanes() {
265        let mut options = Htj2k97CodeBlockOptions {
266            bit_depth: 8,
267            guard_bits: 2,
268            code_block_width_exp: 2,
269            code_block_height_exp: 2,
270            irreversible_quantization_scale: 1.9,
271            irreversible_quantization_subband_scales:
272                IrreversibleQuantizationSubbandScales::default(),
273        };
274        let high_low_delta = htj2k97_subband_delta(options, J2kSubBandType::HighLow);
275        let high_high_delta = htj2k97_subband_delta(options, J2kSubBandType::HighHigh);
276        let default_hh_bitplanes =
277            htj2k97_subband_total_bitplanes(options, J2kSubBandType::HighHigh);
278
279        options.irreversible_quantization_subband_scales.high_high = 1.5;
280
281        assert_eq!(
282            htj2k97_subband_delta(options, J2kSubBandType::HighLow).to_bits(),
283            high_low_delta.to_bits()
284        );
285        assert!(htj2k97_subband_delta(options, J2kSubBandType::HighHigh) > high_high_delta);
286        assert_ne!(
287            htj2k97_subband_total_bitplanes(options, J2kSubBandType::HighHigh),
288            default_hh_bitplanes
289        );
290    }
291}