Skip to main content

mbtree_bench/
mbtree_bench.rs

1// Dev/test target, not shipped: a panic here IS the diagnostic (that is what an
2// assertion is). The workspace's unwrap/expect/panic denials exist to keep them
3// off the decoder's untrusted-input path, so they are relaxed for this file.
4#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
5// Dev tools also accumulate fields and helpers kept for the NEXT investigation;
6// dead code here is a scratchpad, not a defect.
7#![allow(dead_code, unused)]
8#![allow(clippy::unnecessary_unwrap, clippy::zombie_processes)]
9//! mb-tree lookahead cost/benefit instrument: encode the same clip with the
10//! lookahead OFF and ON, reporting each arm's wall (best-of-N) and an FNV hash
11//! of the bitstream.
12//!
13//! The hash is the correctness gate for lookahead *speed* work: a faster cost
14//! kernel that yields the same SATD values must leave the QP map — and therefore
15//! the bitstream — byte-identical. The wall ratio is the overhead mb-tree is
16//! asking us to pay for its BD win.
17//!
18//!   mbtree_bench <clip.y4m>     env: MB_FRAMES (48) MB_QP (27) MB_GOP (30) MB_REPS (3)
19//!                                    MB_LA (full|hybrid|half — RFF_MBTREE_LA also works)
20
21use rusty_h264_common::types::YuvFrame;
22use rusty_h264_encoder::{Encoder, EncoderConfig, Preset};
23
24fn fnv1a(d: &[u8]) -> u64 {
25    let mut h = 0xcbf29ce484222325u64;
26    for &b in d {
27        h ^= b as u64;
28        h = h.wrapping_mul(0x100000001b3);
29    }
30    h
31}
32
33fn read_y4m(path: &str, max: usize) -> (usize, usize, Vec<YuvFrame>) {
34    let raw = std::fs::read(path).expect("read clip");
35    let e = raw.iter().position(|&b| b == b'\n').expect("y4m header");
36    let hdr = std::str::from_utf8(&raw[..e]).expect("utf8 header");
37    let (mut w, mut h) = (0usize, 0usize);
38    for t in hdr.split_whitespace() {
39        match t.as_bytes().first() {
40            Some(b'W') => w = t[1..].parse().expect("W"),
41            Some(b'H') => h = t[1..].parse().expect("H"),
42            _ => {}
43        }
44    }
45    let (ys, cs) = (w * h, (w / 2) * (h / 2));
46    let (mut f, mut p) = (Vec::new(), e + 1);
47    while f.len() < max {
48        let Some(r) = raw[p..].iter().position(|&b| b == b'\n') else { break };
49        p += r + 1;
50        if p + ys + 2 * cs > raw.len() {
51            break;
52        }
53        f.push(YuvFrame {
54            width: w,
55            height: h,
56            y: raw[p..p + ys].to_vec(),
57            u: raw[p + ys..p + ys + cs].to_vec(),
58            v: raw[p + ys + cs..p + ys + 2 * cs].to_vec(),
59        });
60        p += ys + 2 * cs;
61    }
62    (w, h, f)
63}
64
65fn main() {
66    let path = std::env::args().nth(1).expect("usage: mbtree_bench <clip.y4m>");
67    let env = |k: &str, d: usize| -> usize {
68        std::env::var(k).ok().and_then(|v| v.parse().ok()).unwrap_or(d)
69    };
70    let (n, qp, gop, reps) = (env("MB_FRAMES", 48), env("MB_QP", 27), env("MB_GOP", 30), env("MB_REPS", 3));
71    if let Ok(la) = std::env::var("MB_LA") {
72        std::env::set_var("RFF_MBTREE_LA", la);
73    }
74    let (w, h, frames) = read_y4m(&path, n);
75    println!("mbtree_bench {}x{} x{} qp{qp} gop{gop} (best-of-{reps})", w, h, frames.len());
76
77    let mut off_ms = f64::MAX;
78    let mut on_ms = f64::MAX;
79    let (mut off_h, mut on_h, mut off_b, mut on_b) = (0u64, 0u64, 0usize, 0usize);
80    let mut calls = 0u64;
81    // PAIRED sampling (H-41/H-43). Alternating the arms is not enough: taking
82    // `min` over each arm INDEPENDENTLY draws the two minima from different
83    // moments, so drift never cancels and the overhead figure swings by more
84    // than the effect. Keep a per-round RATIO instead — both arms of a ratio
85    // are adjacent in time, so the pairing survives whatever the box is doing.
86    let mut ratios: Vec<f64> = Vec::with_capacity(reps);
87    for r in 0..reps {
88        let (mut r_off, mut r_on) = (0.0f64, 0.0f64);
89        // Swap which arm leads each round so a "second one is warmer" effect
90        // cancels across rounds rather than accumulating into the ratio.
91        let order: [bool; 2] = if r % 2 == 0 { [false, true] } else { [true, false] };
92        for on in order {
93            let mut cfg = EncoderConfig::new(w, h);
94            cfg.qp = qp as u8;
95            cfg.gop_size = gop as u32;
96            cfg.preset = Preset::Quality;
97            cfg.mbtree = on;
98            let enc = Encoder::new(cfg).expect("cfg");
99            rusty_h264_encoder::mbtree_satd_reset();
100            let t = std::time::Instant::now();
101            let out: Vec<u8> = enc.encode_all(&frames).expect("encode").concat();
102            let ms = t.elapsed().as_secs_f64() * 1e3;
103            if on {
104                on_ms = on_ms.min(ms);
105                on_h = fnv1a(&out);
106                on_b = out.len();
107                calls = rusty_h264_encoder::mbtree_satd_calls();
108                r_on = ms;
109            } else {
110                off_ms = off_ms.min(ms);
111                off_h = fnv1a(&out);
112                off_b = out.len();
113                r_off = ms;
114            }
115        }
116        ratios.push(r_on / r_off);
117    }
118    println!("  mbtree OFF: {off_ms:8.1} ms  {off_b:>8} bytes  hash {off_h:016x}");
119    println!("  mbtree ON : {on_ms:8.1} ms  {on_b:>8} bytes  hash {on_h:016x}");
120    println!(
121        "  lookahead work: {calls} candidate evals ({:.0}/MB/frame, DETERMINISTIC)",
122        calls as f64 / ((w / 16 * (h / 16)) as f64 * frames.len() as f64)
123    );
124    // The unpaired figure, kept only so old logs stay comparable — do not quote it.
125    println!(
126        "  lookahead overhead (UNPAIRED, min-of-N per arm — noisy, historical): {:+.1}%",
127        100.0 * (on_ms / off_ms - 1.0)
128    );
129    let mut sorted = ratios.clone();
130    sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite"));
131    let med = sorted[sorted.len() / 2];
132    let wins = ratios.iter().filter(|&&r| r < 1.0).count();
133    let n = ratios.len();
134    let z = (wins as f64 - n as f64 / 2.0) / (0.5 * (n as f64).sqrt());
135    print!("  per-round ON/OFF ratios:");
136    for r in &ratios {
137        print!(" {r:.3}");
138    }
139    println!();
140    println!(
141        "  lookahead overhead (PAIRED median): {:+.1}%   [ON faster in {wins}/{n}, z={z:+.2} {}]",
142        100.0 * (med - 1.0),
143        if z.abs() > 2.0 { "VERDICT" } else { "no directional verdict" }
144    );
145    println!("  size {:+.2}%  (deterministic)", 100.0 * (on_b as f64 / off_b as f64 - 1.0));
146}