pub struct EncoderConfig {Show 40 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_bskip_rd: Option<f64>,
pub tune_bskip_busy_pct: Option<usize>,
pub tune_bskip_dirwin_pct: Option<usize>,
pub tune_b_split: bool,
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 tune_lme_hi: Option<f64>,
pub tune_lme_tex_thresh: Option<i64>,
pub tune_lme_motion_thresh: Option<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_bskip_rd: Option<f64>RD B_Skip strength, in units of lambda. DEFAULT-ON at 48.0;
None/<=0 restores the previous exactly-free-only rule byte-identically.
A B macroblock is skipped when direct WON the mode decision and its
prediction distortion is under T*lambda — i.e. the residual is not worth
its bits. Our previous rule demanded the residual quantize to EXACTLY zero,
which reaches 93.5% of B macroblocks on akiyo and 34.5% on foreman (at or
ABOVE x264) but collapses to 7.8% on mobile where x264 still finds 27.4%.
The deficit is BUSY-CONTENT-ONLY, so this is DISPATCHED on the online
free-skip rate of the frame (Self::tune_bskip_busy_pct) rather than
applied as a flat constant — on content where we already out-skip x264 it
stays a byte-identical no-op.
4-QP per-clip BD at T=48 (worst clip 0.00): mobile -0.51 PSNR / -0.96 SSIM, foreman -0.30 / -0.34, bus -0.10 / -0.50, akiyo byte-identical.
tune_bskip_busy_pct: Option<usize>Engage Self::tune_bskip_rd only while the frame’s online FREE-skip rate
is below this percentage — the busy-content dispatch. Default 60.
tune_bskip_dirwin_pct: Option<usize>Minimum online DIRECT-WIN rate (percent of not-free B macroblocks where
direct won the mode decision) for Self::tune_bskip_rd to engage.
Default 10 — calibrated on the one corpus clip that regressed (football,
7.0%) against the lowest-rate winner (foreman, 14.1%).
tune_b_split: boolSearch B 16x8 / 8x16 partitions. x264 spends 13.5% of its B macroblocks there; we had none, which is why the B bucket kept reading as a CODING gap after every constant in it had been swept flat. DEFAULT ON: the 4-QP per-clip table wins on all seven clips and both metrics with no sign flip (BD-SSIM akiyo -0.17%, FourPeople -0.80%, tempete -1.66%, mobile -3.36%, foreman -3.49%, bus -4.56%, football -7.09%), so there is nothing to dispatch on – the win simply concentrates on busy/high-motion content. CABAC B path only; the CAVLC B path still emits 16x16 modes.
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.
tune_lme_hi: Option<f64>ME lambda scale used on NORMAL-texture content, dispatched by the frame’s
median source macroblock variance. None = no dispatch (always
Self::cabac_lambda_scale). DEFAULT None — the dispatch is OFF.
The texture gate works for what it was built for: it holds mobile (median MB
variance 1554) at the conservative value, byte-identically, where a flat 1.8
costs +0.42% BD-SSIM. But it does NOT clear the monotone bar, because bus
regresses at EVERY high value tried (1.4 +0.35, 1.6 +0.09, 1.8 +0.29 BD-PSNR
vs 1.25) and bus’s texture (454) sits BELOW football’s (583), which WANTS the
high value — so no threshold on this signal separates them. Kept as an
opt-in knob with the machinery intact; needs a second, motion-flavoured term
before it can be default-on.
tune_lme_tex_thresh: Option<i64>Median source MB variance at or above which the conservative
Self::cabac_lambda_scale is used instead of Self::tune_lme_hi.
Default 650. Measured medians: akiyo 61, foreman 219, city 300, bus 454,
football 583, tempete 746, mobile 1554. 650 sits between football (583,
which WANTS the high lambda) and tempete (746, which regresses on BD-SSIM at
it) — the third loser, found only after the first two were gated. An earlier
800 let tempete through by 54 points.
tune_lme_motion_thresh: Option<f64>Global-MC residual at or above which the conservative
Self::cabac_lambda_scale is used. Default 26.0 — measured residuals are
akiyo 1.5, foreman 9.6, city 12.4, mobile 19.5, football 24.8, bus 27.5;
Default 20.0. Measured residuals: akiyo 1.5, foreman 9.6, city 12.4,
mobile 19.5, football 24.8, bus 27.5. At 20 bus is clean (0.00 BD-PSNR /
-0.03 BD-SSIM); a looser 24-26 leaves bus slightly positive because the
per-frame residual straddles it. The cost of 20 is football’s win (-0.57 ->
-0.01): its per-frame residual also crosses 20, so it is held conservative.
Deliberate — protecting a regressor outranks capturing a win.
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
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}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