/src/grok/mercury/src/dwt/level_builder.rs
Line | Count | Source |
1 | | //! Direct construction of [`Synthesis`] engines from band dimensions — |
2 | | //! computes per-level synthesis geometry directly in Rust. |
3 | | //! |
4 | | //! Used by the decode pipeline ([`crate::decode::graph`]) and golden tests. |
5 | | |
6 | | use super::synthesis::{SamplePrec, StepSpec, Synthesis, SynthesisParams}; |
7 | | use super::{KERNEL_W5X3, KERNEL_W9X7, align_samples16, align_samples32}; |
8 | | use crate::decode::DecodeError; |
9 | | |
10 | | /// A band's position and size (JPEG 2000 canvas coordinates, `pos + size`). |
11 | | #[derive(Clone, Copy, Debug)] |
12 | | pub struct BandDims { |
13 | | pub x0: i32, |
14 | | pub y0: i32, |
15 | | pub w: i32, |
16 | | pub h: i32, |
17 | | } |
18 | | |
19 | | /// The five geometry inputs of one resolution level. |
20 | | #[derive(Clone, Copy, Debug)] |
21 | | pub struct LevelSpec { |
22 | | /// This level's output region (the resolution's dims). |
23 | | pub node: BandDims, |
24 | | pub ll: BandDims, |
25 | | pub hl: BandDims, |
26 | | pub lh: BandDims, |
27 | | } |
28 | | |
29 | | /// Compute `SynthesisParams` for a level with both transforms present |
30 | | /// (`vert_xform_exists && hor_xform_exists`). |
31 | | /// |
32 | | /// Band geometry comes from the codestream, so a corrupt one can break the |
33 | | /// band-splitting relations this relies on. Those are rejected as errors |
34 | | /// rather than asserted, so the host gets a decode failure and falls back. |
35 | 5.14k | pub fn draft_params( |
36 | 5.14k | spec: &LevelSpec, |
37 | 5.14k | sample_prec: SamplePrec, |
38 | 5.14k | reversible: bool, |
39 | 5.14k | num_steps: i32, |
40 | 5.14k | symmetric_extension: bool, |
41 | 5.14k | ) -> Result<SynthesisParams, DecodeError> { |
42 | 5.14k | let node = &spec.node; |
43 | 5.14k | let y_min_out = node.y0; |
44 | 5.14k | let y_max_out = node.y0 + node.h - 1; |
45 | 5.14k | let x_min_out = node.x0; |
46 | 5.14k | let x_max_out = node.x0 + node.w - 1; |
47 | 5.14k | let pull_offset = 0; |
48 | 5.14k | let x_min_pull = x_min_out - pull_offset; |
49 | 5.14k | let mut x_min_buf = x_min_pull; |
50 | | |
51 | 5.14k | let empty = node.w <= 0 || node.h <= 0; |
52 | 5.14k | if empty { |
53 | 36 | let mut p = bare_params(sample_prec, reversible); |
54 | 36 | p.empty = true; |
55 | 36 | p.unit_height = y_min_out == y_max_out; |
56 | 36 | p.unit_width = x_min_out == x_max_out; |
57 | 36 | return Ok(p); |
58 | 5.10k | } |
59 | | |
60 | | // --- vertical geometry --- |
61 | 5.10k | let mut unit_height = false; |
62 | 5.10k | let mut y_min_in = [0i32; 2]; |
63 | 5.10k | let mut y_max_in = [0i32; 2]; |
64 | 5.10k | y_min_in[0] = spec.ll.y0 << 1; |
65 | 5.10k | y_max_in[0] = (spec.ll.y0 + spec.ll.h - 1) << 1; |
66 | 5.10k | y_min_in[1] = (spec.lh.y0 << 1) + 1; |
67 | 5.10k | y_max_in[1] = ((spec.lh.y0 + spec.lh.h - 1) << 1) + 1; |
68 | 5.10k | if spec.lh.h <= 0 { |
69 | 12 | unit_height = true; |
70 | 12 | require_single_line(y_min_in[0], y_max_in[0], "lh", "height", "ll")?; |
71 | 12 | y_min_in[1] = y_min_in[0] + 1; |
72 | 12 | y_max_in[1] = y_min_in[1] - 2; |
73 | 5.09k | } |
74 | 5.10k | if spec.ll.h <= 0 { |
75 | 36 | unit_height = true; |
76 | 36 | require_single_line(y_min_in[1], y_max_in[1], "ll", "height", "lh")?; |
77 | 36 | y_min_in[0] = y_min_in[1] + 1; |
78 | 36 | y_max_in[0] = y_min_in[0] - 2; |
79 | 5.06k | } |
80 | | |
81 | | // --- horizontal geometry --- |
82 | 5.10k | let mut unit_width = false; |
83 | 5.10k | let mut min_in = [spec.ll.x0 << 1, (spec.hl.x0 << 1) + 1]; |
84 | 5.10k | let mut max_in = [ |
85 | 5.10k | (spec.ll.x0 + spec.ll.w - 1) << 1, |
86 | 5.10k | ((spec.hl.x0 + spec.hl.w - 1) << 1) + 1, |
87 | 5.10k | ]; |
88 | 5.10k | if spec.hl.w <= 0 { |
89 | 15 | unit_width = true; |
90 | 15 | require_single_line(min_in[0], max_in[0], "hl", "width", "ll")?; |
91 | 15 | min_in[1] = min_in[0] + 1; |
92 | 15 | max_in[1] = min_in[1] - 2; |
93 | 5.09k | } |
94 | 5.10k | if spec.ll.w <= 0 { |
95 | 0 | unit_width = true; |
96 | 0 | require_single_line(min_in[1], max_in[1], "ll", "width", "hl")?; |
97 | 0 | min_in[0] = min_in[1] + 1; |
98 | 0 | max_in[0] = min_in[0] - 2; |
99 | 5.10k | } |
100 | 5.10k | let x_min_in = min_in[0].min(min_in[1]); |
101 | 5.10k | let x_max_in = max_in[0].max(max_in[1]); |
102 | | |
103 | 5.10k | let alignment = match sample_prec { |
104 | 3.55k | SamplePrec::I16 => align_samples16(), |
105 | 1.54k | _ => align_samples32(), |
106 | | }; |
107 | 5.10k | while x_min_in < x_min_buf { |
108 | 0 | x_min_buf -= alignment; |
109 | 0 | } |
110 | | |
111 | 5.10k | let phase_off_in = [ |
112 | 5.10k | ((x_min_in + 1) >> 1) - ((x_min_buf + 1) >> 1), |
113 | 5.10k | (x_min_in >> 1) - (x_min_buf >> 1), |
114 | 5.10k | ]; |
115 | 5.10k | let phase_width_in = [ |
116 | 5.10k | 1 + (x_max_in >> 1) - ((x_min_in + 1) >> 1), |
117 | 5.10k | 1 + ((x_max_in - 1) >> 1) - (x_min_in >> 1), |
118 | 5.10k | ]; |
119 | 5.10k | let request_offset = [ |
120 | 5.10k | spec.ll.x0 - ((x_min_buf + 1) >> 1), |
121 | 5.10k | spec.hl.x0 - (x_min_buf >> 1), |
122 | 5.10k | ]; |
123 | 5.10k | let request_width = [spec.ll.w, spec.hl.w]; |
124 | 5.10k | let left_fill = [ |
125 | 5.10k | request_offset[0] - phase_off_in[0], |
126 | 5.10k | request_offset[1] - phase_off_in[1], |
127 | 5.10k | ]; |
128 | 5.10k | let right_fill = [ |
129 | 5.10k | (phase_off_in[0] + phase_width_in[0]) - (request_offset[0] + request_width[0]), |
130 | 5.10k | (phase_off_in[1] + phase_width_in[1]) - (request_offset[1] + request_width[1]), |
131 | 5.10k | ]; |
132 | 5.10k | if !left_fill |
133 | 5.10k | .iter() |
134 | 5.10k | .chain(right_fill.iter()) |
135 | 20.4k | .all(|&f| (0..256).contains(&f)) |
136 | | { |
137 | 0 | return Err(DecodeError::Logic(format!( |
138 | 0 | "level fill out of range: left {left_fill:?} right {right_fill:?}" |
139 | 0 | ))); |
140 | 5.10k | } |
141 | | |
142 | 5.10k | let phase_off_out = [ |
143 | 5.10k | ((x_min_out + 1) >> 1) - ((x_min_buf + 1) >> 1), |
144 | 5.10k | (x_min_out >> 1) - (x_min_buf >> 1), |
145 | 5.10k | ]; |
146 | 5.10k | let phase_width_out = [ |
147 | 5.10k | 1 + (x_max_out >> 1) - ((x_min_out + 1) >> 1), |
148 | 5.10k | 1 + ((x_max_out - 1) >> 1) - (x_min_out >> 1), |
149 | 5.10k | ]; |
150 | | |
151 | 5.10k | Ok(SynthesisParams { |
152 | 5.10k | y_min_out, |
153 | 5.10k | y_max_out, |
154 | 5.10k | x_min_out, |
155 | 5.10k | x_max_out, |
156 | 5.10k | x_min_pull, |
157 | 5.10k | x_min_buf, |
158 | 5.10k | y_min_in, |
159 | 5.10k | y_max_in, |
160 | 5.10k | x_min_in, |
161 | 5.10k | x_max_in, |
162 | 5.10k | phase_off_in, |
163 | 5.10k | phase_width_in, |
164 | 5.10k | request_offset, |
165 | 5.10k | request_width, |
166 | 5.10k | left_fill, |
167 | 5.10k | right_fill, |
168 | 5.10k | phase_off_out, |
169 | 5.10k | phase_width_out, |
170 | 5.10k | num_vert_steps: num_steps, |
171 | 5.10k | num_hor_steps: num_steps, |
172 | 5.10k | vert_symmetric_extension: symmetric_extension, |
173 | 5.10k | hor_symmetric_extension: symmetric_extension, |
174 | 5.10k | reversible, |
175 | 5.10k | vertical_first: false, |
176 | 5.10k | unit_height, |
177 | 5.10k | unit_width, |
178 | 5.10k | empty: false, |
179 | 5.10k | sample_prec, |
180 | 5.10k | }) |
181 | 5.14k | } |
182 | | |
183 | | /// A subband with no extent along one axis means the level is one sample across |
184 | | /// it, so the opposite subband has to be a single line. `empty` and `other` name |
185 | | /// the two subbands for the error message. |
186 | 63 | fn require_single_line( |
187 | 63 | min: i32, |
188 | 63 | max: i32, |
189 | 63 | empty: &str, |
190 | 63 | axis: &str, |
191 | 63 | other: &str, |
192 | 63 | ) -> Result<(), DecodeError> { |
193 | 63 | if min == max { |
194 | 63 | return Ok(()); |
195 | 0 | } |
196 | 0 | Err(DecodeError::Logic(format!( |
197 | 0 | "{empty} subband has no {axis} but {other} spans {min}..{max}" |
198 | 0 | ))) |
199 | 63 | } |
200 | | |
201 | 36 | fn bare_params(sample_prec: SamplePrec, reversible: bool) -> SynthesisParams { |
202 | 36 | SynthesisParams { |
203 | 36 | y_min_out: 0, |
204 | 36 | y_max_out: 0, |
205 | 36 | x_min_out: 0, |
206 | 36 | x_max_out: 0, |
207 | 36 | x_min_pull: 0, |
208 | 36 | x_min_buf: 0, |
209 | 36 | y_min_in: [0; 2], |
210 | 36 | y_max_in: [0; 2], |
211 | 36 | x_min_in: 0, |
212 | 36 | x_max_in: 0, |
213 | 36 | phase_off_in: [0; 2], |
214 | 36 | phase_width_in: [0; 2], |
215 | 36 | request_offset: [0; 2], |
216 | 36 | request_width: [0; 2], |
217 | 36 | left_fill: [0; 2], |
218 | 36 | right_fill: [0; 2], |
219 | 36 | phase_off_out: [0; 2], |
220 | 36 | phase_width_out: [0; 2], |
221 | 36 | num_vert_steps: 0, |
222 | 36 | num_hor_steps: 0, |
223 | 36 | vert_symmetric_extension: false, |
224 | 36 | hor_symmetric_extension: false, |
225 | 36 | reversible, |
226 | 36 | vertical_first: false, |
227 | 36 | unit_height: false, |
228 | 36 | unit_width: false, |
229 | 36 | empty: false, |
230 | 36 | sample_prec, |
231 | 36 | } |
232 | 36 | } |
233 | | |
234 | 18.5k | fn treadle_step( |
235 | 18.5k | support_min: i16, |
236 | 18.5k | downshift: u8, |
237 | 18.5k | rounding_offset: i16, |
238 | 18.5k | coeffs: [f32; 2], |
239 | 18.5k | icoeffs: [i32; 2], |
240 | 18.5k | kernel_id: u8, |
241 | 18.5k | ) -> StepSpec { |
242 | 18.5k | StepSpec { |
243 | 18.5k | support_min, |
244 | 18.5k | support_length: 2, |
245 | 18.5k | downshift, |
246 | 18.5k | rounding_offset, |
247 | 18.5k | coeffs: coeffs.to_vec(), |
248 | 18.5k | icoeffs: icoeffs.to_vec(), |
249 | 18.5k | kernel_id, |
250 | 18.5k | } |
251 | 18.5k | } |
252 | | |
253 | | /// W5X3 reversible lifting-step table: step 0 = predict {-1/2,-1/2} >>1 |
254 | | /// round 1, step 1 = update {1/4,1/4} >>2 round 2. |
255 | | /// `icoeffs = coeff * (1<<downshift)`. |
256 | 987 | pub fn w5x3_draft() -> Vec<StepSpec> { |
257 | 987 | vec![ |
258 | 987 | treadle_step(0, 1, 1, [-0.5, -0.5], [-1, -1], KERNEL_W5X3), |
259 | 987 | treadle_step(-1, 2, 2, [0.25, 0.25], [1, 1], KERNEL_W5X3), |
260 | | ] |
261 | 987 | } |
262 | | |
263 | | /// Build one W5X3 reversible engine at the given sample precision (I16 when |
264 | | /// every band's coefficients fit 15 bits + sign; I32 for higher precision). |
265 | 987 | pub fn warp_w5x3_prec(spec: &LevelSpec, prec: SamplePrec) -> Result<Synthesis, DecodeError> { |
266 | 987 | let params = draft_params(spec, prec, true, 2, true)?; |
267 | 987 | let steps = w5x3_draft(); |
268 | 987 | Ok(Synthesis::warp(params, &steps, &steps)) |
269 | 987 | } |
270 | | |
271 | | /// W9X7 irreversible lifting-step table: four float steps, no downshift; |
272 | | /// synthesis subtracts these analysis-direction factors. |
273 | 1.11k | pub fn w9x7_draft() -> Vec<StepSpec> { |
274 | | const A: f32 = -1.586_134_3; |
275 | | const B: f32 = -0.052980118; |
276 | | const C: f32 = 0.882_911_1; |
277 | | const D: f32 = 0.443_506_87; |
278 | 1.11k | vec![ |
279 | 1.11k | treadle_step(0, 0, 0, [A, A], [0, 0], KERNEL_W9X7), |
280 | 1.11k | treadle_step(-1, 0, 0, [B, B], [0, 0], KERNEL_W9X7), |
281 | 1.11k | treadle_step(0, 0, 0, [C, C], [0, 0], KERNEL_W9X7), |
282 | 1.11k | treadle_step(-1, 0, 0, [D, D], [0, 0], KERNEL_W9X7), |
283 | | ] |
284 | 1.11k | } |
285 | | |
286 | | /// Build one W9X7 irreversible engine (f32 samples in i32 lines). |
287 | 1.11k | pub fn warp_w9x7(spec: &LevelSpec) -> Result<Synthesis, DecodeError> { |
288 | 1.11k | let params = draft_params(spec, SamplePrec::I32, false, 4, true)?; |
289 | 1.11k | let steps = w9x7_draft(); |
290 | 1.11k | Ok(Synthesis::warp(params, &steps, &steps)) |
291 | 1.11k | } |
292 | | |
293 | | /// W9X7 int16 fixed-point lifting-step table (Q13 samples, see |
294 | | /// [`super::fixed97`]). Same step geometry as [`w9x7_draft`]; the arithmetic |
295 | | /// is hardcoded in kernels keyed on `step_idx`, so the coefficient fields |
296 | | /// are informational. |
297 | 3.03k | pub fn w9x7_i16_draft() -> Vec<StepSpec> { |
298 | | const A: f32 = -1.586_134_3; |
299 | | const B: f32 = -0.052980118; |
300 | | const C: f32 = 0.882_911_1; |
301 | | const D: f32 = 0.443_506_87; |
302 | 3.03k | vec![ |
303 | 3.03k | treadle_step(0, 0, 0, [A, A], [0, 0], KERNEL_W9X7), |
304 | 3.03k | treadle_step(-1, 0, 0, [B, B], [0, 0], KERNEL_W9X7), |
305 | 3.03k | treadle_step(0, 0, 0, [C, C], [0, 0], KERNEL_W9X7), |
306 | 3.03k | treadle_step(-1, 0, 0, [D, D], [0, 0], KERNEL_W9X7), |
307 | | ] |
308 | 3.03k | } |
309 | | |
310 | | /// Build one W9X7 irreversible engine on int16 Q13 fixed-point samples. |
311 | 3.03k | pub fn warp_w9x7_i16(spec: &LevelSpec) -> Result<Synthesis, DecodeError> { |
312 | 3.03k | let params = draft_params(spec, SamplePrec::I16, false, 4, true)?; |
313 | 3.03k | let steps = w9x7_i16_draft(); |
314 | 3.03k | Ok(Synthesis::warp(params, &steps, &steps)) |
315 | 3.03k | } |
316 | | |
317 | | /// W9X7 `low_scale`/`high_scale` (1/DC gain of the derived low analysis |
318 | | /// filter, 1/Nyquist gain of the high), in f32. Numerically (1/K, K/2) |
319 | | /// with K = 1.2301741. |
320 | 1.38k | pub fn w9x7_gains() -> (f32, f32) { |
321 | 1.38k | let support_min = [0i32, -1, 0, -1]; |
322 | 1.38k | let factors: [f32; 4] = [-1.586_134_3, -0.052980118, 0.882_911_1, 0.443_506_87]; |
323 | 1.38k | let num_steps = 4usize; |
324 | | const L: i32 = 16; // ample for the 9/7 support |
325 | 91.3k | let idx = |i: i32| (i + L) as usize; |
326 | | |
327 | 1.38k | let mut low_analysis: Vec<f32> = Vec::new(); |
328 | 1.38k | let mut high_analysis: Vec<f32> = Vec::new(); |
329 | 4.15k | for which in 0..2i32 { |
330 | | // Inverse-lift a coefficient impulse to get the synthesis branch. |
331 | 2.76k | let mut buf = [[0.0f32; (2 * L + 1) as usize]; 2]; |
332 | 2.76k | let mut bmin = [1i32, 1]; |
333 | 2.76k | let mut bmax = [-1i32, -1]; |
334 | 2.76k | buf[which as usize][idx(0)] = 1.0; |
335 | 2.76k | bmin[which as usize] = 0; |
336 | 2.76k | bmax[which as usize] = 0; |
337 | 11.0k | for s in (0..num_steps).rev() { |
338 | 11.0k | let sp = s & 1; |
339 | 11.0k | if bmax[sp] < bmin[sp] { |
340 | 1.38k | continue; |
341 | 9.68k | } |
342 | 9.68k | let ns = support_min[s]; |
343 | 9.68k | let ps = 2 - 1 + ns; |
344 | 9.68k | let other = 1 - sp; |
345 | 9.68k | let lo = bmin[sp] - ps; |
346 | 9.68k | let hi = bmax[sp] - ns; |
347 | 9.68k | if bmax[other] < bmin[other] { |
348 | 2.76k | bmin[other] = lo; |
349 | 2.76k | bmax[other] = hi; |
350 | 6.92k | } else { |
351 | 6.92k | bmin[other] = bmin[other].min(lo); |
352 | 6.92k | bmax[other] = bmax[other].max(hi); |
353 | 6.92k | } |
354 | 22.1k | for i in bmin[sp]..=bmax[sp] { |
355 | 22.1k | let val = buf[sp][idx(i)]; |
356 | 44.2k | for n in ns..=ps { |
357 | 44.2k | // Both taps of each step share one factor. |
358 | 44.2k | buf[other][idx(i - n)] -= val * factors[s]; |
359 | 44.2k | } |
360 | | } |
361 | | } |
362 | | // Interleave phases into the synthesis filter, derive analysis by |
363 | | // sign alternation (which=0 -> high analysis; which=1 -> low). |
364 | 2.76k | let mut syn = vec![0.0f32; (4 * L + 1) as usize]; |
365 | 202k | let sidx = |i: i32| (i + 2 * L) as usize; |
366 | 8.30k | for s in 0..2i32 { |
367 | 5.53k | if bmax[s as usize] < bmin[s as usize] { |
368 | 0 | continue; |
369 | 5.53k | } |
370 | 22.1k | for nn in bmin[s as usize]..=bmax[s as usize] { |
371 | 22.1k | syn[sidx(2 * nn + s - which)] = buf[s as usize][idx(nn)]; |
372 | 22.1k | } |
373 | | } |
374 | 2.76k | let ana: Vec<f32> = (-2 * L..=2 * L) |
375 | 179k | .map(|n| { |
376 | 179k | if n & 1 != 0 { |
377 | 88.5k | -syn[sidx(n)] |
378 | | } else { |
379 | 91.3k | syn[sidx(n)] |
380 | | } |
381 | 179k | }) |
382 | 2.76k | .collect(); |
383 | 2.76k | if which == 0 { |
384 | 1.38k | high_analysis = ana; |
385 | 1.38k | } else { |
386 | 1.38k | low_analysis = ana; |
387 | 1.38k | } |
388 | | } |
389 | | // Gains summed in ascending tap order. |
390 | 1.38k | let mut low_gain = 0.0f32; |
391 | 91.3k | for &t in &low_analysis { |
392 | 89.9k | low_gain += t; |
393 | 89.9k | } |
394 | 1.38k | let mut high_gain = 0.0f32; |
395 | 89.9k | for (n, &t) in high_analysis.iter().enumerate() { |
396 | 89.9k | let signed_n = n as i32 - 2 * L; |
397 | 89.9k | high_gain += if signed_n & 1 != 0 { -t } else { t }; |
398 | | } |
399 | 1.38k | (1.0 / low_gain, 1.0 / high_gain) |
400 | 1.38k | } |
401 | | |
402 | | #[cfg(test)] |
403 | | mod tests { |
404 | | use super::*; |
405 | | |
406 | | fn bd(w: i32, h: i32) -> BandDims { |
407 | | BandDims { x0: 0, y0: 0, w, h } |
408 | | } |
409 | | |
410 | | fn spec(node_h: i32, ll_h: i32, lh_h: i32) -> LevelSpec { |
411 | | LevelSpec { |
412 | | node: bd(4, node_h), |
413 | | ll: bd(2, ll_h), |
414 | | hl: bd(2, ll_h), |
415 | | lh: bd(2, lh_h), |
416 | | } |
417 | | } |
418 | | |
419 | | fn draft(spec: &LevelSpec) -> Result<SynthesisParams, DecodeError> { |
420 | | draft_params(spec, SamplePrec::I32, true, 2, true) |
421 | | } |
422 | | |
423 | | /// `Result::expect_err` needs `Debug` on the success type, which |
424 | | /// `SynthesisParams` does not derive. |
425 | | fn reject_reason(spec: &LevelSpec) -> String { |
426 | | match draft(spec) { |
427 | | Ok(_) => panic!("inconsistent geometry must be rejected"), |
428 | | Err(DecodeError::Logic(msg)) => msg, |
429 | | } |
430 | | } |
431 | | |
432 | | #[test] |
433 | | fn accepts_a_normal_level() { |
434 | | assert!(draft(&spec(4, 2, 2)).is_ok()); |
435 | | } |
436 | | |
437 | | #[test] |
438 | | fn accepts_a_single_row_level() { |
439 | | // node one row tall: lh is legitimately empty and ll is a single line |
440 | | let params = draft(&spec(1, 1, 0)).expect("single-row level must be accepted"); |
441 | | assert!(params.unit_height); |
442 | | } |
443 | | |
444 | | #[test] |
445 | | fn rejects_an_empty_subband_beside_a_tall_one() { |
446 | | // the v4dwt_interleave_h.gsr105.j2k geometry: lh has no height while ll |
447 | | // spans 100 rows, which used to trip an assert and panic |
448 | | let msg = reject_reason(&spec(100, 100, 0)); |
449 | | assert!(msg.contains("lh subband has no height"), "{msg}"); |
450 | | } |
451 | | |
452 | | use super::super::synthesis::{PullStatus, RowSource}; |
453 | | |
454 | | /// Deterministic band sample at absolute band coordinates. |
455 | | fn band_sample(sb: usize, x: i32, y: i32) -> i32 { |
456 | | (x * 31 + y * 17 + sb as i32 * 101).rem_euclid(64) - 32 |
457 | | } |
458 | | |
459 | | /// Serves band rows from `band_sample`, starting at each band's window |
460 | | /// origin — the same values a full source serves on the overlap. |
461 | | struct ArraySource { |
462 | | /// per band: (x0, y0, width) of the served window |
463 | | origin: [(i32, i32, i32); 4], |
464 | | next_row: [i32; 4], |
465 | | } |
466 | | |
467 | | impl ArraySource { |
468 | | fn warp(origin: [(i32, i32, i32); 4]) -> Self { |
469 | | let next_row = [origin[0].1, origin[1].1, origin[2].1, origin[3].1]; |
470 | | ArraySource { origin, next_row } |
471 | | } |
472 | | } |
473 | | |
474 | | impl RowSource for ArraySource { |
475 | | fn subband_ready(&self, _sb: i32) -> bool { |
476 | | true |
477 | | } |
478 | | unsafe fn pack(&mut self, sb: i32, buf: *mut u8, width: i32) { |
479 | | let (x0, _, w) = self.origin[sb as usize]; |
480 | | assert_eq!(w, width, "band {sb} width"); |
481 | | let y = self.next_row[sb as usize]; |
482 | | self.next_row[sb as usize] += 1; |
483 | | let out = buf as *mut i32; |
484 | | for i in 0..width { |
485 | | unsafe { *out.add(i as usize) = band_sample(sb as usize, x0 + i, y) }; |
486 | | } |
487 | | } |
488 | | } |
489 | | |
490 | | /// Pull every row the spec's node describes; returns rows of node-width |
491 | | /// i32 samples. |
492 | | fn pull_all(spec: &LevelSpec) -> Vec<Vec<i32>> { |
493 | | use super::super::synthesis::AlignedVec; |
494 | | let engine = warp_w5x3_prec(spec, SamplePrec::I32).expect("engine must build"); |
495 | | let mut engine = engine; |
496 | | let mut src = ArraySource::warp([ |
497 | | (spec.ll.x0, spec.ll.y0, spec.ll.w), |
498 | | (spec.hl.x0, spec.hl.y0, spec.hl.w), |
499 | | (spec.lh.x0, spec.lh.y0, spec.lh.w), |
500 | | (spec.hl.x0, spec.lh.y0, spec.hl.w), // hh: hl's width, lh's height |
501 | | ]); |
502 | | let mut rows = Vec::new(); |
503 | | let mut buf = AlignedVec::bare(engine.hem_row_bytes().max(64)); |
504 | | for _ in 0..spec.node.h { |
505 | | match unsafe { engine.try_draw(buf.as_mut_ptr(), &mut src) } { |
506 | | PullStatus::Row => { |
507 | | let row = unsafe { |
508 | | std::slice::from_raw_parts(buf.as_ptr() as *const i32, spec.node.w as usize) |
509 | | }; |
510 | | rows.push(row.to_vec()); |
511 | | } |
512 | | PullStatus::Stalled => panic!("engine stalled with an always-ready source"), |
513 | | } |
514 | | } |
515 | | rows |
516 | | } |
517 | | |
518 | | /// A windowed spec (a padded sub-rect of the full level, with its B-15 |
519 | | /// band splits) must synthesize the window's interior bit-exact against |
520 | | /// the full-level engine. |
521 | | #[test] |
522 | | fn windowed_specs_match_the_full_synthesis() { |
523 | | let full = LevelSpec { |
524 | | node: BandDims { |
525 | | x0: 0, |
526 | | y0: 0, |
527 | | w: 64, |
528 | | h: 48, |
529 | | }, |
530 | | ll: BandDims { |
531 | | x0: 0, |
532 | | y0: 0, |
533 | | w: 32, |
534 | | h: 24, |
535 | | }, |
536 | | hl: BandDims { |
537 | | x0: 0, |
538 | | y0: 0, |
539 | | w: 32, |
540 | | h: 24, |
541 | | }, |
542 | | lh: BandDims { |
543 | | x0: 0, |
544 | | y0: 0, |
545 | | w: 32, |
546 | | h: 24, |
547 | | }, |
548 | | }; |
549 | | let reference = pull_all(&full); |
550 | | |
551 | | // vertical window rows 8..40, padded to node rows 4..44 |
552 | | let vertical = LevelSpec { |
553 | | node: BandDims { |
554 | | x0: 0, |
555 | | y0: 4, |
556 | | w: 64, |
557 | | h: 40, |
558 | | }, |
559 | | ll: BandDims { |
560 | | x0: 0, |
561 | | y0: 2, |
562 | | w: 32, |
563 | | h: 20, |
564 | | }, |
565 | | hl: BandDims { |
566 | | x0: 0, |
567 | | y0: 2, |
568 | | w: 32, |
569 | | h: 20, |
570 | | }, |
571 | | lh: BandDims { |
572 | | x0: 0, |
573 | | y0: 2, |
574 | | w: 32, |
575 | | h: 20, |
576 | | }, |
577 | | }; |
578 | | let got = pull_all(&vertical); |
579 | | for y in 8..40 { |
580 | | assert_eq!( |
581 | | got[(y - 4) as usize], |
582 | | reference[y as usize], |
583 | | "vertical window row {y}" |
584 | | ); |
585 | | } |
586 | | |
587 | | // horizontal window cols 16..48, padded to node cols 12..52 |
588 | | let horizontal = LevelSpec { |
589 | | node: BandDims { |
590 | | x0: 12, |
591 | | y0: 0, |
592 | | w: 40, |
593 | | h: 48, |
594 | | }, |
595 | | ll: BandDims { |
596 | | x0: 6, |
597 | | y0: 0, |
598 | | w: 20, |
599 | | h: 24, |
600 | | }, |
601 | | hl: BandDims { |
602 | | x0: 6, |
603 | | y0: 0, |
604 | | w: 20, |
605 | | h: 24, |
606 | | }, |
607 | | lh: BandDims { |
608 | | x0: 6, |
609 | | y0: 0, |
610 | | w: 20, |
611 | | h: 24, |
612 | | }, |
613 | | }; |
614 | | let got = pull_all(&horizontal); |
615 | | for y in 0..48 { |
616 | | assert_eq!( |
617 | | got[y as usize][(16 - 12)..(48 - 12)], |
618 | | reference[y as usize][16..48], |
619 | | "horizontal window row {y}" |
620 | | ); |
621 | | } |
622 | | } |
623 | | |
624 | | /// The int16 Q13 9/7 engine must track the f32 engine within a few Q13 |
625 | | /// LSBs (well under one sample LSB at the path's 8-bit precision limit). |
626 | | /// Sources fold the one-level 2D band gains the way the decode graph |
627 | | /// does: into the f32 deltas for the float path, into the Q13 dequant |
628 | | /// for the fixed-point path. |
629 | | #[test] |
630 | | fn q13_engine_tracks_the_float_engine() { |
631 | | use super::super::synthesis::AlignedVec; |
632 | | struct FnSource<F: FnMut(i32, *mut u8, i32)> { |
633 | | fill: F, |
634 | | } |
635 | | impl<F: FnMut(i32, *mut u8, i32)> RowSource for FnSource<F> { |
636 | | fn subband_ready(&self, _sb: i32) -> bool { |
637 | | true |
638 | | } |
639 | | unsafe fn pack(&mut self, sb: i32, buf: *mut u8, width: i32) { |
640 | | (self.fill)(sb, buf, width) |
641 | | } |
642 | | } |
643 | | |
644 | | let spec = LevelSpec { |
645 | | node: BandDims { |
646 | | x0: 0, |
647 | | y0: 0, |
648 | | w: 64, |
649 | | h: 48, |
650 | | }, |
651 | | ll: BandDims { |
652 | | x0: 0, |
653 | | y0: 0, |
654 | | w: 32, |
655 | | h: 24, |
656 | | }, |
657 | | hl: BandDims { |
658 | | x0: 0, |
659 | | y0: 0, |
660 | | w: 32, |
661 | | h: 24, |
662 | | }, |
663 | | lh: BandDims { |
664 | | x0: 0, |
665 | | y0: 0, |
666 | | w: 32, |
667 | | h: 24, |
668 | | }, |
669 | | }; |
670 | | let (low, high) = w9x7_gains(); |
671 | | let gain = [ |
672 | | 1.0 / (low * low), |
673 | | 1.0 / (low * high), |
674 | | 1.0 / (high * low), |
675 | | 1.0 / (high * high), |
676 | | ]; |
677 | | // normalized band values in ±0.25 — sample scale for the BIBO budget |
678 | | let value = |sb: i32, x: i32, y: i32| band_sample(sb as usize, x, y) as f32 / 128.0; |
679 | | |
680 | | let mut engine_f = warp_w9x7(&spec).expect("f32 engine"); |
681 | | let mut engine_q = warp_w9x7_i16(&spec).expect("q13 engine"); |
682 | | |
683 | | let mut next_f = [0i32; 4]; |
684 | | let mut src_f = FnSource { |
685 | | fill: |sb: i32, buf: *mut u8, width: i32| { |
686 | | let y = next_f[sb as usize]; |
687 | | next_f[sb as usize] += 1; |
688 | | let out = buf as *mut f32; |
689 | | for i in 0..width { |
690 | | unsafe { *out.add(i as usize) = value(sb, i, y) * gain[sb as usize] }; |
691 | | } |
692 | | }, |
693 | | }; |
694 | | let mut next_q = [0i32; 4]; |
695 | | let mut src_q = FnSource { |
696 | | fill: |sb: i32, buf: *mut u8, width: i32| { |
697 | | let y = next_q[sb as usize]; |
698 | | next_q[sb as usize] += 1; |
699 | | let out = buf as *mut i16; |
700 | | for i in 0..width { |
701 | | let q = (value(sb, i, y) * gain[sb as usize] * 8192.0).round_ties_even(); |
702 | | unsafe { *out.add(i as usize) = q as i16 }; |
703 | | } |
704 | | }, |
705 | | }; |
706 | | |
707 | | let mut buf_f = AlignedVec::bare(engine_f.hem_row_bytes().max(64)); |
708 | | let mut buf_q = AlignedVec::bare(engine_q.hem_row_bytes().max(64)); |
709 | | let mut max_diff = 0.0f32; |
710 | | for y in 0..spec.node.h { |
711 | | let s = unsafe { engine_f.try_draw(buf_f.as_mut_ptr(), &mut src_f) }; |
712 | | assert_eq!(s, PullStatus::Row, "f32 row {y}"); |
713 | | let s = unsafe { engine_q.try_draw(buf_q.as_mut_ptr(), &mut src_q) }; |
714 | | assert_eq!(s, PullStatus::Row, "q13 row {y}"); |
715 | | let row_f = unsafe { |
716 | | std::slice::from_raw_parts(buf_f.as_ptr() as *const f32, spec.node.w as usize) |
717 | | }; |
718 | | let row_q = unsafe { |
719 | | std::slice::from_raw_parts(buf_q.as_ptr() as *const i16, spec.node.w as usize) |
720 | | }; |
721 | | for x in 0..spec.node.w as usize { |
722 | | let diff = (row_q[x] as f32 / 8192.0 - row_f[x]).abs(); |
723 | | max_diff = max_diff.max(diff); |
724 | | assert!( |
725 | | diff <= 8.0 / 8192.0, |
726 | | "row {y} col {x}: q13 {} vs f32 {} (max so far {max_diff})", |
727 | | row_q[x], |
728 | | row_f[x] |
729 | | ); |
730 | | } |
731 | | } |
732 | | } |
733 | | |
734 | | #[test] |
735 | | fn rejects_an_empty_subband_beside_a_wide_one() { |
736 | | let wide = LevelSpec { |
737 | | node: bd(100, 4), |
738 | | ll: bd(100, 2), |
739 | | hl: bd(0, 2), |
740 | | lh: bd(100, 2), |
741 | | }; |
742 | | let msg = reject_reason(&wide); |
743 | | assert!(msg.contains("hl subband has no width"), "{msg}"); |
744 | | } |
745 | | } |