Skip to main content

rusty_h264_encoder/
bitacct.rs

1//! The BIT ACCOUNTANT — `codec-analyzer` instrument #6, the rate-domain twin of
2//! the stage profiler.
3//!
4//! The stage profiler buckets nanoseconds per stage; this buckets BITS per
5//! syntax element. A stage that is 5% of encode TIME can be 40% of the
6//! BITRATE, and the remaining ~4% BD-rate gap vs x264 veryfast is a rate
7//! question, so it needs the rate instrument.
8//!
9//! **Reconciliation is the whole design.** Buckets are deltas of the CABAC
10//! coder's exact emitted-bit position (`CabacEncoder::pos`), so accounted bits
11//! sum EXACTLY to the coded slice payload; `dump()` prints
12//! accounted-vs-actual and the residue. An accountant that cannot reconcile is
13//! measuring nothing (rav1e: 96.7% = working, 340% = broken).
14//!
15//! Observe-only and env-gated (`RFF_BITACCT=1`): when off, every tap is an
16//! atomic load of a `bool` and the encoder's output is byte-identical.
17//!
18//! Buckets mirror x264's own `i_mv_bits` / `i_tex_bits` / `i_misc_bits` split
19//! (so the comparison is like-for-like) but finer, because "misc" is where our
20//! suspected overhead would hide.
21
22use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
23
24/// Syntax-element buckets. Order is the dump order.
25#[derive(Clone, Copy)]
26pub enum B {
27    SkipFlag = 0,
28    MbType = 1,
29    RefIdx = 2,
30    Mvd = 3,
31    IntraBody = 4,
32    Cbp = 5,
33    QpDelta = 6,
34    ResidLuma = 7,
35    ResidChroma = 8,
36    Terminate = 9,
37    /// mvd's BYPASS tail (EG3 suffix + sign) — uncompressible by construction;
38    /// separating it says whether our motion bits are context-modelling or
39    /// simply LARGE VECTORS (which would point back at the search, not the coder).
40    MvdBypass = 10,
41    /// Intra MB residual, split out of the intra body so the texture line is exact.
42    IntraResid = 11,
43    /// mvd SIGN bits — one per NON-ZERO component (spec-mandated bypass).
44    MvdSign = 12,
45}
46
47pub const N: usize = 13;
48const NAMES: [&str; N] = [
49    "mb_skip_flag",
50    "mb_type/sub_type",
51    "ref_idx",
52    "mvd (MOTION)",
53    "intra MB body (I+P)",
54    "cbp",
55    "mb_qp_delta",
56    "residual luma (TEX)",
57    "residual chroma (TEX)",
58    "end_of_slice",
59    "  └ of which mvd bypass",
60    "intra residual (TEX)",
61    "  └ of which mvd SIGNS",
62];
63
64static ON: AtomicBool = AtomicBool::new(false);
65static BITS: [AtomicU64; N] = [const { AtomicU64::new(0) }; N];
66static COUNT: [AtomicU64; N] = [const { AtomicU64::new(0) }; N];
67/// Actual coded payload bits, for the reconciliation line.
68static ACTUAL: AtomicU64 = AtomicU64::new(0);
69
70#[inline]
71pub fn enabled() -> bool {
72    ON.load(Ordering::Relaxed)
73}
74
75/// Turn accounting on (from the harness) — never on by default.
76pub fn set_enabled(on: bool) {
77    ON.store(on, Ordering::Relaxed);
78}
79
80pub fn init_from_env() {
81    if std::env::var("RFF_BITACCT").map(|v| v != "0").unwrap_or(false) {
82        set_enabled(true);
83    }
84}
85
86#[inline]
87pub fn add(b: B, bits: u64) {
88    BITS[b as usize].fetch_add(bits, Ordering::Relaxed);
89    COUNT[b as usize].fetch_add(1, Ordering::Relaxed);
90}
91
92/// Record the real coded size of a finished slice payload (bytes → bits).
93pub fn add_actual_bytes(n: usize) {
94    ACTUAL.fetch_add(n as u64 * 8, Ordering::Relaxed);
95}
96
97pub fn reset() {
98    for i in 0..N {
99        BITS[i].store(0, Ordering::Relaxed);
100        COUNT[i].store(0, Ordering::Relaxed);
101    }
102    ACTUAL.store(0, Ordering::Relaxed);
103}
104
105/// The report: per-element bits, share, and the reconciliation against the real
106/// payload. `mbs` scales the per-MB column.
107pub fn dump(label: &str, mbs: u64) {
108    let vals: Vec<u64> = (0..N).map(|i| BITS[i].load(Ordering::Relaxed)).collect();
109    let cnts: Vec<u64> = (0..N).map(|i| COUNT[i].load(Ordering::Relaxed)).collect();
110    let accounted: u64 = vals.iter().sum();
111    let actual = ACTUAL.load(Ordering::Relaxed);
112    println!("\n=== BIT ACCOUNTANT — {label} ===");
113    println!(
114        "{:<24}{:>12}{:>9}{:>12}{:>12}",
115        "syntax element", "bits", "share", "bits/MB", "elements"
116    );
117    println!("{}", "-".repeat(69));
118    for i in 0..N {
119        if cnts[i] == 0 && vals[i] == 0 {
120            continue;
121        }
122        println!(
123            "{:<24}{:>12}{:>8.1}%{:>12.1}{:>12}",
124            NAMES[i],
125            vals[i],
126            100.0 * vals[i] as f64 / (accounted - vals[B::MvdBypass as usize] - vals[B::IntraResid as usize] - vals[B::MvdSign as usize]).max(1) as f64,
127            vals[i] as f64 / mbs.max(1) as f64,
128            cnts[i]
129        );
130    }
131    println!("{}", "-".repeat(69));
132    // x264-comparable rollup (its i_mv_bits / i_tex_bits / i_misc_bits split).
133    // `MvdBypass` and `IntraResid` are SUB-buckets (already inside Mvd /
134    // IntraBody), so they are excluded from the additive total and the shares.
135    let sub = vals[B::MvdBypass as usize] + vals[B::IntraResid as usize] + vals[B::MvdSign as usize];
136    let accounted = accounted - sub;
137    let mv = vals[B::Mvd as usize] + vals[B::RefIdx as usize];
138    let tex = vals[B::ResidLuma as usize] + vals[B::ResidChroma as usize] + vals[B::IntraResid as usize];
139    // x264's `i_mv_bits` is ALL non-residual MB syntax, so mirror that here.
140    let x264_syntax = accounted - tex - vals[B::Terminate as usize];
141    let misc = accounted - mv - tex;
142    let _ = misc;
143    let pc = |v: u64| 100.0 * v as f64 / accounted.max(1) as f64;
144    println!(
145        "x264-comparable:  NON-RESIDUAL SYNTAX {:.1}%  (of which mvd {:.1}%)   TEXTURE {:.1}%   hdr/term {:.1}%",
146        pc(x264_syntax),
147        pc(mv),
148        pc(tex),
149        pc(vals[B::Terminate as usize])
150    );
151    // THE line that makes this an instrument rather than a model.
152    println!(
153        "reconciliation:   accounted {accounted} / actual {actual} = {:.1}%  (residue {} bits = slice headers + NAL + flush)",
154        100.0 * accounted as f64 / actual.max(1) as f64,
155        actual as i64 - accounted as i64
156    );
157}
158
159// --- H-25: mvd TRUE-COST harvest -------------------------------------------
160// Average REAL CABAC bits per |mvd| component, from the production emitter.
161// Both ME cost models (Exp-Golomb step, x264's smooth curve) are analytic
162// guesses; this measures the actual adapted-context cost so the model can be
163// the TRUTH instead of a guess — the foreman fix candidate.
164pub const MVD_K: usize = 65; // |d| clamped to 64+
165static MVD_BITS: [AtomicU64; MVD_K] = [const { AtomicU64::new(0) }; MVD_K];
166static MVD_CNT: [AtomicU64; MVD_K] = [const { AtomicU64::new(0) }; MVD_K];
167
168#[inline]
169pub fn add_mvd_sample(abs_d: u32, bits: u64) {
170    let k = (abs_d as usize).min(MVD_K - 1);
171    MVD_BITS[k].fetch_add(bits, Ordering::Relaxed);
172    MVD_CNT[k].fetch_add(1, Ordering::Relaxed);
173}
174
175pub fn dump_mvd_table() {
176    println!("|d|,count,avg_bits");
177    for k in 0..MVD_K {
178        let (b, c) = (MVD_BITS[k].load(Ordering::Relaxed), MVD_CNT[k].load(Ordering::Relaxed));
179        if c > 0 {
180            println!("{k},{c},{:.3}", b as f64 / c as f64);
181        }
182    }
183}