Skip to main content

rusty_h264_decoder/
cabac.rs

1//! CABAC arithmetic decoding engine (spec §9.3.3.2) + context initialization
2//! (§9.3.1.1). The literal-spec engine (codIRange/codIOffset, RenormD), which is
3//! bit-exact to openh264's optimized variant. Tables in [`crate::cabac_tables`].
4
5use rusty_h264_common::cabac_tables::{CTX_INIT, RANGE_LPS, STATE_TRANS};
6
7/// Profile-only bin census: how many bins of each class the engine decodes.
8/// The entropy stage's time divided by these counts gives ns/bin — the number
9/// that decides whether the engine or the syntax around it is the target.
10#[cfg(feature = "profile")]
11pub mod bin_census {
12    use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
13    pub static DECISIONS: AtomicU64 = AtomicU64::new(0);
14    pub static BYPASSES: AtomicU64 = AtomicU64::new(0);
15    pub static TERMINATES: AtomicU64 = AtomicU64::new(0);
16    /// Decision bins whose renormalization shift was nonzero.
17    pub static RENORMS: AtomicU64 = AtomicU64::new(0);
18    pub fn reset() {
19        DECISIONS.store(0, Relaxed);
20        BYPASSES.store(0, Relaxed);
21        TERMINATES.store(0, Relaxed);
22    }
23    pub fn snapshot() -> (u64, u64, u64) {
24        (DECISIONS.load(Relaxed), BYPASSES.load(Relaxed), TERMINATES.load(Relaxed))
25    }
26    pub fn renorms() -> u64 {
27        RENORMS.load(Relaxed)
28    }
29}
30
31/// FUSED per-(quartile, packed-state) record: `lps | trans_mps<<8 | trans_lps<<16`.
32///
33/// A context model is ONE byte: `state * 2 + mps` (0..=127) — ffmpeg/openh264's
34/// packing (H-35). The literal two-field form cost two loads and two stores per
35/// bin plus `1 - mps` arithmetic; packed, a bin is one byte load, one table
36/// lookup, one byte store, and `s & 1` for the value. This table folds the state
37/// transition AND the state-0 MPS flip (spec §9.3.3.2.1.1) into the lookup, so
38/// the decoded bins are identical by construction. Built at compile time from
39/// the spec tables: no init cost, no `OnceLock` check on the hot path.
40///
41/// Why: the serial chain of a decision bin ended with a LATE load — the
42/// transition table's address needs the LPS/MPS MASK, which exists only after
43/// the compare, so the context write-back (and every same-context successor
44/// bin: all unary and level-prefix loops re-read the context they just wrote)
45/// waited on a ~5-cycle L1 load issued at the chain's end. Folding both
46/// transition bytes into the SAME u32 the LPS quantity comes from makes them
47/// arrive EARLY (with the lps load, whose address needs only `s` and `q`),
48/// and the post-compare step becomes a 1-cycle shift-select:
49/// `(entry >> (8 + (mask & 8))) & 0xFF`. 2 KB, L1-resident like the tables it
50/// replaces on this path.
51const fn build_fused() -> [u32; 4 * 128] {
52    let mut t = [0u32; 4 * 128];
53    let mut q = 0;
54    while q < 4 {
55        let mut s = 0;
56        while s < 128 {
57            let lps = RANGE_LPS[s >> 1][q] as u32;
58            let tm = {
59                let mps = s as u8 & 1;
60                ((STATE_TRANS[s >> 1][1] << 1) | mps) as u32
61            };
62            let tl = {
63                let mps = s as u8 & 1;
64                let new_mps = if s >> 1 == 0 { 1 - mps } else { mps };
65                ((STATE_TRANS[s >> 1][0] << 1) | new_mps) as u32
66            };
67            t[q * 128 + s] = lps | (tm << 8) | (tl << 16);
68            s += 1;
69        }
70        q += 1;
71    }
72    t
73}
74static FUSED: [u32; 4 * 128] = build_fused();
75
76/// Bit position of the arithmetic offset field inside [`Cabac::low`].
77const OFF: u32 = 41;
78/// Refill when fewer than this many buffered bits remain. 8 covers the worst
79/// single renormalization (6 bits) with margin; a 4-byte refill then lasts
80/// ~30 typical bins.
81const REFILL_AT: i32 = 8;
82
83/// The CABAC decoder: arithmetic engine reading MSB-first from the RBSP plus the
84/// 460 adaptive context models.
85pub struct Cabac<'a> {
86    data: &'a [u8],
87    /// Next byte to load into the bit window.
88    byte_pos: usize,
89    /// FUSED offset+window register (the renorm/refill reshape, WHYS Part 22
90    /// follow-through). `low = codIOffset · 2^41 + buf`, where `buf < 2^41`
91    /// holds the next `cnt` stream bits LEFT-ALIGNED at bit 40 downward.
92    ///
93    /// Why fused: the old engine kept `offset` and a separate MSB-aligned
94    /// `window`, so every renormalization did `offset = (offset<<n)|take(n)`
95    /// — a window shift, a `wbits` check+update, and a merge, all on the
96    /// serial per-bin chain. With the stream bits sitting DIRECTLY BELOW the
97    /// offset in one register, renorm is `low <<= n`: the next bits enter the
98    /// offset field by construction.
99    ///
100    /// The invariants that make it exact (not approximate):
101    /// - `offset >= range  ⟺  low >= range << 41`, because
102    ///   `low = offset·2^41 + buf` with `buf < 2^41` — the buffered bits can
103    ///   never flip the comparison.
104    /// - The LPS subtraction `low -= range << 41` cannot borrow into `buf`:
105    ///   the subtrahend is zero below bit 41 and (mask-gated) `low ≥` it.
106    /// - `cnt ≤ 6 + 32 < 41`: refill fires only under `REFILL_AT`, so the
107    ///   buffer never collides with the offset field.
108    /// Zero-fill past the buffer end is preserved exactly (the fuzzer's
109    /// slice-loop bound relies on it).
110    low: u64,
111    /// Valid buffered bits below the offset field.
112    cnt: i32,
113    range: u32,
114    /// 460 context models, each packed as `state * 2 + mps`.
115    ctx: [u8; 460],
116    /// Bring-up symbol trace (Brick 0.3): when `RH_CABAC_TRACE=1`, print the
117    /// spec-canonical entering `(codIRange, codIOffset)` before each bin, in the
118    /// SAME `"<n> <D|B|T> r=<range> o=<offset>"` format as the instrumented openh264
119    /// oracle — so the two traces diff line-for-line to localise the first divergence.
120    trace: bool,
121    sym: u64,
122}
123
124impl Cabac<'_> {
125    #[inline]
126    fn tr(&mut self, kind: &str) {
127        if self.trace {
128            eprintln!("{} {} r={} o={}", self.sym, kind, self.range, self.low >> OFF);
129            self.sym += 1;
130        }
131    }
132
133}
134
135impl<'a> Cabac<'a> {
136    /// Initializes from the RBSP `data` at byte offset `start_byte` (the slice
137    /// data, byte-aligned past the header), the slice's `qp` (clamped 0..51),
138    /// `cabac_init_idc`, and whether the slice is I/SI (spec §9.3.1).
139    pub fn new(data: &'a [u8], start_byte: usize, qp: i32, init_idc: u32, is_i: bool) -> Self {
140        let model = if is_i { 0 } else { ((init_idc + 1) as usize).min(3) };
141        let q = qp.clamp(0, 51);
142        let mut ctx = [0u8; 460];
143        for (i, c) in ctx.iter_mut().enumerate() {
144            let (m, n) = CTX_INIT[i][model];
145            let pre = (((m as i32 * q) >> 4) + n as i32).clamp(1, 126);
146            // Packed as state*2 + mps; same (state, mps) pair as the spec form.
147            *c = if pre <= 63 {
148                ((63 - pre) as u8) << 1
149            } else {
150                (((pre - 64) as u8) << 1) | 1
151            };
152        }
153        let trace = std::env::var_os("RH_CABAC_TRACE").is_some();
154        let mut e = Cabac { data, byte_pos: start_byte, low: 0, cnt: 0, range: 510, ctx, trace, sym: 0 };
155        e.refill();
156        // codIOffset = first 9 bits: shift them from the buffer into the
157        // offset field — the same fused move renorm makes every bin.
158        e.low <<= 9;
159        e.cnt -= 9;
160        e
161    }
162
163    /// Engine state `(codIRange, codIOffset)` — for bring-up verification against the
164    /// oracle's symbol 0 (Brick 1.1). At slice start this is `(510, first-9-bits)`.
165    pub fn dbg_state(&self) -> (u32, u32) {
166        (self.range, (self.low >> OFF) as u32)
167    }
168
169    /// I_PCM sample position (spec §7.3.5 + §9.3.3.2.5). The PCM marker is a
170    /// terminate bin; after it decodes as 1, the encoder's flush output is
171    /// already inside the engine's borrowed offset bits, so the raw
172    /// `pcm_sample_*` bytes start at the consumed-bit position rounded up to
173    /// the next byte boundary (`pcm_alignment_zero_bit`s). `byte_pos·8 − cnt`
174    /// is that consumed position (offset-field bits count as read, buffered
175    /// bits do not). Valid only immediately after `decode_terminate()`
176    /// returned `true` (no renormalization has run since).
177    pub fn pcm_start_byte(&self) -> usize {
178        let consumed = self.byte_pos as isize * 8 - self.cnt as isize;
179        ((consumed + 7) >> 3) as usize
180    }
181
182    /// Re-initializes the arithmetic engine at absolute `byte` (spec §9.3.1.2,
183    /// invoked after the I_PCM samples), KEEPING the adaptive context models —
184    /// only the engine registers restart. Mirrors the tail of [`Cabac::new`].
185    pub fn reinit_at(&mut self, byte: usize) {
186        self.byte_pos = byte;
187        self.low = 0;
188        self.cnt = 0;
189        self.range = 510;
190        self.refill();
191        self.low <<= 9;
192        self.cnt -= 9;
193    }
194
195    /// Appends 32 fresh stream bits directly below the current buffer fill
196    /// (zero-filled past the end of the data, exactly like the old reader).
197    /// Only called when `cnt < REFILL_AT`, so the insert shift `9 - cnt` is
198    /// always in `[2..=9]` and the result stays under bit 41.
199    #[inline]
200    fn refill(&mut self) {
201        let v = match self.data.get(self.byte_pos..self.byte_pos + 4) {
202            Some(c) => u32::from_be_bytes([c[0], c[1], c[2], c[3]]),
203            None => {
204                let b = |i: usize| self.data.get(self.byte_pos + i).copied().unwrap_or(0) as u32;
205                (b(0) << 24) | (b(1) << 16) | (b(2) << 8) | b(3)
206            }
207        };
208        self.low |= (v as u64) << ((OFF as i32 - 32 - self.cnt) as u32);
209        self.byte_pos += 4;
210        self.cnt += 32;
211    }
212
213    /// Renormalization (spec §9.3.3.2.2): keep `range` ≥ 256. BRANCHLESS shift
214    /// count as before (`range ≤ 510` ⇒ `leading_zeros()-23` is exactly the
215    /// spec loop's iteration count), but the offset refill is now ONE shared
216    /// shift of the fused register — the old `(offset<<n)|take(n)` bookkeeping
217    /// (window shift, wbits check+update, merge) is gone from the serial chain.
218    #[inline(always)]
219    fn renorm(&mut self) {
220        let n = self.range.leading_zeros() - 23;
221        self.range <<= n;
222        self.low <<= n;
223        self.cnt -= n as i32;
224        if self.cnt < REFILL_AT {
225            self.refill();
226        }
227    }
228
229    /// Decodes a context-coded bin (spec §9.3.3.2.1), updating the context model.
230    /// STATE-RESIDENCY REFUTED (WHYS Part 21): this attribute was added on the
231    /// Part 19 hypothesis that the engine state round-tripped memory per bin
232    /// through an outlined call. The symbol table refuted it — LLVM already
233    /// fully inlined this method in the un-attributed build (zero outlined
234    /// copies in either binary), and the A/B was null as that predicts. The
235    /// Part 19 ns/bin sizing was also census-tax-inflated: the true engine
236    /// cost is ~4 ns/bin, and the residual gap vs ffmpeg's ~2 is the engine's
237    /// per-bin WORK (u64-window renorm bookkeeping vs a 16-bit lazy refill),
238    /// not call overhead. The attribute stays as documentation + insurance.
239    #[inline(always)]
240    pub fn decode_decision(&mut self, ctx_idx: usize) -> u32 {
241        #[cfg(feature = "profile")]
242        bin_census::DECISIONS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
243        self.tr("D");
244        // BRANCHLESS bin decode (H-35, ffmpeg's `get_cabac_inline` shape). The
245        // LPS/MPS test is inherently ~coin-flip on a well-adapted context, so a
246        // branch here mispredicts constantly; instead derive an all-ones/zero
247        // MASK and select with arithmetic. `& 127` is free insurance that also
248        // proves every table index in range, dropping the bounds checks.
249        // STATE THE CEILING. `& 127` (below) proves the FUSED index but says
250        // nothing about `ctx_idx` itself, so BOTH the load here and the
251        // write-back at the end of this function carried a check — on a path
252        // that runs ~154M times per clip. `ctx` is `[u8; 460]` and every context
253        // index the spec defines is below that, so `.min(459)` is a no-op.
254        let ctx_idx = ctx_idx.min(459);
255        let s = (self.ctx[ctx_idx] & 127) as usize;
256        let q = ((self.range >> 6) & 3) as usize;
257        // ONE early load yields the LPS range AND both context transitions —
258        // see `build_fused` for why the transitions must not be a second,
259        // mask-addressed (late) load.
260        let e = FUSED[q * 128 + s];
261        let lps = e & 0xFF;
262        // PRECONDITION of the mask arithmetic below: `range >= 256` on entry, so
263        // `range - lps` (lps <= 240) stays positive and the i32 sign test is a
264        // true "offset >= range" test. Renormalization guarantees it after every
265        // bin, and `new()` starts at 510 — the literal `if` form did not need
266        // this, so it is asserted rather than assumed.
267        debug_assert!(self.range >= 256, "renorm invariant broken: range={}", self.range);
268        self.range -= lps;
269        // mask = !0 when `offset >= range` (the LPS path), else 0 — the same
270        // sign trick in 64 bits against the SCALED range. Values stay below
271        // 2^51, so the i64 arithmetic cannot overflow, and the buffered bits
272        // cannot flip the comparison (see the `low` invariants).
273        let scaled = (self.range as u64) << OFF;
274        let mask64 = ((scaled as i64 - self.low as i64 - 1) >> 63) as u64;
275        let mask = mask64 as u32;
276        // LPS: offset -= range; range = lps.  MPS: both unchanged.
277        self.low -= scaled & mask64;
278        self.range = self.range.wrapping_add(lps.wrapping_sub(self.range) & mask);
279        // Both transitions arrived with the lps load; pick by mask with a
280        // shift (mask & 8 = 8 exactly on the LPS path).
281        self.ctx[ctx_idx] = ((e >> (8 + (mask & 8))) & 0xFF) as u8;
282        // MPS -> s&1; LPS -> (s&1)^1.
283        let bin = (s as u32 ^ mask) & 1;
284        #[cfg(feature = "profile")]
285        if self.range < 256 {
286            bin_census::RENORMS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
287        }
288        self.renorm();
289        bin
290    }
291
292    /// Decodes a bypass (equiprobable) bin (spec §9.3.3.2.3).
293    #[inline(always)]
294    pub fn decode_bypass(&mut self) -> u32 {
295        #[cfg(feature = "profile")]
296        bin_census::BYPASSES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
297        self.tr("B");
298        self.low <<= 1;
299        self.cnt -= 1;
300        if self.cnt < REFILL_AT {
301            self.refill();
302        }
303        let scaled = (self.range as u64) << OFF;
304        if self.low >= scaled {
305            self.low -= scaled;
306            1
307        } else {
308            0
309        }
310    }
311
312    /// Decodes the terminate bin (spec §9.3.3.2.4); `true` ends the slice (or
313    /// marks I_PCM). No renormalization on terminate.
314    #[inline(always)]
315    pub fn decode_terminate(&mut self) -> bool {
316        #[cfg(feature = "profile")]
317        bin_census::TERMINATES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
318        self.tr("T");
319        self.range -= 2;
320        if self.low >= (self.range as u64) << OFF {
321            true
322        } else {
323            self.renorm();
324            false
325        }
326    }
327
328    // NB: the byte offset where byte-aligned `pcm_sample` data resumes after an
329    // I_PCM under CABAC IS wired: `pcm_start_byte()` + `reinit_at()` above are
330    // the byte-realign/re-init pair, dispatched from the decoder's mb16 I_PCM
331    // arm and gated by tests/ipcm_cabac.rs against ffmpeg.
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    /// Packed-form twin tables — the two-table formulation `FUSED` folded away.
339    /// Kept HERE (test-only) as the oracle that pins each `FUSED` field to the
340    /// spec tables; production reads only `FUSED`.
341    const fn build_lps_range() -> [u8; 4 * 128] {
342        let mut t = [0u8; 4 * 128];
343        let mut q = 0;
344        while q < 4 {
345            let mut s = 0;
346            while s < 128 {
347                t[q * 128 + s] = RANGE_LPS[s >> 1][q];
348                s += 1;
349            }
350            q += 1;
351        }
352        t
353    }
354    /// ONE transition table covering both paths: `[0..128)` MPS (state advances,
355    /// MPS unchanged), `[128..256)` LPS (state falls back; at state 0 the MPS
356    /// FLIPS per spec §9.3.3.2.1.1 — baked in, never branched).
357    const fn build_trans() -> [u8; 256] {
358        let mut t = [0u8; 256];
359        let mut s = 0;
360        while s < 128 {
361            let mps = s as u8 & 1;
362            t[s] = (STATE_TRANS[s >> 1][1] << 1) | mps;
363            let new_mps = if s >> 1 == 0 { 1 - mps } else { mps };
364            t[128 + s] = (STATE_TRANS[s >> 1][0] << 1) | new_mps;
365            s += 1;
366        }
367        t
368    }
369    static LPS_RANGE: [u8; 4 * 128] = build_lps_range();
370    static TRANS: [u8; 256] = build_trans();
371
372    /// Literal-spec CABAC *encoder* (§9.3.4), the inverse of [`Cabac`]. Used only
373    /// to validate the decoder by round-trip — encode a bin sequence, decode it,
374    /// assert equality. Encoder and decoder are independent algorithms (encode
375    /// vs decode), so a shared latent bug is implausible; a clean round-trip over
376    /// thousands of mixed bins exercises the full range/offset evolution, every
377    /// `RANGE_LPS`/`STATE_TRANS` entry reached, and the bypass/terminate paths.
378    struct Enc {
379        low: u32,
380        range: u32,
381        outstanding: u32,
382        first: bool,
383        bits: Vec<u8>,
384        ctx: Vec<(u8, u8)>, // (state, mps)
385    }
386
387    fn init_ctx(qp: i32, init_idc: u32, is_i: bool) -> Vec<(u8, u8)> {
388        let model = if is_i { 0 } else { ((init_idc + 1) as usize).min(3) };
389        let q = qp.clamp(0, 51);
390        (0..460)
391            .map(|i| {
392                let (m, n) = CTX_INIT[i][model];
393                let pre = (((m as i32 * q) >> 4) + n as i32).clamp(1, 126);
394                if pre <= 63 {
395                    ((63 - pre) as u8, 0)
396                } else {
397                    ((pre - 64) as u8, 1)
398                }
399            })
400            .collect()
401    }
402
403    impl Enc {
404        fn new(qp: i32, init_idc: u32, is_i: bool) -> Self {
405            Enc {
406                low: 0,
407                range: 510,
408                outstanding: 0,
409                first: true,
410                bits: Vec::new(),
411                ctx: init_ctx(qp, init_idc, is_i),
412            }
413        }
414
415        fn put_bit(&mut self, b: u32) {
416            if self.first {
417                self.first = false;
418            } else {
419                self.bits.push(b as u8);
420            }
421            while self.outstanding > 0 {
422                self.bits.push((1 - b) as u8);
423                self.outstanding -= 1;
424            }
425        }
426
427        /// RenormE (§9.3.4.3.3).
428        fn renorm(&mut self) {
429            while self.range < 256 {
430                if self.low < 256 {
431                    self.put_bit(0);
432                } else if self.low >= 512 {
433                    self.low -= 512;
434                    self.put_bit(1);
435                } else {
436                    self.low -= 256;
437                    self.outstanding += 1;
438                }
439                self.range <<= 1;
440                self.low <<= 1;
441            }
442        }
443
444        /// EncodeDecision (§9.3.4.3.1).
445        fn encode(&mut self, ctx_idx: usize, bin: u32) {
446            let (state, mps) = self.ctx[ctx_idx];
447            let q = ((self.range >> 6) & 3) as usize;
448            let lps = RANGE_LPS[state as usize][q] as u32;
449            self.range -= lps;
450            if bin != mps as u32 {
451                self.low += self.range;
452                self.range = lps;
453                let nm = if state == 0 { 1 - mps } else { mps };
454                self.ctx[ctx_idx] = (STATE_TRANS[state as usize][0], nm);
455            } else {
456                self.ctx[ctx_idx].0 = STATE_TRANS[state as usize][1];
457            }
458            self.renorm();
459        }
460
461        /// EncodeBypass (§9.3.4.3.2).
462        fn encode_bypass(&mut self, bin: u32) {
463            self.low <<= 1;
464            if bin != 0 {
465                self.low += self.range;
466            }
467            if self.low >= 1024 {
468                self.put_bit(1);
469                self.low -= 1024;
470            } else if self.low < 512 {
471                self.put_bit(0);
472            } else {
473                self.low -= 512;
474                self.outstanding += 1;
475            }
476        }
477
478        /// EncodeTerminate(1) + flush (§9.3.4.5 / EncodeFlush) — ends the stream.
479        fn finish(&mut self) -> Vec<u8> {
480            self.range -= 2;
481            self.low += self.range;
482            self.range = 2;
483            self.renorm();
484            self.put_bit((self.low >> 9) & 1);
485            let v = ((self.low >> 7) & 3) | 1;
486            self.bits.push(((v >> 1) & 1) as u8);
487            self.bits.push((v & 1) as u8);
488            // Pack MSB-first into bytes.
489            let mut out = vec![0u8; self.bits.len().div_ceil(8)];
490            for (i, &b) in self.bits.iter().enumerate() {
491                out[i / 8] |= b << (7 - (i % 8));
492            }
493            out
494        }
495    }
496
497    /// Deterministic xorshift RNG so the test is reproducible.
498    struct Rng(u32);
499    impl Rng {
500        fn next(&mut self) -> u32 {
501            self.0 ^= self.0 << 13;
502            self.0 ^= self.0 >> 17;
503            self.0 ^= self.0 << 5;
504            self.0
505        }
506    }
507
508    /// Encode a scripted mix of context-coded, bypass, and terminate bins, then
509    /// decode and assert every bin (and the terminate) round-trips exactly.
510    fn roundtrip(qp: i32, init_idc: u32, is_i: bool, seed: u32, n: usize) {
511        let mut rng = Rng(seed);
512        // (kind, ctx, bin): kind 0 = decision, 1 = bypass.
513        let mut script: Vec<(u8, usize, u32)> = Vec::with_capacity(n);
514        let mut enc = Enc::new(qp, init_idc, is_i);
515        for _ in 0..n {
516            let r = rng.next();
517            let kind = (r & 1) as u8;
518            let ctx = (r >> 1) as usize % 460;
519            let bin = (r >> 12) & 1;
520            script.push((kind, ctx, bin));
521            if kind == 0 {
522                enc.encode(ctx, bin);
523            } else {
524                enc.encode_bypass(bin);
525            }
526        }
527        let bytes = enc.finish();
528
529        let mut dec = Cabac::new(&bytes, 0, qp, init_idc, is_i);
530        for (i, &(kind, ctx, bin)) in script.iter().enumerate() {
531            let got = if kind == 0 {
532                dec.decode_decision(ctx)
533            } else {
534                dec.decode_bypass()
535            };
536            assert_eq!(got, bin, "bin {i} (kind {kind}, ctx {ctx}) mismatched");
537        }
538        assert!(dec.decode_terminate(), "terminate should signal end-of-stream");
539    }
540
541    #[test]
542    fn engine_roundtrip_many() {
543        // Sweep QP, init model, and many random scripts: every code path
544        // (LPS/MPS transitions across all 64 states, bypass, terminate, renorm).
545        for &qp in &[0, 12, 26, 37, 51] {
546            for &(idc, is_i) in &[(0u32, true), (0, false), (1, false), (2, false)] {
547                for seed in 1..=40u32 {
548                    roundtrip(qp, idc, is_i, seed.wrapping_mul(2654435761), seed as usize * 53);
549                }
550            }
551        }
552    }
553
554    #[test]
555    fn engine_init_matches_spec() {
556        // ctxIdx 0 (I mb_type, m=20 n=-15) at QP 26: preCtxState =
557        // Clip3(1,126,(20*26>>4)-15) = 17 -> state 63-17 = 46, MPS 0.
558        let dec = Cabac::new(&[0xFF, 0xFF, 0xFF], 0, 26, 0, true);
559        // Packed as state*2 + mps (H-35): state 46, MPS 0 -> 92.
560        assert_eq!(dec.ctx[0] >> 1, 46, "state");
561        assert_eq!(dec.ctx[0] & 1, 0, "mps");
562        // Engine init: range 510, offset = first 9 bits of 0xFFFF = 0x1FF.
563        assert_eq!(dec.range, 510);
564        assert_eq!(dec.dbg_state().1, 0x1FF);
565    }
566
567    /// H-35 oracle: for EVERY packed state and range quartile, the packed tables
568    /// must reproduce the literal spec derivation (RangeLPS, the bin value, and
569    /// both transitions including the state-0 MPS flip) exactly. 512 cases —
570    /// cheaper and stricter than trusting a corpus.
571    #[test]
572    fn packed_state_tables_match_spec_form() {
573        for s in 0usize..128 {
574            let (state, mps) = ((s >> 1) as u8, (s & 1) as u8);
575            for q in 0usize..4 {
576                assert_eq!(LPS_RANGE[q * 128 + s], RANGE_LPS[state as usize][q], "lps s={s} q={q}");
577            }
578            // MPS half: bin == mps, state advances, mps unchanged.
579            let mps_t = TRANS[s];
580            assert_eq!(mps_t >> 1, STATE_TRANS[state as usize][1], "mps-trans state s={s}");
581            assert_eq!(mps_t & 1, mps, "mps-trans mps s={s}");
582            // LPS half: bin == 1-mps, state falls back, mps flips only at state 0.
583            let lps_t = TRANS[128 + s];
584            let want_mps = if state == 0 { 1 - mps } else { mps };
585            assert_eq!(lps_t >> 1, STATE_TRANS[state as usize][0], "lps-trans state s={s}");
586            assert_eq!(lps_t & 1, want_mps, "lps-trans mps s={s}");
587        }
588    }
589
590    /// H-35 oracle #2: the BRANCHLESS mask arithmetic must equal the literal
591    /// `if offset >= range` form for every (range, offset, state) combination
592    /// the engine can present — the mask, the two conditional updates, the
593    /// transition-table half selection, and the bin value. This is the whole
594    /// risk surface of the branchless rewrite, checked exhaustively rather than
595    /// inferred from a corpus that happens to decode.
596    #[test]
597    fn branchless_mask_matches_conditional_form() {
598        // Reachable domain only: renorm guarantees `range` in 256..=510 on entry
599        // and the spec invariant `offset < range` holds throughout. (Widening
600        // past this tests states the engine cannot present — and the wrapped
601        // `range - lps` there makes BOTH forms meaningless, not just one.)
602        for s in 0usize..128 {
603            for range in [256u32, 257, 300, 383, 384, 400, 448, 509, 510] {
604                for offset in [0u32, 1, 127, 128, 255, 256, 300, 383, 384, 509] {
605                    if offset >= range {
606                        continue;
607                    }
608                    let q = ((range >> 6) & 3) as usize;
609                    let lps = LPS_RANGE[q * 128 + s] as u32;
610                    let r1 = range.wrapping_sub(lps);
611                    // literal spec form
612                    let (mut lr, mut lo, lbin, lctx) = if offset >= r1 {
613                        (lps, offset - r1, (s as u32 & 1) ^ 1, TRANS[128 + s])
614                    } else {
615                        (r1, offset, s as u32 & 1, TRANS[s])
616                    };
617                    // branchless form, exactly as `decode_decision` computes it
618                    let mask = ((r1 as i32 - offset as i32 - 1) >> 31) as u32;
619                    let bo = offset - (r1 & mask);
620                    let br = r1.wrapping_add(lps.wrapping_sub(r1) & mask);
621                    let bctx = TRANS[s | (mask as usize & 128)];
622                    let bbin = (s as u32 ^ mask) & 1;
623                    // (silence unused-mut on the literal bindings)
624                    lr += 0;
625                    lo += 0;
626                    assert_eq!((lr, lo, lbin, lctx), (br, bo, bbin, bctx), "s={s} range={range} offset={offset}");
627                }
628            }
629        }
630    }
631
632    #[test]
633    fn tables_match_spec_boundaries() {
634        assert_eq!(RANGE_LPS[0], [128, 176, 208, 240]);
635        assert_eq!(RANGE_LPS[63], [2, 2, 2, 2]);
636        assert_eq!(STATE_TRANS[0], [0, 1]);
637        assert_eq!(STATE_TRANS[63], [63, 63]);
638        assert_eq!(CTX_INIT[0][0], (20, -15));
639    }
640}