Skip to main content

j2k_transcode/
dct53_2d.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Constrained 2D DCT to 5/3 wavelet experiments.
4//!
5//! The direct float path projects an 8x8 DCT block into one separable
6//! single-level 5/3 result without first storing the 8x8 spatial samples. The
7//! reference path materializes samples to keep the oracle easy to audit.
8
9use j2k_codec_math::dwt::{
10    linearized_dwt53_row, Dwt53Band, DWT53_MAX_HIGH_LINEAR_TAPS, DWT53_MAX_LINEAR_TAPS,
11};
12
13use crate::allocation::{
14    checked_add_allocation_bytes, checked_allocation_bytes, checked_allocation_len,
15    checked_capacity_bytes, try_vec_filled, try_vec_reserve_len, try_vec_resize_with,
16    try_vec_with_capacity, TranscodeAllocationError,
17};
18use crate::dct_grid::{high_len, idct8_basis, low_len, validate_dct_block_grid};
19use crate::{DctTransformError, Dwt53TwoDimensional};
20
21#[cfg(test)]
22impl Dwt53TwoDimensional<f64> {
23    /// Maximum absolute coefficient difference across matching bands.
24    #[must_use]
25    pub(crate) fn max_abs_diff(&self, other: &Self) -> f64 {
26        assert_eq!(self.low_width, other.low_width);
27        assert_eq!(self.low_height, other.low_height);
28        assert_eq!(self.high_width, other.high_width);
29        assert_eq!(self.high_height, other.high_height);
30
31        self.ll
32            .iter()
33            .zip(other.ll.iter())
34            .chain(self.hl.iter().zip(other.hl.iter()))
35            .chain(self.lh.iter().zip(other.lh.iter()))
36            .chain(self.hh.iter().zip(other.hh.iter()))
37            .map(|(actual, expected)| (actual - expected).abs())
38            .fold(0.0, f64::max)
39    }
40}
41
42/// Scratch storage for repeated DCT-grid to 5/3 projection calls.
43///
44/// Reuse one value per worker when transforming many components or tiles with
45/// matching geometry. The scratch caches linearized 5/3 weight rows; it does
46/// not store spatial samples.
47#[derive(Debug, Default)]
48pub(crate) struct Dct53GridScratch {
49    geometry: Option<(usize, usize)>,
50    x_weights: Dwt53WeightRows,
51    y_weights: Dwt53WeightRows,
52}
53
54impl Dct53GridScratch {
55    pub(crate) fn retained_bytes(&self) -> Result<usize, TranscodeAllocationError> {
56        checked_add_allocation_bytes(
57            self.x_weights.retained_bytes()?,
58            self.y_weights.retained_bytes()?,
59        )
60    }
61
62    #[cfg(test)]
63    fn weight_row_capacity(&self) -> usize {
64        self.x_weights.weight_capacity() + self.y_weights.weight_capacity()
65    }
66}
67
68/// Map an adjacent 8x8 DCT block grid directly into a linearized one-level 2D
69/// 5/3 result for the logical component dimensions.
70///
71/// Padded JPEG edge samples outside `width x height` are ignored.
72pub fn dct8x8_blocks_to_dwt53_float_linear(
73    blocks: &[[[f64; 8]; 8]],
74    block_cols: usize,
75    block_rows: usize,
76    width: usize,
77    height: usize,
78) -> Result<Dwt53TwoDimensional<f64>, DctTransformError> {
79    let mut scratch = Dct53GridScratch::default();
80    dct8x8_blocks_to_dwt53_float_linear_with_scratch(
81        blocks,
82        block_cols,
83        block_rows,
84        width,
85        height,
86        &mut scratch,
87    )
88}
89
90/// Map an adjacent 8x8 DCT block grid directly into a linearized one-level 2D
91/// 5/3 result using caller-owned scratch for reusable weight rows.
92pub(crate) fn dct8x8_blocks_to_dwt53_float_linear_with_scratch(
93    blocks: &[[[f64; 8]; 8]],
94    block_cols: usize,
95    block_rows: usize,
96    width: usize,
97    height: usize,
98    scratch: &mut Dct53GridScratch,
99) -> Result<Dwt53TwoDimensional<f64>, DctTransformError> {
100    validate_grid(blocks.len(), block_cols, block_rows, width, height)?;
101    validate_direct_workspace(width, height)?;
102
103    if scratch.geometry != Some((width, height)) {
104        *scratch = Dct53GridScratch::default();
105        scratch.geometry = Some((width, height));
106    }
107
108    let low_width = low_len(width);
109    let low_height = low_len(height);
110    let high_width = high_len(width);
111    let high_height = high_len(height);
112    scratch.x_weights.ensure_sample_len(width)?;
113    scratch.y_weights.ensure_sample_len(height)?;
114    let x_weights = &scratch.x_weights;
115    let y_weights = &scratch.y_weights;
116
117    let mut ll = try_vec_with_capacity(checked_allocation_len::<f64>(low_width, low_height)?)?;
118    let mut hl = try_vec_with_capacity(checked_allocation_len::<f64>(high_width, low_height)?)?;
119    let mut lh = try_vec_with_capacity(checked_allocation_len::<f64>(low_width, high_height)?)?;
120    let mut hh = try_vec_with_capacity(checked_allocation_len::<f64>(high_width, high_height)?)?;
121
122    for y in 0..low_height {
123        for x in 0..low_width {
124            ll.push(project_dct_grid(
125                blocks,
126                block_cols,
127                &y_weights.low[y].taps,
128                &x_weights.low[x].taps,
129            ));
130        }
131        for x in 0..high_width {
132            hl.push(project_dct_grid(
133                blocks,
134                block_cols,
135                &y_weights.low[y].taps,
136                &x_weights.high[x].taps,
137            ));
138        }
139    }
140
141    for y in 0..high_height {
142        for x in 0..low_width {
143            lh.push(project_dct_grid(
144                blocks,
145                block_cols,
146                &y_weights.high[y].taps,
147                &x_weights.low[x].taps,
148            ));
149        }
150        for x in 0..high_width {
151            hh.push(project_dct_grid(
152                blocks,
153                block_cols,
154                &y_weights.high[y].taps,
155                &x_weights.high[x].taps,
156            ));
157        }
158    }
159
160    Ok(Dwt53TwoDimensional {
161        ll,
162        hl,
163        lh,
164        hh,
165        low_width,
166        low_height,
167        high_width,
168        high_height,
169    })
170}
171
172/// Reference path for a DCT block grid:
173/// DCT coefficients -> float IDCT samples -> separable linearized 5/3.
174pub fn dct8x8_blocks_then_dwt53_float(
175    blocks: &[[[f64; 8]; 8]],
176    block_cols: usize,
177    block_rows: usize,
178    width: usize,
179    height: usize,
180) -> Result<Dwt53TwoDimensional<f64>, DctTransformError> {
181    validate_grid(blocks.len(), block_cols, block_rows, width, height)?;
182    let sample_count = checked_allocation_len::<f64>(width, height)?;
183    validate_reference_workspace(sample_count, height)?;
184
185    let mut samples = try_vec_with_capacity(sample_count)?;
186    for y in 0..height {
187        let block_y = y / 8;
188        let local_y = y % 8;
189        for x in 0..width {
190            let block_x = x / 8;
191            let local_x = x % 8;
192            let block = &blocks[block_y * block_cols + block_x];
193            samples.push(idct8x8_sample(block, local_x, local_y));
194        }
195    }
196
197    linearized_53_2d_from_plane(&samples, width, height)
198}
199
200fn project_dct_grid(
201    blocks: &[[[f64; 8]; 8]],
202    block_cols: usize,
203    y_weights: &[SparseWeightTap],
204    x_weights: &[SparseWeightTap],
205) -> f64 {
206    let mut output = 0.0;
207
208    for &SparseWeightTap {
209        sample_idx: sample_y,
210        weight: y_weight,
211    } in y_weights
212    {
213        let block_y = sample_y / 8;
214        let local_y = sample_y % 8;
215
216        for &SparseWeightTap {
217            sample_idx: sample_x,
218            weight: x_weight,
219        } in x_weights
220        {
221            let block_x = sample_x / 8;
222            let local_x = sample_x % 8;
223            let block = &blocks[block_y * block_cols + block_x];
224            let sample_weight = y_weight * x_weight;
225
226            for (freq_y, coefficient_row) in block.iter().enumerate() {
227                let y_basis = idct8_basis(local_y, freq_y);
228                for (freq_x, coefficient) in coefficient_row.iter().copied().enumerate() {
229                    output += sample_weight * y_basis * idct8_basis(local_x, freq_x) * coefficient;
230                }
231            }
232        }
233    }
234
235    output
236}
237
238fn idct8x8_sample(block: &[[f64; 8]; 8], x: usize, y: usize) -> f64 {
239    let mut sample = 0.0;
240    for (freq_y, row) in block.iter().enumerate() {
241        let y_basis = idct8_basis(y, freq_y);
242        for (freq_x, coefficient) in row.iter().copied().enumerate() {
243            sample += coefficient * y_basis * idct8_basis(x, freq_x);
244        }
245    }
246    sample
247}
248
249pub(crate) fn linearized_53_2d_from_plane(
250    samples: &[f64],
251    width: usize,
252    height: usize,
253) -> Result<Dwt53TwoDimensional<f64>, DctTransformError> {
254    if width == 0 || height == 0 {
255        return Err(DctTransformError::InvalidSamplePlaneDimensions { width, height });
256    }
257    let sample_count = checked_allocation_len::<f64>(width, height)?;
258    if samples.len() != sample_count {
259        return Err(DctTransformError::SamplePlaneLengthMismatch {
260            sample_count: samples.len(),
261            width,
262            height,
263        });
264    }
265    validate_plane_workspace(sample_count, height)?;
266
267    let low_width = low_len(width);
268    let low_height = low_len(height);
269    let high_width = high_len(width);
270    let high_height = high_len(height);
271
272    let mut row_low = try_vec_with_capacity(checked_allocation_len::<f64>(height, low_width)?)?;
273    let mut row_high = try_vec_with_capacity(checked_allocation_len::<f64>(height, high_width)?)?;
274    {
275        let mut transformed = Dwt53OneDimensional {
276            low: try_vec_with_capacity(low_width)?,
277            high: try_vec_with_capacity(high_width)?,
278        };
279        for row in samples.chunks_exact(width) {
280            linearized_53_into(row, &mut transformed);
281            row_low.extend_from_slice(&transformed.low);
282            row_high.extend_from_slice(&transformed.high);
283        }
284    }
285
286    let mut ll = try_vec_filled(checked_allocation_len::<f64>(low_width, low_height)?, 0.0)?;
287    let mut lh = try_vec_filled(checked_allocation_len::<f64>(low_width, high_height)?, 0.0)?;
288    let mut hl = try_vec_filled(checked_allocation_len::<f64>(high_width, low_height)?, 0.0)?;
289    let mut hh = try_vec_filled(checked_allocation_len::<f64>(high_width, high_height)?, 0.0)?;
290    let mut column = try_vec_with_capacity(height)?;
291    let mut transformed = Dwt53OneDimensional {
292        low: try_vec_with_capacity(low_height)?,
293        high: try_vec_with_capacity(high_height)?,
294    };
295    for x in 0..low_width {
296        fill_column(&row_low, low_width, x, height, &mut column);
297        linearized_53_into(&column, &mut transformed);
298        store_column(&transformed.low, low_width, x, &mut ll);
299        store_column(&transformed.high, low_width, x, &mut lh);
300    }
301
302    for x in 0..high_width {
303        fill_column(&row_high, high_width, x, height, &mut column);
304        linearized_53_into(&column, &mut transformed);
305        store_column(&transformed.low, high_width, x, &mut hl);
306        store_column(&transformed.high, high_width, x, &mut hh);
307    }
308
309    Ok(Dwt53TwoDimensional {
310        ll,
311        hl,
312        lh,
313        hh,
314        low_width,
315        low_height,
316        high_width,
317        high_height,
318    })
319}
320
321fn fill_column(rows: &[f64], stride: usize, x: usize, height: usize, column: &mut Vec<f64>) {
322    column.clear();
323    for y in 0..height {
324        column.push(rows[y * stride + x]);
325    }
326}
327
328fn store_column(column: &[f64], stride: usize, x: usize, band: &mut [f64]) {
329    for (y, value) in column.iter().copied().enumerate() {
330        band[y * stride + x] = value;
331    }
332}
333
334fn linearized_53_into(samples: &[f64], output: &mut Dwt53OneDimensional) {
335    output.high.clear();
336    for odd_idx in (1..samples.len()).step_by(2) {
337        let left = samples[odd_idx - 1];
338        let right = samples.get(odd_idx + 1).copied().unwrap_or(left);
339        output.high.push(samples[odd_idx] - ((left + right) * 0.5));
340    }
341
342    output.low.clear();
343    for even_idx in (0..samples.len()).step_by(2) {
344        let current = samples[even_idx];
345        let even_output_idx = even_idx / 2;
346        let left_high = even_output_idx
347            .checked_sub(1)
348            .and_then(|idx| output.high.get(idx));
349        let right_high = output.high.get(even_output_idx);
350        let update = match (left_high, right_high) {
351            (Some(left), Some(right)) => (*left + *right) * 0.25,
352            (None, Some(right)) => *right * 0.5,
353            (Some(left), None) => *left * 0.5,
354            (None, None) => 0.0,
355        };
356        output.low.push(current + update);
357    }
358}
359
360fn validate_grid(
361    block_count: usize,
362    block_cols: usize,
363    block_rows: usize,
364    width: usize,
365    height: usize,
366) -> Result<(), DctTransformError> {
367    validate_dct_block_grid(block_count, block_cols, block_rows, width, height)?;
368    Ok(())
369}
370
371fn validate_direct_workspace(width: usize, height: usize) -> Result<(), DctTransformError> {
372    let sample_count = checked_allocation_len::<f64>(width, height)?;
373    let x_weight_bytes = weight_workspace_bytes(width)?;
374    let y_weight_bytes = weight_workspace_bytes(height)?;
375    let retained_weight_bytes = checked_add_allocation_bytes(x_weight_bytes, y_weight_bytes)?;
376
377    let output_bytes = checked_allocation_bytes::<f64>(sample_count)?;
378    checked_add_allocation_bytes(retained_weight_bytes, output_bytes)?;
379    Ok(())
380}
381
382fn validate_reference_workspace(
383    sample_count: usize,
384    height: usize,
385) -> Result<(), DctTransformError> {
386    let sample_bytes = checked_allocation_bytes::<f64>(sample_count)?;
387    checked_add_allocation_bytes(sample_bytes, plane_workspace_bytes(sample_count, height)?)?;
388    Ok(())
389}
390
391fn validate_plane_workspace(sample_count: usize, height: usize) -> Result<(), DctTransformError> {
392    plane_workspace_bytes(sample_count, height)?;
393    Ok(())
394}
395
396fn plane_workspace_bytes(sample_count: usize, height: usize) -> Result<usize, DctTransformError> {
397    let row_and_output_bytes = allocation_product_bytes::<f64>(sample_count, 2)?;
398    let column_and_split_bytes = allocation_product_bytes::<f64>(height, 2)?;
399    Ok(checked_add_allocation_bytes(
400        row_and_output_bytes,
401        column_and_split_bytes,
402    )?)
403}
404
405fn weight_workspace_bytes(sample_len: usize) -> Result<usize, DctTransformError> {
406    let row_bytes = checked_allocation_bytes::<SparseWeightRow>(sample_len)?;
407    let low_tap_bytes =
408        allocation_product_bytes::<SparseWeightTap>(low_len(sample_len), DWT53_MAX_LINEAR_TAPS)?;
409    let high_tap_bytes = allocation_product_bytes::<SparseWeightTap>(
410        high_len(sample_len),
411        DWT53_MAX_HIGH_LINEAR_TAPS,
412    )?;
413    let tap_bytes = checked_add_allocation_bytes(low_tap_bytes, high_tap_bytes)?;
414    Ok(checked_add_allocation_bytes(row_bytes, tap_bytes)?)
415}
416
417fn allocation_product_bytes<T>(left: usize, right: usize) -> Result<usize, DctTransformError> {
418    let element_count = checked_allocation_len::<T>(left, right)?;
419    Ok(checked_allocation_bytes::<T>(element_count)?)
420}
421
422#[derive(Debug, Default)]
423struct Dwt53WeightRows {
424    sample_len: Option<usize>,
425    low: Vec<SparseWeightRow>,
426    high: Vec<SparseWeightRow>,
427}
428
429impl Dwt53WeightRows {
430    fn retained_bytes(&self) -> Result<usize, TranscodeAllocationError> {
431        let mut total = checked_add_allocation_bytes(
432            checked_capacity_bytes::<SparseWeightRow>(self.low.capacity())?,
433            checked_capacity_bytes::<SparseWeightRow>(self.high.capacity())?,
434        )?;
435        for row in self.low.iter().chain(&self.high) {
436            total = checked_add_allocation_bytes(
437                total,
438                checked_capacity_bytes::<SparseWeightTap>(row.taps.capacity())?,
439            )?;
440        }
441        Ok(total)
442    }
443
444    fn ensure_sample_len(&mut self, sample_len: usize) -> Result<(), DctTransformError> {
445        if self.sample_len == Some(sample_len) {
446            return Ok(());
447        }
448
449        *self = Self::default();
450        resize_weight_rows(&mut self.low, low_len(sample_len), DWT53_MAX_LINEAR_TAPS)?;
451        resize_weight_rows(
452            &mut self.high,
453            high_len(sample_len),
454            DWT53_MAX_HIGH_LINEAR_TAPS,
455        )?;
456        write_symbolic_weight_rows(&mut self.low, sample_len, Dwt53Band::Low)?;
457        write_symbolic_weight_rows(&mut self.high, sample_len, Dwt53Band::High)?;
458
459        self.sample_len = Some(sample_len);
460        Ok(())
461    }
462
463    #[cfg(test)]
464    fn weight_capacity(&self) -> usize {
465        self.low
466            .iter()
467            .map(|row| row.taps.capacity())
468            .sum::<usize>()
469            + self
470                .high
471                .iter()
472                .map(|row| row.taps.capacity())
473                .sum::<usize>()
474    }
475}
476
477fn write_symbolic_weight_rows(
478    rows: &mut [SparseWeightRow],
479    sample_len: usize,
480    band: Dwt53Band,
481) -> Result<(), DctTransformError> {
482    for (output_index, row) in rows.iter_mut().enumerate() {
483        let symbolic = linearized_dwt53_row(sample_len, band, output_index).ok_or(
484            DctTransformError::SymbolicWeightIndexOutOfRange {
485                sample_len,
486                output_index,
487                high_pass: matches!(band, Dwt53Band::High),
488            },
489        )?;
490        for tap in symbolic.taps() {
491            push_weight_tap(
492                row,
493                SparseWeightTap {
494                    sample_idx: tap.sample_index(),
495                    weight: tap.weight(),
496                },
497            )?;
498        }
499    }
500    Ok(())
501}
502
503fn resize_weight_rows(
504    rows: &mut Vec<SparseWeightRow>,
505    row_count: usize,
506    max_taps: usize,
507) -> Result<(), DctTransformError> {
508    try_vec_resize_with(rows, row_count, SparseWeightRow::default)?;
509    for row in rows.iter_mut().take(row_count) {
510        row.taps.clear();
511        try_vec_reserve_len(&mut row.taps, max_taps)?;
512    }
513    rows.truncate(row_count);
514    Ok(())
515}
516
517fn push_weight_tap(
518    row: &mut SparseWeightRow,
519    tap: SparseWeightTap,
520) -> Result<(), DctTransformError> {
521    let required_len =
522        row.taps
523            .len()
524            .checked_add(1)
525            .ok_or(DctTransformError::MemoryCapExceeded {
526                requested: usize::MAX,
527                cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
528            })?;
529    try_vec_reserve_len(&mut row.taps, required_len)?;
530    row.taps.push(tap);
531    Ok(())
532}
533
534#[derive(Debug, Default)]
535struct SparseWeightRow {
536    taps: Vec<SparseWeightTap>,
537}
538
539#[derive(Debug, Clone, Copy)]
540struct SparseWeightTap {
541    sample_idx: usize,
542    weight: f64,
543}
544
545struct Dwt53OneDimensional {
546    low: Vec<f64>,
547    high: Vec<f64>,
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553
554    #[test]
555    fn reference_workspace_rejects_aggregate_before_any_single_vector_hits_cap() {
556        let cap = j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES;
557        let sample_count = cap / core::mem::size_of::<f64>() / 3 + 1;
558        assert!(checked_allocation_bytes::<f64>(sample_count).is_ok());
559        assert!(matches!(
560            validate_reference_workspace(sample_count, 1),
561            Err(DctTransformError::MemoryCapExceeded { requested, cap: limit })
562                if requested > limit && limit == cap
563        ));
564    }
565
566    #[test]
567    fn dct8x8_grid_scratch_reuses_weight_rows_for_same_geometry() {
568        let blocks = synthetic_grid_blocks(2, 2);
569        let mut scratch = Dct53GridScratch::default();
570
571        let direct =
572            dct8x8_blocks_to_dwt53_float_linear_with_scratch(&blocks, 2, 2, 13, 11, &mut scratch)
573                .expect("valid DCT grid");
574        let stateless =
575            dct8x8_blocks_to_dwt53_float_linear(&blocks, 2, 2, 13, 11).expect("valid DCT grid");
576        let capacity_after_first = scratch.weight_row_capacity();
577
578        let repeated =
579            dct8x8_blocks_to_dwt53_float_linear_with_scratch(&blocks, 2, 2, 13, 11, &mut scratch)
580                .expect("valid DCT grid");
581
582        assert!(capacity_after_first > 0);
583        assert_eq!(scratch.weight_row_capacity(), capacity_after_first);
584        assert!(direct.max_abs_diff(&stateless) <= 1.0e-9);
585        assert!(repeated.max_abs_diff(&stateless) <= 1.0e-9);
586    }
587
588    #[test]
589    fn dct8x8_grid_scratch_uses_sparse_weight_rows_for_wsi_tile() {
590        let dim = 224_usize;
591        let block_cols = dim / 8;
592        let block_rows = dim / 8;
593        let blocks = vec![[[0.0; 8]; 8]; block_cols * block_rows];
594        let mut scratch = Dct53GridScratch::default();
595
596        dct8x8_blocks_to_dwt53_float_linear_with_scratch(
597            &blocks,
598            block_cols,
599            block_rows,
600            dim,
601            dim,
602            &mut scratch,
603        )
604        .expect("valid DCT grid");
605
606        assert!(
607            scratch.weight_row_capacity() <= dim * 10,
608            "5/3 grid weights should stay sparse at WSI tile sizes, got capacity {}",
609            scratch.weight_row_capacity()
610        );
611    }
612
613    #[expect(
614        clippy::cast_precision_loss,
615        reason = "small deterministic test-grid indices are exactly representable in f64"
616    )]
617    fn synthetic_grid_blocks(block_cols: usize, block_rows: usize) -> Vec<[[f64; 8]; 8]> {
618        let mut blocks = Vec::with_capacity(block_cols * block_rows);
619        for block_y in 0..block_rows {
620            for block_x in 0..block_cols {
621                let mut block = [[0.0; 8]; 8];
622                block[0][0] = 192.0 + (block_x * 17 + block_y * 23) as f64;
623                block[0][1] = -31.0 + block_x as f64;
624                block[1][0] = 27.0 - block_y as f64;
625                block[2][3] = 9.0;
626                block[7][7] = -6.0;
627                blocks.push(block);
628            }
629        }
630        blocks
631    }
632}