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