1use 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 #[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#[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
68pub 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
90pub(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
172pub 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}