pub struct EncoderConfig {Show 33 fields
pub width: usize,
pub height: usize,
pub profile: Profile,
pub chroma: ChromaFormat,
pub level_idc: u8,
pub qp: u8,
pub gop_size: u32,
pub bitrate: u32,
pub framerate: f32,
pub num_ref_frames: u32,
pub preset: Preset,
pub tune_me_subpel_iter: bool,
pub tune_greedy_skip: bool,
pub tune_greedy_skip_min_free: Option<u32>,
pub tune_rd_skip: bool,
pub tune_rd_skip_min_free: Option<u32>,
pub tune_rd_skip_fast_t: Option<f64>,
pub bframes: u32,
pub bframe_qp_offset: i32,
pub aq_strength: f64,
pub bframes_adaptive: bool,
pub i_qp_offset: i32,
pub cabac: bool,
pub cabac_init_idc: u32,
pub cabac_lambda_scale: f64,
pub cabac_dz_div: i64,
pub cabac_rdoq: f64,
pub transform_8x8: bool,
pub sub_8x8: Option<bool>,
pub me_wide: Option<bool>,
pub mbtree: bool,
pub mbtree_strength: f64,
pub mbtree_lookahead: LookaheadMode,
/* private fields */
}Expand description
Configuration for an crate::Encoder.
Fields§
§width: usizePicture width in luma samples. Arbitrary (not restricted to /16).
height: usizePicture height in luma samples.
profile: ProfileTarget profile. Only Profile::ConstrainedBaseline is implemented.
chroma: ChromaFormatChroma format. Only ChromaFormat::Yuv420 is implemented.
level_idc: u8level_idc (e.g. 30 = level 3.0). Caller is responsible for choosing a
level that admits the resolution/bitrate; not yet validated.
qp: u8Quantization parameter (0..=51). With rate control off this is the fixed
QP for every frame; with it on, the base/fallback QP and pic_init_qp.
gop_size: u32Frames between IDR pictures. 1 = all-intra (every frame an IDR).
bitrate: u32Target bitrate in bits per second. 0 disables rate control (constant
QP); any positive value enables average-bitrate control, which varies the
per-frame QP around qp to converge on this rate.
framerate: f32Frame rate (frames per second), used by rate control to turn the bitrate target into a per-frame bit budget.
num_ref_frames: u32Number of reference frames the encoder may use for P-pictures (1..=16).
1 keeps the single-reference bitstream; higher values let P-macroblocks
pick an older reference (ref_idx), helping occlusion/periodic motion.
preset: PresetSpeed/quality trade-off. Defaults to Preset::Balanced.
tune_me_subpel_iter: boolWalk the sub-pel refinement until it stops improving, instead of a single
8-point pass per step. Independent of Self::tune_me_snap — measured
separately, because the two were first built coupled and the attribution
was ambiguous.
tune_greedy_skip: bool§tune_greedy_skip_min_free: Option<u32>Minimum online FREE-skip percentage for Self::tune_greedy_skip to
engage, dispatched on exactly the signal that gates RD skip
(Self::tune_rd_skip_min_free). The greedy skip wins on temporally
redundant content and LOSES on detailed content (BD-SSIM: akiyo -0.59,
FourPeople -0.32 vs foreman +1.23) — the same sign-flip, separated by the
same signal. None resolves to 85 — the calibrated default, at which the
corpus regression on foreman (+1.23% BD-SSIM, previously shipping) becomes
0.00 and nothing else regresses. Some(0) restores the old ungated
behaviour; Some(101) disables the greedy skip entirely.
tune_rd_skip: bool§tune_rd_skip_min_free: Option<u32>Minimum FREE-skip percentage, measured online over the frame so far, for
Self::tune_rd_skip to engage on the rest of that frame.
None resolves per preset, because the signal’s SCALE is preset-dependent:
sub-pel refinement predicts better, so it lifts the free-skip rate on ALL
content and the same absolute bar starts admitting content that loses.
Fast (no sub-pel) calibrates to 60; the sub-pel presets need 90. Each is
the smallest bar at which no corpus clip regresses on BD-PSNR or BD-SSIM.
Some(0) forces RD skip on everywhere (which LOSES badly on detailed
content); Some(101) disables it.
tune_rd_skip_fast_t: Option<f64>Skip-gate on the null arm’s cost, in units of lambda: when
SSD(skip) <= lambda * T the skip is taken WITHOUT trial-encoding the
coded arm at all.
The RD skip decision has to encode the coded arm to price it, and 55-80%
of the time it then throws that encode away — the null arm wins. This is
the standard search-skip gate over that: it trades a small number of
decisions (the RD comparison would occasionally have coded) for not
encoding at all. None/<= 0.0 disables it (every candidate is priced
exactly). Unlike the rest of the decision this is NOT byte-identical, so
it is BD-rate gated.
bframes: u32Number of B-frames between reference (I/P) anchors. 0 = no B-frames
(Constrained Baseline, byte-identical). >0 requires Main profile (B is
illegal in Baseline) and activates the reorder pipeline: anchors are coded
ahead of the B-frames that reference them (L0 past + L1 future), and B-
frames are non-reference. WORK IN PROGRESS — see the B-frame build plan.
bframe_qp_offset: i32QP offset applied to B-frames (added to qp). B-frames are
non-reference, so their coding error never propagates — quantizing them
harder (a positive offset) spends the saved bits on the reference anchors.
Only used when bframes > 0. Default 2.
aq_strength: f64Adaptive Quantization strength. Modulates the QP per macroblock by content:
flat/low-variance MBs (where blocking & banding are visible) get a FINER QP,
busy/high-variance MBs (where the eye masks error) a COARSER one — moving bits
to where they’re seen, a perceptual (SSIM) win at ~neutral PSNR. The QP shift
is relative to the FRAME’s mean log-variance (content-invariant), rate-
compensated, and its EFFECTIVE strength backs off automatically where the
log-variance spread is extreme (pathological synthetic content), so it never
regresses. Default 1.0 (on); 0.0 = off (uniform QP, byte-identical).
bframes_adaptive: boolContent-adaptive B-frame ENABLE. When set (with bframes > 0), the encoder
measures the clip’s temporal predictability (a cheap global-motion bi-
prediction residual) and codes B-frames ONLY when they’ll help — smooth /
predictable motion, where bi-pred + spatial-direct are cheap. On busy content
it falls back to P-only, so B-frames never regress. Default false.
i_qp_offset: i32Per-GOP I-frame QP cascade — the BASE offset for the classic ip_ratio
(added to qp on each GOP’s I-frame; it’s the root reference for
its whole GOP, so coding it finer propagates quality GOP-wide). Default -3.
In the B-capable batch path this is CONTENT-ADAPTIVE per GOP: predictable
GOPs — where the I-frame dominates the GOP’s bits — deepen it up to 2 further
QP steps (calibrated: busy ≈ base, compressible ≈ base−2). 0 disables the
cascade entirely (byte-identical escape hatch). Constant-QP only.
cabac: boolCABAC entropy coding (PPS entropy_coding_mode_flag = 1, Main profile).
Codes ~5–17% smaller than CAVLC at matched quality (I- and P-slices; B-slice
CABAC pending). Default false (CAVLC — Constrained Baseline, unchanged).
cabac_init_idc: u32cabac_init_idc (0..2) — selects one of 3 context-initialization tables for
P/B slices (I-slices always use the I preset). The best table is
content-dependent; 0 is the default. Signalled in the P/B slice header.
cabac_lambda_scale: f64Multiplier on the mode-decision Lagrangian (√λ) in the CABAC P/B path only.
CABAC codes ~9% fewer bits than the CAVLC-flavoured rate estimate the mode
decision uses, so the rate term is slightly over-weighted; this retunes it.
Default 1.0 (unchanged). CAVLC path is never affected.
cabac_dz_div: i64Quantizer dead-zone divisor override for the CABAC path (F = 2^qbits/dz).
A smaller divisor (bigger F) keeps more near-threshold coefficients — cheaper
under CABAC’s context-coded residual than under CAVLC. 0 = use the standard
content-derived dead-zone (default, unchanged).
cabac_rdoq: f64CABAC trellis-quantization (RDOQ) strength. Each 4×4 residual coefficient is
RD-optimized (level vs level−1 minimizing SSD + λ·R_cabac, λ scaled by
this; ~8 calibrated). 0.0 = off. DEFAULT-ON for CABAC I-slices (frame-type
adaptive — P/B off, sparse residual gains ~0); CAVLC path always off.
transform_8x8: boolHigh-profile 8×8 transform (transform_8x8_mode_flag). When set, an intra
macroblock may use one 8×8 integer DCT per 8×8 block (I_8x8) instead of four
4×4s — a per-MB RD choice that wins on smooth / large-structure content.
Requires High profile (profile_idc 100). CAVLC only (our decoder has no CABAC
8×8). Default false.
sub_8x8: Option<bool>P_8x8 sub-partition motion: allow a P macroblock to split into four 8×8
partitions, each with its own motion vector (finer motion granularity on
complex / boundary motion). A per-MB RD choice vs 16×16/16×8/8×16, gated on the
heavy-16×16 motion-boundary signal. A NET WIN on real content (12-clip Derf
corpus: −0.23% mean BD, big wins on bus/mobile/flower; a rigorous 6-channel
discovery harvest proved no cheap gate beats default-on, oracle headroom only
0.18%), so it is DEFAULT-ON for the Quality preset. Quality-only. None =
follow the preset (ON for Quality); Some(b) forces it either way. (8×4/4×8/4×4
sub-shapes within an 8×8 are a further split, not yet built.)
me_wide: Option<bool>Adaptive WIDE motion search: on flat source blocks (where the gradient-descent
diamond stalls at a plateau and misses the true MV) cover the ±16 neighbourhood
with a grid search instead; busy blocks keep the fast diamond. A big win on
smooth/low-motion content (the diamond’s flat-surface failure), free on busy
content — content-adaptive (a per-frame coherence gate keeps it from regressing
even on pure pans), so it is DEFAULT-ON for the Quality preset. Quality-only.
None = follow the preset (ON for Quality); Some(b) forces it either way.
mbtree: boolMacroblock-tree lookahead adaptive QP (TEMPORAL AQ). A cheap forward pass over
each GOP’s source frames propagates future-reference importance backward along
motion vectors and lowers the QP of heavily-referenced macroblocks — investing
bits where they pay off across many later frames. The complement to the spatial
aq_strength. Per-GOP-centered (rate-preserving). Applies
only in the batch (encode_all) constant-QP path, where the GOP’s future frames
are available (a bframes > 0 encode uses the reorder pipeline and ignores it).
Default true since 0.5.0 (H-37) — the gate cleared and the architectural
blocker is gone: the streaming path now carries a one-GOP lookahead queue, so
encode() + flush is byte-identical to
encode_all(). Set false for zero added latency (one AU per encode call)
or for the pre-0.5.0 bytes. Evidence:
- BD: the 4-QP per-clip gate CLEARS with room to spare (akiyo −4.82%, foreman −3.13%, football −0.53%, bus −0.29%, mobile −0.24%, city_4cif +0.01% neutral) — the monotone non-regression bar, not a mean.
- Cost: content-INDEPENDENT at 16-21 candidate evaluations per macroblock per frame across that corpus (1.3× spread) ≈ 1-2% of a busy-clip encode. The per-clip “blowups” (+251%, +34%) were wall-clock artifacts of a drifting box.
COST OF THE DEFAULT: encode() now returns a whole GOP’s access units at once
(empty while the GOP fills), so end-to-end latency is up to gop_size frames
and flush() is required at end of stream. Batch callers
(encode_all) are unaffected — they already had the whole GOP.
mbtree_strength: f64mb-tree QP-offset strength: qp_offset = -strength · log2((intra+propagate)/intra).
Larger = more aggressive bit redistribution toward referenced MBs. Default 0.9.
mbtree_lookahead: LookaheadModeResolution the mb-tree lookahead motion search runs at (see LookaheadMode).
Default HalfRes — fastest (~4× the lookahead), a small
BD-rate cost on fine detail; use Hybrid to recover
full-res quality at ~1.7×. Only relevant when mbtree is on.
Implementations§
Source§impl EncoderConfig
impl EncoderConfig
Sourcepub fn new(width: usize, height: usize) -> Self
pub fn new(width: usize, height: usize) -> Self
A minimal all-intra Constrained Baseline configuration at the given size.
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
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 rusty_h264_encoder::bitacct::reset();
56 rusty_h264_encoder::bitacct::set_enabled(true);
57 let mut enc = Encoder::new(cfg).unwrap();
58 let mut total = 0usize;
59 for f in &frames {
60 total += enc.encode(f).len();
61 }
62 rusty_h264_encoder::bitacct::add_actual_bytes(total);
63 rusty_h264_encoder::bitacct::set_enabled(false);
64 let mbs = (w.div_ceil(16) * h.div_ceil(16) * frames.len()) as u64;
65 rusty_h264_encoder::bitacct::dump(
66 &format!("{name} {pname} qp{qp} x{} ({} bytes)", frames.len(), total),
67 mbs,
68 );
69 if std::env::var_os("BA_MVDTAB").is_some() {
70 rusty_h264_encoder::bitacct::dump_mvd_table();
71 }
72 }
73}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}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 // Alternate the arms so thermal drift hits both equally.
74 for _ in 0..reps {
75 for on in [false, true] {
76 let mut cfg = EncoderConfig::new(w, h);
77 cfg.qp = qp as u8;
78 cfg.gop_size = gop as u32;
79 cfg.preset = Preset::Quality;
80 cfg.mbtree = on;
81 let enc = Encoder::new(cfg).expect("cfg");
82 rusty_h264_encoder::mbtree_satd_reset();
83 let t = std::time::Instant::now();
84 let out: Vec<u8> = enc.encode_all(&frames).expect("encode").concat();
85 let ms = t.elapsed().as_secs_f64() * 1e3;
86 if on {
87 on_ms = on_ms.min(ms);
88 on_h = fnv1a(&out);
89 on_b = out.len();
90 calls = rusty_h264_encoder::mbtree_satd_calls();
91 } else {
92 off_ms = off_ms.min(ms);
93 off_h = fnv1a(&out);
94 off_b = out.len();
95 }
96 }
97 }
98 println!(" mbtree OFF: {off_ms:8.1} ms {off_b:>8} bytes hash {off_h:016x}");
99 println!(" mbtree ON : {on_ms:8.1} ms {on_b:>8} bytes hash {on_h:016x}");
100 println!(
101 " lookahead work: {calls} candidate evals ({:.0}/MB/frame, DETERMINISTIC)",
102 calls as f64 / ((w / 16 * (h / 16)) as f64 * frames.len() as f64)
103 );
104 println!(
105 " lookahead overhead: {:+.1}% size {:+.2}%",
106 100.0 * (on_ms / off_ms - 1.0),
107 100.0 * (on_b as f64 / off_b as f64 - 1.0)
108 );
109}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}Trait Implementations§
Source§impl Clone for EncoderConfig
impl Clone for EncoderConfig
Source§fn clone(&self) -> EncoderConfig
fn clone(&self) -> EncoderConfig
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more