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?
65fn encode(frames: &[YuvFrame], w: usize, h: usize, rd_skip: bool) -> (f64, usize) {
66 let mut cfg = EncoderConfig::new(w, h);
67 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
68 cfg.gop_size = 30;
69 cfg.tune_rd_skip = rd_skip;
70 cfg.tune_rd_skip_fast_t = std::env::var("RS_GATE").ok().and_then(|v| v.parse().ok());
71 let mut enc = Encoder::new(cfg).expect("encoder");
72 let t = std::time::Instant::now();
73 let mut bytes = 0usize;
74 for fr in frames {
75 bytes += enc.encode(fr).len();
76 }
77 (t.elapsed().as_secs_f64(), bytes)
78}More examples
39fn main() {
40 let path = std::env::args().nth(1).unwrap();
41 let n: usize = std::env::var("TB_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(240);
42 let gop: u32 = std::env::var("TB_GOP").ok().and_then(|v| v.parse().ok()).unwrap_or(30);
43 let (w, h, frames) = read_y4m(&path, n);
44 for (pn, preset) in [("balanced", Preset::Balanced), ("quality", Preset::Quality)] {
45 let mut cfg = EncoderConfig::new(w, h);
46 cfg.qp = 27; cfg.gop_size = gop; cfg.preset = preset;
47 // best-of-3 each arm
48 let mut seq_ms = f64::MAX; let mut seq_out = Vec::new();
49 for _ in 0..3 {
50 let mut enc = Encoder::new(cfg.clone()).unwrap();
51 let t = std::time::Instant::now();
52 let mut o = Vec::new();
53 for f in &frames { o.extend_from_slice(&enc.encode(f)); }
54 seq_ms = seq_ms.min(t.elapsed().as_secs_f64() * 1e3);
55 seq_out = o;
56 }
57 let mut par_ms = f64::MAX; let mut par_out = Vec::new();
58 for _ in 0..3 {
59 let enc = Encoder::new(cfg.clone()).unwrap();
60 let t = std::time::Instant::now();
61 let o: Vec<u8> = enc.encode_all(&frames).unwrap().concat();
62 par_ms = par_ms.min(t.elapsed().as_secs_f64() * 1e3);
63 par_out = o;
64 }
65 assert_eq!(seq_out, par_out, "seq != parallel — compression WOULD be compromised");
66 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);
67 }
68}32fn main() {
33 let a: Vec<String> = std::env::args().skip(1).collect();
34 let (w, h) = a[1].split_once('x').unwrap();
35 let (w, h): (usize, usize) = (w.parse().unwrap(), h.parse().unwrap());
36 let frames = load(&a[0], w, h, std::env::var("RS_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(60));
37 let preset = match std::env::var("RS_PRESET").unwrap_or_default().as_str() {
38 "fast" => Preset::Fast, "quality" => Preset::Quality, _ => Preset::Balanced,
39 };
40 let arm_off: u32 = std::env::var("RS_ARM_OFF").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
41 let arm_on: u32 = std::env::var("RS_ARM_ON").ok().and_then(|v| v.parse().ok()).unwrap_or(3);
42 let run = |on: bool| {
43 let m = if on { arm_on } else { arm_off };
44 let mut cfg = EncoderConfig::new(w, h);
45 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
46 cfg.gop_size = 30;
47 cfg.preset = preset;
48 cfg.tune_me_snap = m & 1 != 0;
49 cfg.tune_me_subpel_iter = m & 2 != 0;
50 let mut enc = Encoder::new(cfg).expect("enc");
51 let t = std::time::Instant::now();
52 let mut b = 0usize;
53 for f in &frames { b += enc.encode(f).len(); }
54 (t.elapsed().as_secs_f64(), b)
55 };
56 let mut best = [f64::MAX; 2];
57 let mut bytes = [0usize; 2];
58 for pass in 0..10 {
59 let arm = pass % 2;
60 let (t, b) = run(arm == 1);
61 if t < best[arm] { best[arm] = t; }
62 bytes[arm] = b;
63 }
64 let px = (w * h * frames.len()) as f64;
65 println!("arm {arm_off} -> {arm_on} — {} {w}x{h} {} frames {preset:?}", a[0], frames.len());
66 println!(" off : {:>7.1} ms {:>6.2} Mpx/s {:>9} bytes", best[0]*1e3, px/best[0]/1e6, bytes[0]);
67 println!(" on : {:>7.1} ms {:>6.2} Mpx/s {:>9} bytes", best[1]*1e3, px/best[1]/1e6, bytes[1]);
68 println!(" speed {:>6.3}x size {:>+6.2}%", best[0]/best[1],
69 100.0*(bytes[1] as f64/bytes[0] as f64 - 1.0));
70}39fn main() {
40 let a: Vec<String> = std::env::args().skip(1).collect();
41 let (w, h) = a[1].split_once('x').unwrap();
42 let (w, h): (usize, usize) = (w.parse().unwrap(), h.parse().unwrap());
43 let nf: usize = std::env::var("RS_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(20);
44 let frames = load(&a[0], w, h, nf);
45
46 let preset = match std::env::var("RS_PRESET").unwrap_or_default().as_str() {
47 "fast" => Preset::Fast,
48 "quality" => Preset::Quality,
49 _ => Preset::Balanced,
50 };
51 let mut cfg = EncoderConfig::new(w, h);
52 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
53 cfg.gop_size = 30;
54 cfg.preset = preset;
55 let mut enc = Encoder::new(cfg).expect("encoder");
56 for f in &frames {
57 let _ = enc.encode(f);
58 }
59
60 let p: Vec<u64> = rusty_h264_encoder::ME_PROBE
61 .iter()
62 .map(|c| c.load(std::sync::atomic::Ordering::Relaxed))
63 .collect();
64 let (n, ours, oracle, worse, evals) = (p[0].max(1), p[1], p[2], p[3], p[4]);
65 let (oracle_sp, worse_sp) = (p[5], p[6]);
66 println!("ME oracle — {} ({w}x{h}, {} frames, {preset:?})\n", a[0], frames.len());
67 println!(" searches {n}");
68 println!(" mean cost ours {:>10.1}", ours as f64 / n as f64);
69 println!(" mean cost exhaustive {:>10.1}", oracle as f64 / n as f64);
70 println!(" ---> we are {:>6.2}% above the achievable minimum",
71 100.0 * (ours as f64 - oracle as f64) / oracle as f64);
72 println!(" searches the oracle beat {:>9} ({:.1}%)", worse, 100.0 * worse as f64 / n as f64);
73 // the oracle's own 49x49 grid + 8 sub-pel probes are included in `evals`
74 println!("
75 + exhaustive SUB-PEL (all quarter-pel in +-3):");
76 println!(" mean cost exhaustive {:>10.1}", oracle_sp as f64 / n as f64);
77 println!(" ---> we are {:>6.2}% above the achievable minimum",
78 100.0 * (ours as f64 - oracle_sp as f64) / oracle_sp as f64);
79 println!(" searches it beat {:>9} ({:.1}%)", worse_sp, 100.0 * worse_sp as f64 / n as f64);
80 let oracle_evals = 0;
81 println!("\n cost() evals/search {:>8.1} (ours, oracle's {oracle_evals} excluded)",
82 evals as f64 / n as f64 - oracle_evals as f64);
83}52fn main() {
53 let path = std::env::args().nth(1).unwrap_or_else(|| "video-tests/clips/foreman_cif.y4m".into());
54 let n: usize = std::env::var("BA_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(24);
55 let qp: u8 = std::env::var("BA_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
56 let (w, h, frames) = read_y4m(&path, n);
57 let name = std::path::Path::new(&path).file_stem().unwrap().to_string_lossy().to_string();
58 for (pname, preset) in [("quality", Preset::Quality)] {
59 let mut cfg = EncoderConfig::new(w, h);
60 cfg.qp = qp;
61 cfg.gop_size = 60;
62 cfg.preset = preset;
63 // The accountant must be runnable in the SAME configuration whose gap is
64 // under investigation, or its split describes a different encoder than the
65 // one being measured. BA_BFRAMES / BA_REFS mirror the all-tools arm.
66 if let Ok(v) = std::env::var("BA_BFRAMES") {
67 if let Ok(b) = v.parse::<u32>() {
68 cfg.bframes = b;
69 cfg.bframes_adaptive = false;
70 }
71 }
72 if let Ok(v) = std::env::var("BA_REFS") {
73 if let Ok(r) = v.parse::<u32>() {
74 cfg.num_ref_frames = r;
75 }
76 }
77 let cfg_bframes = cfg.bframes;
78 rusty_h264_encoder::bitacct::reset();
79 rusty_h264_encoder::bitacct::set_enabled(true);
80 let mut enc = Encoder::new(cfg).unwrap();
81 // B-frames require the lookahead path (`encode_all`); the per-frame
82 // `encode` loop cannot reorder. Use whichever the config demands so the
83 // accountant can profile the B-carrying arm at all.
84 let total: usize = if cfg_bframes > 0 {
85 enc.encode_all(&frames).unwrap().iter().map(|n| n.len()).sum()
86 } else {
87 let mut t = 0usize;
88 for f in &frames {
89 t += enc.encode(f).len();
90 }
91 t
92 };
93 rusty_h264_encoder::bitacct::add_actual_bytes(total);
94 rusty_h264_encoder::bitacct::set_enabled(false);
95 let mbs = (w.div_ceil(16) * h.div_ceil(16) * frames.len()) as u64;
96 rusty_h264_encoder::bitacct::dump(
97 &format!("{name} {pname} qp{qp} x{} ({} bytes)", frames.len(), total),
98 mbs,
99 );
100 if std::env::var_os("RFF_BSTATS").is_some() {
101 rusty_h264_encoder::mb16::bstats::dump();
102 }
103 if std::env::var_os("BA_MVDTAB").is_some() {
104 rusty_h264_encoder::bitacct::dump_mvd_table();
105 }
106 }
107}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}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 zero or more Annex-B access units. IDR
placement follows the keyint model (gop_size ceiling, min_keyint,
scene cuts), each IDR prefixed with SPS/PPS.
With a lookahead feature active (mb-tree, on by default) frames buffer
until a window fills, so a call may return EMPTY bytes — call
flush at end of stream to drain the tail.
Examples found in repository?
65fn encode(frames: &[YuvFrame], w: usize, h: usize, rd_skip: bool) -> (f64, usize) {
66 let mut cfg = EncoderConfig::new(w, h);
67 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
68 cfg.gop_size = 30;
69 cfg.tune_rd_skip = rd_skip;
70 cfg.tune_rd_skip_fast_t = std::env::var("RS_GATE").ok().and_then(|v| v.parse().ok());
71 let mut enc = Encoder::new(cfg).expect("encoder");
72 let t = std::time::Instant::now();
73 let mut bytes = 0usize;
74 for fr in frames {
75 bytes += enc.encode(fr).len();
76 }
77 (t.elapsed().as_secs_f64(), bytes)
78}More examples
39fn main() {
40 let path = std::env::args().nth(1).unwrap();
41 let n: usize = std::env::var("TB_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(240);
42 let gop: u32 = std::env::var("TB_GOP").ok().and_then(|v| v.parse().ok()).unwrap_or(30);
43 let (w, h, frames) = read_y4m(&path, n);
44 for (pn, preset) in [("balanced", Preset::Balanced), ("quality", Preset::Quality)] {
45 let mut cfg = EncoderConfig::new(w, h);
46 cfg.qp = 27; cfg.gop_size = gop; cfg.preset = preset;
47 // best-of-3 each arm
48 let mut seq_ms = f64::MAX; let mut seq_out = Vec::new();
49 for _ in 0..3 {
50 let mut enc = Encoder::new(cfg.clone()).unwrap();
51 let t = std::time::Instant::now();
52 let mut o = Vec::new();
53 for f in &frames { o.extend_from_slice(&enc.encode(f)); }
54 seq_ms = seq_ms.min(t.elapsed().as_secs_f64() * 1e3);
55 seq_out = o;
56 }
57 let mut par_ms = f64::MAX; let mut par_out = Vec::new();
58 for _ in 0..3 {
59 let enc = Encoder::new(cfg.clone()).unwrap();
60 let t = std::time::Instant::now();
61 let o: Vec<u8> = enc.encode_all(&frames).unwrap().concat();
62 par_ms = par_ms.min(t.elapsed().as_secs_f64() * 1e3);
63 par_out = o;
64 }
65 assert_eq!(seq_out, par_out, "seq != parallel — compression WOULD be compromised");
66 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);
67 }
68}32fn main() {
33 let a: Vec<String> = std::env::args().skip(1).collect();
34 let (w, h) = a[1].split_once('x').unwrap();
35 let (w, h): (usize, usize) = (w.parse().unwrap(), h.parse().unwrap());
36 let frames = load(&a[0], w, h, std::env::var("RS_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(60));
37 let preset = match std::env::var("RS_PRESET").unwrap_or_default().as_str() {
38 "fast" => Preset::Fast, "quality" => Preset::Quality, _ => Preset::Balanced,
39 };
40 let arm_off: u32 = std::env::var("RS_ARM_OFF").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
41 let arm_on: u32 = std::env::var("RS_ARM_ON").ok().and_then(|v| v.parse().ok()).unwrap_or(3);
42 let run = |on: bool| {
43 let m = if on { arm_on } else { arm_off };
44 let mut cfg = EncoderConfig::new(w, h);
45 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
46 cfg.gop_size = 30;
47 cfg.preset = preset;
48 cfg.tune_me_snap = m & 1 != 0;
49 cfg.tune_me_subpel_iter = m & 2 != 0;
50 let mut enc = Encoder::new(cfg).expect("enc");
51 let t = std::time::Instant::now();
52 let mut b = 0usize;
53 for f in &frames { b += enc.encode(f).len(); }
54 (t.elapsed().as_secs_f64(), b)
55 };
56 let mut best = [f64::MAX; 2];
57 let mut bytes = [0usize; 2];
58 for pass in 0..10 {
59 let arm = pass % 2;
60 let (t, b) = run(arm == 1);
61 if t < best[arm] { best[arm] = t; }
62 bytes[arm] = b;
63 }
64 let px = (w * h * frames.len()) as f64;
65 println!("arm {arm_off} -> {arm_on} — {} {w}x{h} {} frames {preset:?}", a[0], frames.len());
66 println!(" off : {:>7.1} ms {:>6.2} Mpx/s {:>9} bytes", best[0]*1e3, px/best[0]/1e6, bytes[0]);
67 println!(" on : {:>7.1} ms {:>6.2} Mpx/s {:>9} bytes", best[1]*1e3, px/best[1]/1e6, bytes[1]);
68 println!(" speed {:>6.3}x size {:>+6.2}%", best[0]/best[1],
69 100.0*(bytes[1] as f64/bytes[0] as f64 - 1.0));
70}39fn main() {
40 let a: Vec<String> = std::env::args().skip(1).collect();
41 let (w, h) = a[1].split_once('x').unwrap();
42 let (w, h): (usize, usize) = (w.parse().unwrap(), h.parse().unwrap());
43 let nf: usize = std::env::var("RS_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(20);
44 let frames = load(&a[0], w, h, nf);
45
46 let preset = match std::env::var("RS_PRESET").unwrap_or_default().as_str() {
47 "fast" => Preset::Fast,
48 "quality" => Preset::Quality,
49 _ => Preset::Balanced,
50 };
51 let mut cfg = EncoderConfig::new(w, h);
52 cfg.qp = std::env::var("RS_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
53 cfg.gop_size = 30;
54 cfg.preset = preset;
55 let mut enc = Encoder::new(cfg).expect("encoder");
56 for f in &frames {
57 let _ = enc.encode(f);
58 }
59
60 let p: Vec<u64> = rusty_h264_encoder::ME_PROBE
61 .iter()
62 .map(|c| c.load(std::sync::atomic::Ordering::Relaxed))
63 .collect();
64 let (n, ours, oracle, worse, evals) = (p[0].max(1), p[1], p[2], p[3], p[4]);
65 let (oracle_sp, worse_sp) = (p[5], p[6]);
66 println!("ME oracle — {} ({w}x{h}, {} frames, {preset:?})\n", a[0], frames.len());
67 println!(" searches {n}");
68 println!(" mean cost ours {:>10.1}", ours as f64 / n as f64);
69 println!(" mean cost exhaustive {:>10.1}", oracle as f64 / n as f64);
70 println!(" ---> we are {:>6.2}% above the achievable minimum",
71 100.0 * (ours as f64 - oracle as f64) / oracle as f64);
72 println!(" searches the oracle beat {:>9} ({:.1}%)", worse, 100.0 * worse as f64 / n as f64);
73 // the oracle's own 49x49 grid + 8 sub-pel probes are included in `evals`
74 println!("
75 + exhaustive SUB-PEL (all quarter-pel in +-3):");
76 println!(" mean cost exhaustive {:>10.1}", oracle_sp as f64 / n as f64);
77 println!(" ---> we are {:>6.2}% above the achievable minimum",
78 100.0 * (ours as f64 - oracle_sp as f64) / oracle_sp as f64);
79 println!(" searches it beat {:>9} ({:.1}%)", worse_sp, 100.0 * worse_sp as f64 / n as f64);
80 let oracle_evals = 0;
81 println!("\n cost() evals/search {:>8.1} (ours, oracle's {oracle_evals} excluded)",
82 evals as f64 / n as f64 - oracle_evals as f64);
83}52fn main() {
53 let path = std::env::args().nth(1).unwrap_or_else(|| "video-tests/clips/foreman_cif.y4m".into());
54 let n: usize = std::env::var("BA_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(24);
55 let qp: u8 = std::env::var("BA_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
56 let (w, h, frames) = read_y4m(&path, n);
57 let name = std::path::Path::new(&path).file_stem().unwrap().to_string_lossy().to_string();
58 for (pname, preset) in [("quality", Preset::Quality)] {
59 let mut cfg = EncoderConfig::new(w, h);
60 cfg.qp = qp;
61 cfg.gop_size = 60;
62 cfg.preset = preset;
63 // The accountant must be runnable in the SAME configuration whose gap is
64 // under investigation, or its split describes a different encoder than the
65 // one being measured. BA_BFRAMES / BA_REFS mirror the all-tools arm.
66 if let Ok(v) = std::env::var("BA_BFRAMES") {
67 if let Ok(b) = v.parse::<u32>() {
68 cfg.bframes = b;
69 cfg.bframes_adaptive = false;
70 }
71 }
72 if let Ok(v) = std::env::var("BA_REFS") {
73 if let Ok(r) = v.parse::<u32>() {
74 cfg.num_ref_frames = r;
75 }
76 }
77 let cfg_bframes = cfg.bframes;
78 rusty_h264_encoder::bitacct::reset();
79 rusty_h264_encoder::bitacct::set_enabled(true);
80 let mut enc = Encoder::new(cfg).unwrap();
81 // B-frames require the lookahead path (`encode_all`); the per-frame
82 // `encode` loop cannot reorder. Use whichever the config demands so the
83 // accountant can profile the B-carrying arm at all.
84 let total: usize = if cfg_bframes > 0 {
85 enc.encode_all(&frames).unwrap().iter().map(|n| n.len()).sum()
86 } else {
87 let mut t = 0usize;
88 for f in &frames {
89 t += enc.encode(f).len();
90 }
91 t
92 };
93 rusty_h264_encoder::bitacct::add_actual_bytes(total);
94 rusty_h264_encoder::bitacct::set_enabled(false);
95 let mbs = (w.div_ceil(16) * h.div_ceil(16) * frames.len()) as u64;
96 rusty_h264_encoder::bitacct::dump(
97 &format!("{name} {pname} qp{qp} x{} ({} bytes)", frames.len(), total),
98 mbs,
99 );
100 if std::env::var_os("RFF_BSTATS").is_some() {
101 rusty_h264_encoder::mb16::bstats::dump();
102 }
103 if std::env::var_os("BA_MVDTAB").is_some() {
104 rusty_h264_encoder::bitacct::dump_mvd_table();
105 }
106 }
107}Sourcepub fn encode_planes(
&mut self,
frame: &YuvPlanes<'_>,
) -> Result<Vec<u8>, EncodeError>
pub fn encode_planes( &mut self, frame: &YuvPlanes<'_>, ) -> Result<Vec<u8>, EncodeError>
encode over a borrowed frame — the camera’s DMA
buffer, not a copy. A tight view feeds the coder directly; a padded one
is gathered into a reused scratch frame first. Byte-identical to
encode(&view.to_frame()).
Sourcepub fn encode_into(
&mut self,
frame: &YuvPlanes<'_>,
out: &mut [u8],
) -> Result<usize, EncodeError>
pub fn encode_into( &mut self, frame: &YuvPlanes<'_>, out: &mut [u8], ) -> Result<usize, EncodeError>
encode_planes into a caller-owned buffer — the
packetizer’s, on a chip — returning the access unit’s length.
An access unit that does not fit is
EncodeError::BufferTooSmall with the size it needed; the picture is
then lost (the encoder has already coded it and moved on) and the next
call still works, so size the buffer for the worst case: an IDR at low
QP can approach width * height * 3 / 2. Bytes are identical to
[encode_planes].
Sourcepub fn flush_into(&mut self, out: &mut [u8]) -> Result<usize, EncodeError>
pub fn flush_into(&mut self, out: &mut [u8]) -> Result<usize, EncodeError>
flush into a caller-owned buffer; see
encode_into for the buffer contract.
Sourcepub fn request_keyframe(&mut self)
pub fn request_keyframe(&mut self)
Make the next picture an IDR (a late joiner on the mesh needs one now, not at the next GOP boundary). Rate-control state, the frame counter and the scene-cut history survive; only the DPB restarts, as at any IDR. With a lookahead active the buffered frames are coded first and the IDR lands on the next frame submitted, in coding order.
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?
39fn main() {
40 let path = std::env::args().nth(1).unwrap();
41 let n: usize = std::env::var("TB_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(240);
42 let gop: u32 = std::env::var("TB_GOP").ok().and_then(|v| v.parse().ok()).unwrap_or(30);
43 let (w, h, frames) = read_y4m(&path, n);
44 for (pn, preset) in [("balanced", Preset::Balanced), ("quality", Preset::Quality)] {
45 let mut cfg = EncoderConfig::new(w, h);
46 cfg.qp = 27; cfg.gop_size = gop; cfg.preset = preset;
47 // best-of-3 each arm
48 let mut seq_ms = f64::MAX; let mut seq_out = Vec::new();
49 for _ in 0..3 {
50 let mut enc = Encoder::new(cfg.clone()).unwrap();
51 let t = std::time::Instant::now();
52 let mut o = Vec::new();
53 for f in &frames { o.extend_from_slice(&enc.encode(f)); }
54 seq_ms = seq_ms.min(t.elapsed().as_secs_f64() * 1e3);
55 seq_out = o;
56 }
57 let mut par_ms = f64::MAX; let mut par_out = Vec::new();
58 for _ in 0..3 {
59 let enc = Encoder::new(cfg.clone()).unwrap();
60 let t = std::time::Instant::now();
61 let o: Vec<u8> = enc.encode_all(&frames).unwrap().concat();
62 par_ms = par_ms.min(t.elapsed().as_secs_f64() * 1e3);
63 par_out = o;
64 }
65 assert_eq!(seq_out, par_out, "seq != parallel — compression WOULD be compromised");
66 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);
67 }
68}More examples
52fn main() {
53 let path = std::env::args().nth(1).unwrap_or_else(|| "video-tests/clips/foreman_cif.y4m".into());
54 let n: usize = std::env::var("BA_FRAMES").ok().and_then(|v| v.parse().ok()).unwrap_or(24);
55 let qp: u8 = std::env::var("BA_QP").ok().and_then(|v| v.parse().ok()).unwrap_or(27);
56 let (w, h, frames) = read_y4m(&path, n);
57 let name = std::path::Path::new(&path).file_stem().unwrap().to_string_lossy().to_string();
58 for (pname, preset) in [("quality", Preset::Quality)] {
59 let mut cfg = EncoderConfig::new(w, h);
60 cfg.qp = qp;
61 cfg.gop_size = 60;
62 cfg.preset = preset;
63 // The accountant must be runnable in the SAME configuration whose gap is
64 // under investigation, or its split describes a different encoder than the
65 // one being measured. BA_BFRAMES / BA_REFS mirror the all-tools arm.
66 if let Ok(v) = std::env::var("BA_BFRAMES") {
67 if let Ok(b) = v.parse::<u32>() {
68 cfg.bframes = b;
69 cfg.bframes_adaptive = false;
70 }
71 }
72 if let Ok(v) = std::env::var("BA_REFS") {
73 if let Ok(r) = v.parse::<u32>() {
74 cfg.num_ref_frames = r;
75 }
76 }
77 let cfg_bframes = cfg.bframes;
78 rusty_h264_encoder::bitacct::reset();
79 rusty_h264_encoder::bitacct::set_enabled(true);
80 let mut enc = Encoder::new(cfg).unwrap();
81 // B-frames require the lookahead path (`encode_all`); the per-frame
82 // `encode` loop cannot reorder. Use whichever the config demands so the
83 // accountant can profile the B-carrying arm at all.
84 let total: usize = if cfg_bframes > 0 {
85 enc.encode_all(&frames).unwrap().iter().map(|n| n.len()).sum()
86 } else {
87 let mut t = 0usize;
88 for f in &frames {
89 t += enc.encode(f).len();
90 }
91 t
92 };
93 rusty_h264_encoder::bitacct::add_actual_bytes(total);
94 rusty_h264_encoder::bitacct::set_enabled(false);
95 let mbs = (w.div_ceil(16) * h.div_ceil(16) * frames.len()) as u64;
96 rusty_h264_encoder::bitacct::dump(
97 &format!("{name} {pname} qp{qp} x{} ({} bytes)", frames.len(), total),
98 mbs,
99 );
100 if std::env::var_os("RFF_BSTATS").is_some() {
101 rusty_h264_encoder::mb16::bstats::dump();
102 }
103 if std::env::var_os("BA_MVDTAB").is_some() {
104 rusty_h264_encoder::bitacct::dump_mvd_table();
105 }
106 }
107}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}