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}