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