pub struct Encoder { /* private fields */ }Expand description
A Constrained Baseline H.264 encoder.
Implementations§
Source§impl Encoder
impl Encoder
Sourcepub fn new(cfg: EncoderConfig) -> Result<Self, EncodeError>
pub fn new(cfg: EncoderConfig) -> Result<Self, EncodeError>
Creates an encoder, validating that the configuration is within the implemented subset.
Examples found in repository?
57fn encode(frames: &[YuvFrame], w: usize, h: usize, rd_skip: bool) -> (f64, usize) {
58 let mut cfg = EncoderConfig::new(w, h);
59 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
60 cfg.gop_size = 30;
61 cfg.tune_rd_skip = rd_skip;
62 cfg.tune_rd_skip_fast_t = std::env::var("RS_GATE").ok().and_then(|v| v.parse().ok());
63 let mut enc = Encoder::new(cfg).expect("encoder");
64 let t = std::time::Instant::now();
65 let mut bytes = 0usize;
66 for fr in frames {
67 bytes += enc.encode(fr).len();
68 }
69 (t.elapsed().as_secs_f64(), bytes)
70}More examples
31fn main() {
32 let path = std::env::args().nth(1).unwrap();
33 let n: usize = std::env::var("TB_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(240);
34 let gop: u32 = std::env::var("TB_GOP").ok().and_then(|v| v.parse().ok()).unwrap_or(30);
35 let (w, h, frames) = read_y4m(&path, n);
36 for (pn, preset) in [("balanced", Preset::Balanced), ("quality", Preset::Quality)] {
37 let mut cfg = EncoderConfig::new(w, h);
38 cfg.qp = 27; cfg.gop_size = gop; cfg.preset = preset;
39 // best-of-3 each arm
40 let mut seq_ms = f64::MAX; let mut seq_out = Vec::new();
41 for _ in 0..3 {
42 let mut enc = Encoder::new(cfg.clone()).unwrap();
43 let t = std::time::Instant::now();
44 let mut o = Vec::new();
45 for f in &frames { o.extend_from_slice(&enc.encode(f)); }
46 seq_ms = seq_ms.min(t.elapsed().as_secs_f64() * 1e3);
47 seq_out = o;
48 }
49 let mut par_ms = f64::MAX; let mut par_out = Vec::new();
50 for _ in 0..3 {
51 let enc = Encoder::new(cfg.clone()).unwrap();
52 let t = std::time::Instant::now();
53 let o: Vec<u8> = enc.encode_all(&frames).unwrap().concat();
54 par_ms = par_ms.min(t.elapsed().as_secs_f64() * 1e3);
55 par_out = o;
56 }
57 assert_eq!(seq_out, par_out, "seq != parallel — compression WOULD be compromised");
58 println!("{pn:<9} x{} gop{gop}: seq {seq_ms:.0} ms parallel {par_ms:.0} ms speedup {:.2}x (byte-identical ✓)", frames.len(), seq_ms / par_ms);
59 }
60}24fn main() {
25 let a: Vec<String> = std::env::args().skip(1).collect();
26 let (w, h) = a[1].split_once('x').unwrap();
27 let (w, h): (usize, usize) = (w.parse().unwrap(), h.parse().unwrap());
28 let frames = load(&a[0], w, h, std::env::var("RS_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(60));
29 let preset = match std::env::var("RS_PRESET").unwrap_or_default().as_str() {
30 "fast" => Preset::Fast, "quality" => Preset::Quality, _ => Preset::Balanced,
31 };
32 let arm_off: u32 = std::env::var("RS_ARM_OFF").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
33 let arm_on: u32 = std::env::var("RS_ARM_ON").ok().and_then(|v| v.parse().ok()).unwrap_or(3);
34 let run = |on: bool| {
35 let m = if on { arm_on } else { arm_off };
36 let mut cfg = EncoderConfig::new(w, h);
37 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
38 cfg.gop_size = 30;
39 cfg.preset = preset;
40 cfg.tune_me_snap = m & 1 != 0;
41 cfg.tune_me_subpel_iter = m & 2 != 0;
42 let mut enc = Encoder::new(cfg).expect("enc");
43 let t = std::time::Instant::now();
44 let mut b = 0usize;
45 for f in &frames { b += enc.encode(f).len(); }
46 (t.elapsed().as_secs_f64(), b)
47 };
48 let mut best = [f64::MAX; 2];
49 let mut bytes = [0usize; 2];
50 for pass in 0..10 {
51 let arm = pass % 2;
52 let (t, b) = run(arm == 1);
53 if t < best[arm] { best[arm] = t; }
54 bytes[arm] = b;
55 }
56 let px = (w * h * frames.len()) as f64;
57 println!("arm {arm_off} -> {arm_on} — {} {w}x{h} {} frames {preset:?}", a[0], frames.len());
58 println!(" off : {:>7.1} ms {:>6.2} Mpx/s {:>9} bytes", best[0]*1e3, px/best[0]/1e6, bytes[0]);
59 println!(" on : {:>7.1} ms {:>6.2} Mpx/s {:>9} bytes", best[1]*1e3, px/best[1]/1e6, bytes[1]);
60 println!(" speed {:>6.3}x size {:>+6.2}%", best[0]/best[1],
61 100.0*(bytes[1] as f64/bytes[0] as f64 - 1.0));
62}31fn main() {
32 let a: Vec<String> = std::env::args().skip(1).collect();
33 let (w, h) = a[1].split_once('x').unwrap();
34 let (w, h): (usize, usize) = (w.parse().unwrap(), h.parse().unwrap());
35 let nf: usize = std::env::var("RS_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(20);
36 let frames = load(&a[0], w, h, nf);
37
38 let preset = match std::env::var("RS_PRESET").unwrap_or_default().as_str() {
39 "fast" => Preset::Fast,
40 "quality" => Preset::Quality,
41 _ => Preset::Balanced,
42 };
43 let mut cfg = EncoderConfig::new(w, h);
44 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
45 cfg.gop_size = 30;
46 cfg.preset = preset;
47 let mut enc = Encoder::new(cfg).expect("encoder");
48 for f in &frames {
49 let _ = enc.encode(f);
50 }
51
52 let p: Vec<u64> = rusty_h264_encoder::ME_PROBE
53 .iter()
54 .map(|c| c.load(std::sync::atomic::Ordering::Relaxed))
55 .collect();
56 let (n, ours, oracle, worse, evals) = (p[0].max(1), p[1], p[2], p[3], p[4]);
57 let (oracle_sp, worse_sp) = (p[5], p[6]);
58 println!("ME oracle — {} ({w}x{h}, {} frames, {preset:?})\n", a[0], frames.len());
59 println!(" searches {n}");
60 println!(" mean cost ours {:>10.1}", ours as f64 / n as f64);
61 println!(" mean cost exhaustive {:>10.1}", oracle as f64 / n as f64);
62 println!(" ---> we are {:>6.2}% above the achievable minimum",
63 100.0 * (ours as f64 - oracle as f64) / oracle as f64);
64 println!(" searches the oracle beat {:>9} ({:.1}%)", worse, 100.0 * worse as f64 / n as f64);
65 // the oracle's own 49x49 grid + 8 sub-pel probes are included in `evals`
66 println!("
67 + exhaustive SUB-PEL (all quarter-pel in +-3):");
68 println!(" mean cost exhaustive {:>10.1}", oracle_sp as f64 / n as f64);
69 println!(" ---> we are {:>6.2}% above the achievable minimum",
70 100.0 * (ours as f64 - oracle_sp as f64) / oracle_sp as f64);
71 println!(" searches it beat {:>9} ({:.1}%)", worse_sp, 100.0 * worse_sp as f64 / n as f64);
72 let oracle_evals = 0;
73 println!("\n cost() evals/search {:>8.1} (ours, oracle's {oracle_evals} excluded)",
74 evals as f64 / n as f64 - oracle_evals as f64);
75}44fn main() {
45 let path = std::env::args().nth(1).unwrap_or_else(|| "video-tests/clips/foreman_cif.y4m".into());
46 let n: usize = std::env::var("BA_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(24);
47 let qp: u8 = std::env::var("BA_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
48 let (w, h, frames) = read_y4m(&path, n);
49 let name = std::path::Path::new(&path).file_stem().unwrap().to_string_lossy().to_string();
50 for (pname, preset) in [("quality", Preset::Quality)] {
51 let mut cfg = EncoderConfig::new(w, h);
52 cfg.qp = qp;
53 cfg.gop_size = 60;
54 cfg.preset = preset;
55 // The accountant must be runnable in the SAME configuration whose gap is
56 // under investigation, or its split describes a different encoder than the
57 // one being measured. BA_BFRAMES / BA_REFS mirror the all-tools arm.
58 if let Ok(v) = std::env::var("BA_BFRAMES") {
59 if let Ok(b) = v.parse::<u32>() {
60 cfg.bframes = b;
61 cfg.bframes_adaptive = false;
62 }
63 }
64 if let Ok(v) = std::env::var("BA_REFS") {
65 if let Ok(r) = v.parse::<u32>() {
66 cfg.num_ref_frames = r;
67 }
68 }
69 let cfg_bframes = cfg.bframes;
70 rusty_h264_encoder::bitacct::reset();
71 rusty_h264_encoder::bitacct::set_enabled(true);
72 let mut enc = Encoder::new(cfg).unwrap();
73 // B-frames require the lookahead path (`encode_all`); the per-frame
74 // `encode` loop cannot reorder. Use whichever the config demands so the
75 // accountant can profile the B-carrying arm at all.
76 let total: usize = if cfg_bframes > 0 {
77 enc.encode_all(&frames).unwrap().iter().map(|n| n.len()).sum()
78 } else {
79 let mut t = 0usize;
80 for f in &frames {
81 t += enc.encode(f).len();
82 }
83 t
84 };
85 rusty_h264_encoder::bitacct::add_actual_bytes(total);
86 rusty_h264_encoder::bitacct::set_enabled(false);
87 let mbs = (w.div_ceil(16) * h.div_ceil(16) * frames.len()) as u64;
88 rusty_h264_encoder::bitacct::dump(
89 &format!("{name} {pname} qp{qp} x{} ({} bytes)", frames.len(), total),
90 mbs,
91 );
92 if std::env::var_os("RFF_BSTATS").is_some() {
93 rusty_h264_encoder::mb16::bstats::dump();
94 }
95 if std::env::var_os("BA_MVDTAB").is_some() {
96 rusty_h264_encoder::bitacct::dump_mvd_table();
97 }
98 }
99}57fn main() {
58 let path = std::env::args().nth(1).expect("usage: mbtree_bench <clip.y4m>");
59 let env = |k: &str, d: usize| -> usize {
60 std::env::var(k).ok().and_then(|v| v.parse().ok()).unwrap_or(d)
61 };
62 let (n, qp, gop, reps) = (env("MB_FRAMES", 48), env("MB_QP", 27), env("MB_GOP", 30), env("MB_REPS", 3));
63 if let Ok(la) = std::env::var("MB_LA") {
64 std::env::set_var("RFF_MBTREE_LA", la);
65 }
66 let (w, h, frames) = read_y4m(&path, n);
67 println!("mbtree_bench {}x{} x{} qp{qp} gop{gop} (best-of-{reps})", w, h, frames.len());
68
69 let mut off_ms = f64::MAX;
70 let mut on_ms = f64::MAX;
71 let (mut off_h, mut on_h, mut off_b, mut on_b) = (0u64, 0u64, 0usize, 0usize);
72 let mut calls = 0u64;
73 // PAIRED sampling (H-41/H-43). Alternating the arms is not enough: taking
74 // `min` over each arm INDEPENDENTLY draws the two minima from different
75 // moments, so drift never cancels and the overhead figure swings by more
76 // than the effect. Keep a per-round RATIO instead — both arms of a ratio
77 // are adjacent in time, so the pairing survives whatever the box is doing.
78 let mut ratios: Vec<f64> = Vec::with_capacity(reps);
79 for r in 0..reps {
80 let (mut r_off, mut r_on) = (0.0f64, 0.0f64);
81 // Swap which arm leads each round so a "second one is warmer" effect
82 // cancels across rounds rather than accumulating into the ratio.
83 let order: [bool; 2] = if r % 2 == 0 { [false, true] } else { [true, false] };
84 for on in order {
85 let mut cfg = EncoderConfig::new(w, h);
86 cfg.qp = qp as u8;
87 cfg.gop_size = gop as u32;
88 cfg.preset = Preset::Quality;
89 cfg.mbtree = on;
90 let enc = Encoder::new(cfg).expect("cfg");
91 rusty_h264_encoder::mbtree_satd_reset();
92 let t = std::time::Instant::now();
93 let out: Vec<u8> = enc.encode_all(&frames).expect("encode").concat();
94 let ms = t.elapsed().as_secs_f64() * 1e3;
95 if on {
96 on_ms = on_ms.min(ms);
97 on_h = fnv1a(&out);
98 on_b = out.len();
99 calls = rusty_h264_encoder::mbtree_satd_calls();
100 r_on = ms;
101 } else {
102 off_ms = off_ms.min(ms);
103 off_h = fnv1a(&out);
104 off_b = out.len();
105 r_off = ms;
106 }
107 }
108 ratios.push(r_on / r_off);
109 }
110 println!(" mbtree OFF: {off_ms:8.1} ms {off_b:>8} bytes hash {off_h:016x}");
111 println!(" mbtree ON : {on_ms:8.1} ms {on_b:>8} bytes hash {on_h:016x}");
112 println!(
113 " lookahead work: {calls} candidate evals ({:.0}/MB/frame, DETERMINISTIC)",
114 calls as f64 / ((w / 16 * (h / 16)) as f64 * frames.len() as f64)
115 );
116 // The unpaired figure, kept only so old logs stay comparable — do not quote it.
117 println!(
118 " lookahead overhead (UNPAIRED, min-of-N per arm — noisy, historical): {:+.1}%",
119 100.0 * (on_ms / off_ms - 1.0)
120 );
121 let mut sorted = ratios.clone();
122 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite"));
123 let med = sorted[sorted.len() / 2];
124 let wins = ratios.iter().filter(|&&r| r < 1.0).count();
125 let n = ratios.len();
126 let z = (wins as f64 - n as f64 / 2.0) / (0.5 * (n as f64).sqrt());
127 print!(" per-round ON/OFF ratios:");
128 for r in &ratios {
129 print!(" {r:.3}");
130 }
131 println!();
132 println!(
133 " lookahead overhead (PAIRED median): {:+.1}% [ON faster in {wins}/{n}, z={z:+.2} {}]",
134 100.0 * (med - 1.0),
135 if z.abs() > 2.0 { "VERDICT" } else { "no directional verdict" }
136 );
137 println!(" size {:+.2}% (deterministic)", 100.0 * (on_b as f64 / off_b as f64 - 1.0));
138}Sourcepub fn config(&self) -> &EncoderConfig
pub fn config(&self) -> &EncoderConfig
The active configuration.
Sourcepub fn encode(&mut self, frame: &YuvFrame) -> Vec<u8> ⓘ
pub fn encode(&mut self, frame: &YuvFrame) -> Vec<u8> ⓘ
Encodes one frame, returning the Annex-B access unit. Every gop_size
frames (and always the first) is coded as an IDR, prefixed with SPS/PPS.
Generation 1 codes every picture as an IDR (all-intra); inter frames arrive with motion compensation later.
Examples found in repository?
57fn encode(frames: &[YuvFrame], w: usize, h: usize, rd_skip: bool) -> (f64, usize) {
58 let mut cfg = EncoderConfig::new(w, h);
59 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
60 cfg.gop_size = 30;
61 cfg.tune_rd_skip = rd_skip;
62 cfg.tune_rd_skip_fast_t = std::env::var("RS_GATE").ok().and_then(|v| v.parse().ok());
63 let mut enc = Encoder::new(cfg).expect("encoder");
64 let t = std::time::Instant::now();
65 let mut bytes = 0usize;
66 for fr in frames {
67 bytes += enc.encode(fr).len();
68 }
69 (t.elapsed().as_secs_f64(), bytes)
70}More examples
31fn main() {
32 let path = std::env::args().nth(1).unwrap();
33 let n: usize = std::env::var("TB_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(240);
34 let gop: u32 = std::env::var("TB_GOP").ok().and_then(|v| v.parse().ok()).unwrap_or(30);
35 let (w, h, frames) = read_y4m(&path, n);
36 for (pn, preset) in [("balanced", Preset::Balanced), ("quality", Preset::Quality)] {
37 let mut cfg = EncoderConfig::new(w, h);
38 cfg.qp = 27; cfg.gop_size = gop; cfg.preset = preset;
39 // best-of-3 each arm
40 let mut seq_ms = f64::MAX; let mut seq_out = Vec::new();
41 for _ in 0..3 {
42 let mut enc = Encoder::new(cfg.clone()).unwrap();
43 let t = std::time::Instant::now();
44 let mut o = Vec::new();
45 for f in &frames { o.extend_from_slice(&enc.encode(f)); }
46 seq_ms = seq_ms.min(t.elapsed().as_secs_f64() * 1e3);
47 seq_out = o;
48 }
49 let mut par_ms = f64::MAX; let mut par_out = Vec::new();
50 for _ in 0..3 {
51 let enc = Encoder::new(cfg.clone()).unwrap();
52 let t = std::time::Instant::now();
53 let o: Vec<u8> = enc.encode_all(&frames).unwrap().concat();
54 par_ms = par_ms.min(t.elapsed().as_secs_f64() * 1e3);
55 par_out = o;
56 }
57 assert_eq!(seq_out, par_out, "seq != parallel — compression WOULD be compromised");
58 println!("{pn:<9} x{} gop{gop}: seq {seq_ms:.0} ms parallel {par_ms:.0} ms speedup {:.2}x (byte-identical ✓)", frames.len(), seq_ms / par_ms);
59 }
60}24fn main() {
25 let a: Vec<String> = std::env::args().skip(1).collect();
26 let (w, h) = a[1].split_once('x').unwrap();
27 let (w, h): (usize, usize) = (w.parse().unwrap(), h.parse().unwrap());
28 let frames = load(&a[0], w, h, std::env::var("RS_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(60));
29 let preset = match std::env::var("RS_PRESET").unwrap_or_default().as_str() {
30 "fast" => Preset::Fast, "quality" => Preset::Quality, _ => Preset::Balanced,
31 };
32 let arm_off: u32 = std::env::var("RS_ARM_OFF").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
33 let arm_on: u32 = std::env::var("RS_ARM_ON").ok().and_then(|v| v.parse().ok()).unwrap_or(3);
34 let run = |on: bool| {
35 let m = if on { arm_on } else { arm_off };
36 let mut cfg = EncoderConfig::new(w, h);
37 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
38 cfg.gop_size = 30;
39 cfg.preset = preset;
40 cfg.tune_me_snap = m & 1 != 0;
41 cfg.tune_me_subpel_iter = m & 2 != 0;
42 let mut enc = Encoder::new(cfg).expect("enc");
43 let t = std::time::Instant::now();
44 let mut b = 0usize;
45 for f in &frames { b += enc.encode(f).len(); }
46 (t.elapsed().as_secs_f64(), b)
47 };
48 let mut best = [f64::MAX; 2];
49 let mut bytes = [0usize; 2];
50 for pass in 0..10 {
51 let arm = pass % 2;
52 let (t, b) = run(arm == 1);
53 if t < best[arm] { best[arm] = t; }
54 bytes[arm] = b;
55 }
56 let px = (w * h * frames.len()) as f64;
57 println!("arm {arm_off} -> {arm_on} — {} {w}x{h} {} frames {preset:?}", a[0], frames.len());
58 println!(" off : {:>7.1} ms {:>6.2} Mpx/s {:>9} bytes", best[0]*1e3, px/best[0]/1e6, bytes[0]);
59 println!(" on : {:>7.1} ms {:>6.2} Mpx/s {:>9} bytes", best[1]*1e3, px/best[1]/1e6, bytes[1]);
60 println!(" speed {:>6.3}x size {:>+6.2}%", best[0]/best[1],
61 100.0*(bytes[1] as f64/bytes[0] as f64 - 1.0));
62}31fn main() {
32 let a: Vec<String> = std::env::args().skip(1).collect();
33 let (w, h) = a[1].split_once('x').unwrap();
34 let (w, h): (usize, usize) = (w.parse().unwrap(), h.parse().unwrap());
35 let nf: usize = std::env::var("RS_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(20);
36 let frames = load(&a[0], w, h, nf);
37
38 let preset = match std::env::var("RS_PRESET").unwrap_or_default().as_str() {
39 "fast" => Preset::Fast,
40 "quality" => Preset::Quality,
41 _ => Preset::Balanced,
42 };
43 let mut cfg = EncoderConfig::new(w, h);
44 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
45 cfg.gop_size = 30;
46 cfg.preset = preset;
47 let mut enc = Encoder::new(cfg).expect("encoder");
48 for f in &frames {
49 let _ = enc.encode(f);
50 }
51
52 let p: Vec<u64> = rusty_h264_encoder::ME_PROBE
53 .iter()
54 .map(|c| c.load(std::sync::atomic::Ordering::Relaxed))
55 .collect();
56 let (n, ours, oracle, worse, evals) = (p[0].max(1), p[1], p[2], p[3], p[4]);
57 let (oracle_sp, worse_sp) = (p[5], p[6]);
58 println!("ME oracle — {} ({w}x{h}, {} frames, {preset:?})\n", a[0], frames.len());
59 println!(" searches {n}");
60 println!(" mean cost ours {:>10.1}", ours as f64 / n as f64);
61 println!(" mean cost exhaustive {:>10.1}", oracle as f64 / n as f64);
62 println!(" ---> we are {:>6.2}% above the achievable minimum",
63 100.0 * (ours as f64 - oracle as f64) / oracle as f64);
64 println!(" searches the oracle beat {:>9} ({:.1}%)", worse, 100.0 * worse as f64 / n as f64);
65 // the oracle's own 49x49 grid + 8 sub-pel probes are included in `evals`
66 println!("
67 + exhaustive SUB-PEL (all quarter-pel in +-3):");
68 println!(" mean cost exhaustive {:>10.1}", oracle_sp as f64 / n as f64);
69 println!(" ---> we are {:>6.2}% above the achievable minimum",
70 100.0 * (ours as f64 - oracle_sp as f64) / oracle_sp as f64);
71 println!(" searches it beat {:>9} ({:.1}%)", worse_sp, 100.0 * worse_sp as f64 / n as f64);
72 let oracle_evals = 0;
73 println!("\n cost() evals/search {:>8.1} (ours, oracle's {oracle_evals} excluded)",
74 evals as f64 / n as f64 - oracle_evals as f64);
75}44fn main() {
45 let path = std::env::args().nth(1).unwrap_or_else(|| "video-tests/clips/foreman_cif.y4m".into());
46 let n: usize = std::env::var("BA_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(24);
47 let qp: u8 = std::env::var("BA_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
48 let (w, h, frames) = read_y4m(&path, n);
49 let name = std::path::Path::new(&path).file_stem().unwrap().to_string_lossy().to_string();
50 for (pname, preset) in [("quality", Preset::Quality)] {
51 let mut cfg = EncoderConfig::new(w, h);
52 cfg.qp = qp;
53 cfg.gop_size = 60;
54 cfg.preset = preset;
55 // The accountant must be runnable in the SAME configuration whose gap is
56 // under investigation, or its split describes a different encoder than the
57 // one being measured. BA_BFRAMES / BA_REFS mirror the all-tools arm.
58 if let Ok(v) = std::env::var("BA_BFRAMES") {
59 if let Ok(b) = v.parse::<u32>() {
60 cfg.bframes = b;
61 cfg.bframes_adaptive = false;
62 }
63 }
64 if let Ok(v) = std::env::var("BA_REFS") {
65 if let Ok(r) = v.parse::<u32>() {
66 cfg.num_ref_frames = r;
67 }
68 }
69 let cfg_bframes = cfg.bframes;
70 rusty_h264_encoder::bitacct::reset();
71 rusty_h264_encoder::bitacct::set_enabled(true);
72 let mut enc = Encoder::new(cfg).unwrap();
73 // B-frames require the lookahead path (`encode_all`); the per-frame
74 // `encode` loop cannot reorder. Use whichever the config demands so the
75 // accountant can profile the B-carrying arm at all.
76 let total: usize = if cfg_bframes > 0 {
77 enc.encode_all(&frames).unwrap().iter().map(|n| n.len()).sum()
78 } else {
79 let mut t = 0usize;
80 for f in &frames {
81 t += enc.encode(f).len();
82 }
83 t
84 };
85 rusty_h264_encoder::bitacct::add_actual_bytes(total);
86 rusty_h264_encoder::bitacct::set_enabled(false);
87 let mbs = (w.div_ceil(16) * h.div_ceil(16) * frames.len()) as u64;
88 rusty_h264_encoder::bitacct::dump(
89 &format!("{name} {pname} qp{qp} x{} ({} bytes)", frames.len(), total),
90 mbs,
91 );
92 if std::env::var_os("RFF_BSTATS").is_some() {
93 rusty_h264_encoder::mb16::bstats::dump();
94 }
95 if std::env::var_os("BA_MVDTAB").is_some() {
96 rusty_h264_encoder::bitacct::dump_mvd_table();
97 }
98 }
99}Sourcepub fn try_encode(&mut self, frame: &YuvFrame) -> Result<Vec<u8>, EncodeError>
pub fn try_encode(&mut self, frame: &YuvFrame) -> Result<Vec<u8>, EncodeError>
Fallible encode.
With a lookahead feature active (currently mb-tree, on by default in the
constant-QP path) this BUFFERS: mb-tree needs a whole GOP of future frames
before it can assign any of their QPs, so the returned Vec is empty while
the GOP fills and then carries that entire GOP’s access units at once. The
concatenation of every return value plus flush is exactly
what encode_all produces — byte for byte.
You must call flush at end of stream or the final
partial GOP is never emitted. (A debug build asserts if the encoder is
dropped with frames still buffered.) For zero added latency set
cfg.mbtree = false, which restores one-AU-per-call behaviour.
Sourcepub fn flush(&mut self) -> Vec<u8> ⓘ
pub fn flush(&mut self) -> Vec<u8> ⓘ
Emits any frames still held by the lookahead queue (end of stream).
Returns the trailing access units, or empty when nothing is buffered — so it is always safe to call, including when no lookahead feature is active.
Sourcepub fn encode_all(
&self,
frames: &[YuvFrame],
) -> Result<Vec<Vec<u8>>, EncodeError>
pub fn encode_all( &self, frames: &[YuvFrame], ) -> Result<Vec<Vec<u8>>, EncodeError>
Batch-encodes every frame, returning one Annex-B access unit per frame.
At constant QP the GOPs are independent — each begins with an IDR that
resets the DPB, frame_num and POC, and SPS/PPS precede every IDR — so they
are encoded in parallel across CPU cores and the result is
byte-identical to calling encode frame-by-frame. With
rate control enabled the per-frame QP depends on history, so this falls back
to sequential encoding. Within a GOP, P-frames are inherently sequential
(each predicts from the previous reconstruction); the parallelism is across
GOPs, so it scales with the number of GOPs in the clip.
Examples found in repository?
31fn main() {
32 let path = std::env::args().nth(1).unwrap();
33 let n: usize = std::env::var("TB_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(240);
34 let gop: u32 = std::env::var("TB_GOP").ok().and_then(|v| v.parse().ok()).unwrap_or(30);
35 let (w, h, frames) = read_y4m(&path, n);
36 for (pn, preset) in [("balanced", Preset::Balanced), ("quality", Preset::Quality)] {
37 let mut cfg = EncoderConfig::new(w, h);
38 cfg.qp = 27; cfg.gop_size = gop; cfg.preset = preset;
39 // best-of-3 each arm
40 let mut seq_ms = f64::MAX; let mut seq_out = Vec::new();
41 for _ in 0..3 {
42 let mut enc = Encoder::new(cfg.clone()).unwrap();
43 let t = std::time::Instant::now();
44 let mut o = Vec::new();
45 for f in &frames { o.extend_from_slice(&enc.encode(f)); }
46 seq_ms = seq_ms.min(t.elapsed().as_secs_f64() * 1e3);
47 seq_out = o;
48 }
49 let mut par_ms = f64::MAX; let mut par_out = Vec::new();
50 for _ in 0..3 {
51 let enc = Encoder::new(cfg.clone()).unwrap();
52 let t = std::time::Instant::now();
53 let o: Vec<u8> = enc.encode_all(&frames).unwrap().concat();
54 par_ms = par_ms.min(t.elapsed().as_secs_f64() * 1e3);
55 par_out = o;
56 }
57 assert_eq!(seq_out, par_out, "seq != parallel — compression WOULD be compromised");
58 println!("{pn:<9} x{} gop{gop}: seq {seq_ms:.0} ms parallel {par_ms:.0} ms speedup {:.2}x (byte-identical ✓)", frames.len(), seq_ms / par_ms);
59 }
60}More examples
44fn main() {
45 let path = std::env::args().nth(1).unwrap_or_else(|| "video-tests/clips/foreman_cif.y4m".into());
46 let n: usize = std::env::var("BA_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(24);
47 let qp: u8 = std::env::var("BA_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
48 let (w, h, frames) = read_y4m(&path, n);
49 let name = std::path::Path::new(&path).file_stem().unwrap().to_string_lossy().to_string();
50 for (pname, preset) in [("quality", Preset::Quality)] {
51 let mut cfg = EncoderConfig::new(w, h);
52 cfg.qp = qp;
53 cfg.gop_size = 60;
54 cfg.preset = preset;
55 // The accountant must be runnable in the SAME configuration whose gap is
56 // under investigation, or its split describes a different encoder than the
57 // one being measured. BA_BFRAMES / BA_REFS mirror the all-tools arm.
58 if let Ok(v) = std::env::var("BA_BFRAMES") {
59 if let Ok(b) = v.parse::<u32>() {
60 cfg.bframes = b;
61 cfg.bframes_adaptive = false;
62 }
63 }
64 if let Ok(v) = std::env::var("BA_REFS") {
65 if let Ok(r) = v.parse::<u32>() {
66 cfg.num_ref_frames = r;
67 }
68 }
69 let cfg_bframes = cfg.bframes;
70 rusty_h264_encoder::bitacct::reset();
71 rusty_h264_encoder::bitacct::set_enabled(true);
72 let mut enc = Encoder::new(cfg).unwrap();
73 // B-frames require the lookahead path (`encode_all`); the per-frame
74 // `encode` loop cannot reorder. Use whichever the config demands so the
75 // accountant can profile the B-carrying arm at all.
76 let total: usize = if cfg_bframes > 0 {
77 enc.encode_all(&frames).unwrap().iter().map(|n| n.len()).sum()
78 } else {
79 let mut t = 0usize;
80 for f in &frames {
81 t += enc.encode(f).len();
82 }
83 t
84 };
85 rusty_h264_encoder::bitacct::add_actual_bytes(total);
86 rusty_h264_encoder::bitacct::set_enabled(false);
87 let mbs = (w.div_ceil(16) * h.div_ceil(16) * frames.len()) as u64;
88 rusty_h264_encoder::bitacct::dump(
89 &format!("{name} {pname} qp{qp} x{} ({} bytes)", frames.len(), total),
90 mbs,
91 );
92 if std::env::var_os("RFF_BSTATS").is_some() {
93 rusty_h264_encoder::mb16::bstats::dump();
94 }
95 if std::env::var_os("BA_MVDTAB").is_some() {
96 rusty_h264_encoder::bitacct::dump_mvd_table();
97 }
98 }
99}57fn main() {
58 let path = std::env::args().nth(1).expect("usage: mbtree_bench <clip.y4m>");
59 let env = |k: &str, d: usize| -> usize {
60 std::env::var(k).ok().and_then(|v| v.parse().ok()).unwrap_or(d)
61 };
62 let (n, qp, gop, reps) = (env("MB_FRAMES", 48), env("MB_QP", 27), env("MB_GOP", 30), env("MB_REPS", 3));
63 if let Ok(la) = std::env::var("MB_LA") {
64 std::env::set_var("RFF_MBTREE_LA", la);
65 }
66 let (w, h, frames) = read_y4m(&path, n);
67 println!("mbtree_bench {}x{} x{} qp{qp} gop{gop} (best-of-{reps})", w, h, frames.len());
68
69 let mut off_ms = f64::MAX;
70 let mut on_ms = f64::MAX;
71 let (mut off_h, mut on_h, mut off_b, mut on_b) = (0u64, 0u64, 0usize, 0usize);
72 let mut calls = 0u64;
73 // PAIRED sampling (H-41/H-43). Alternating the arms is not enough: taking
74 // `min` over each arm INDEPENDENTLY draws the two minima from different
75 // moments, so drift never cancels and the overhead figure swings by more
76 // than the effect. Keep a per-round RATIO instead — both arms of a ratio
77 // are adjacent in time, so the pairing survives whatever the box is doing.
78 let mut ratios: Vec<f64> = Vec::with_capacity(reps);
79 for r in 0..reps {
80 let (mut r_off, mut r_on) = (0.0f64, 0.0f64);
81 // Swap which arm leads each round so a "second one is warmer" effect
82 // cancels across rounds rather than accumulating into the ratio.
83 let order: [bool; 2] = if r % 2 == 0 { [false, true] } else { [true, false] };
84 for on in order {
85 let mut cfg = EncoderConfig::new(w, h);
86 cfg.qp = qp as u8;
87 cfg.gop_size = gop as u32;
88 cfg.preset = Preset::Quality;
89 cfg.mbtree = on;
90 let enc = Encoder::new(cfg).expect("cfg");
91 rusty_h264_encoder::mbtree_satd_reset();
92 let t = std::time::Instant::now();
93 let out: Vec<u8> = enc.encode_all(&frames).expect("encode").concat();
94 let ms = t.elapsed().as_secs_f64() * 1e3;
95 if on {
96 on_ms = on_ms.min(ms);
97 on_h = fnv1a(&out);
98 on_b = out.len();
99 calls = rusty_h264_encoder::mbtree_satd_calls();
100 r_on = ms;
101 } else {
102 off_ms = off_ms.min(ms);
103 off_h = fnv1a(&out);
104 off_b = out.len();
105 r_off = ms;
106 }
107 }
108 ratios.push(r_on / r_off);
109 }
110 println!(" mbtree OFF: {off_ms:8.1} ms {off_b:>8} bytes hash {off_h:016x}");
111 println!(" mbtree ON : {on_ms:8.1} ms {on_b:>8} bytes hash {on_h:016x}");
112 println!(
113 " lookahead work: {calls} candidate evals ({:.0}/MB/frame, DETERMINISTIC)",
114 calls as f64 / ((w / 16 * (h / 16)) as f64 * frames.len() as f64)
115 );
116 // The unpaired figure, kept only so old logs stay comparable — do not quote it.
117 println!(
118 " lookahead overhead (UNPAIRED, min-of-N per arm — noisy, historical): {:+.1}%",
119 100.0 * (on_ms / off_ms - 1.0)
120 );
121 let mut sorted = ratios.clone();
122 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite"));
123 let med = sorted[sorted.len() / 2];
124 let wins = ratios.iter().filter(|&&r| r < 1.0).count();
125 let n = ratios.len();
126 let z = (wins as f64 - n as f64 / 2.0) / (0.5 * (n as f64).sqrt());
127 print!(" per-round ON/OFF ratios:");
128 for r in &ratios {
129 print!(" {r:.3}");
130 }
131 println!();
132 println!(
133 " lookahead overhead (PAIRED median): {:+.1}% [ON faster in {wins}/{n}, z={z:+.2} {}]",
134 100.0 * (med - 1.0),
135 if z.abs() > 2.0 { "VERDICT" } else { "no directional verdict" }
136 );
137 println!(" size {:+.2}% (deterministic)", 100.0 * (on_b as f64 / off_b as f64 - 1.0));
138}