Coverage Report

Created: 2026-10-28 07:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}