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}