pub struct EncoderConfig {Show 54 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 min_keyint: u32,
pub scenecut: u32,
pub lookahead: 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 b_pyramid: bool,
pub i_qp_offset: i32,
pub cabac: bool,
pub weightp: 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 tune_lme_q: Option<f64>,
pub cabac_dz_div: i64,
pub cabac_rdoq: f64,
pub cabac_rdoq_p: f64,
pub cabac_rdoq_b: f64,
pub tune_sub8x8_split: bool,
pub tune_sub8_rd: bool,
pub tune_intra_rd: bool,
pub tune_shape_rd: bool,
pub tune_rd_lambda_mb: bool,
pub transform_8x8: bool,
pub sub_8x8: Option<bool>,
pub me_wide: Option<bool>,
pub mbtree: bool,
pub mbtree_spread_min: f64,
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: u32MAXIMUM frames between IDR pictures (x264’s keyint). 1 = all-intra
(every frame an IDR). With scenecut active, IDRs
land at detected scene changes and this is only the forced-refresh
ceiling — exactly x264’s model (its default keyint is 250).
min_keyint: u32MINIMUM frames between IDR pictures (x264’s min-keyint, default 25):
a scene cut closer than this to the previous IDR does not spend one.
Ignored by the forced gop_size refresh and by all-intra.
scenecut: u32Scene-cut sensitivity (x264’s scenecut, default 40; 0 = OFF —
fixed-cadence IDRs, byte-identical to the pre-scenecut encoder). A cut
fires when the frame-pair inter/intra activity ratio reaches
1 - scenecut/100 (the x264 rule) — motion compensation recovering
less than scenecut% of the frame’s spatial energy means the content
changed, not moved.
lookahead: u32Lookahead window in frames (x264’s rc-lookahead, default 40): the
buffering bound for streaming mb-tree and the mb-tree window inside
long scenecut GOPs — a 250-frame GOP must not mean a 250-frame buffer.
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.
b_pyramid: boolB-pyramid (x264 parity — its default is normal): with 2+ B’s per
anchor gap, the MIDDLE B is coded first as a REFERENCE (nal_ref_idc 2, deblocked recon in the DPB, sliding-window marking) and the leaf
B’s bracket against it — halving the leaf prediction distance. v1 is
CABAC-path (the default); CAVLC B stays leaf-only.
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).
weightp: boolExplicit weighted prediction for P slices (x264 parity — its weightp
defaults on). Per-slice, per-reference LUMA (w, offset) at denom 6,
estimated by a fade detector (DC-ratio fit, SAD-gated); identity
weights everywhere the estimator finds no gain, so non-fade content
pays only the table’s few header bits per slice. Chroma is unweighted
(flag 0) — matching what x264’s own weightp streams carry.
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.
tune_lme_q: Option<f64>OPT-IN, BD-gate pending (Great Gate P1 machinery — docs/great-gate.md §6 P2):
the population-shaped PER-MB form of the Self::tune_lme_tex_thresh texture
veto. Some(q) routes each P frame’s top-q fraction of highest-variance MBs
to the conservative Self::cabac_lambda_scale individually (per-frame
percentile — the routed fraction is content-invariant by construction, where
the absolute median test cannot separate bus 454 from football 583, which want
opposite values). The motion veto stays frame-level; B slices keep the
frame-median form. None (default) = the frame-level veto exactly,
byte-identical. Env override for sweep arms: RFF_LME_Q.
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.
cabac_rdoq_p: f64Trellis (RDOQ) strength for CABAC P slices — SHIPPED as a CONTENT
DISPATCH (default 32.0), applied only where grain_signature() or
is_screen() fires; every other clip stays byte-identical to off. A
flat default is REFUTED (sign flips by content — see the default’s
comment below). The original structure-adaptive prediction (“a P is a
reference, expect wash-or-loss”) held for natural content and is
exactly why the gate exists. 0.0 = off everywhere.
cabac_rdoq_b: f64Trellis (RDOQ) strength for CABAC B slices — DEFAULT-ON
unconditionally (32.0; 6/6 clips win, zero losers — see the default’s
comment). Non-reference, so the structure-adaptive law says the trade
is clean (nothing depends on a B’s reconstruction). 0.0 = off.
tune_sub8x8_split: boolOPT-IN, BD-gate pending (Great Gate P3.3): search 8x4/4x8/4x4
sub-partitions inside P_8x8 (CABAC quality path, single-ref). The
decoder has parsed these since bring-up; the encoder emits them only
when this is set. false (default) = byte-identical. Env sweep arm:
RFF_SUB8X8_SPLIT=1. Default-on decision DEFERRED until the 4-wide
MC/cost kernels land (census #8 – the scalar fall-through would
double-charge the feature’s speed cost).
tune_sub8_rd: boolPROBE (Great Gate P3.3 gate): price the sub-8x8 SPLIT-vs-8x8 decision in
the RD currency (SSD_recon + lambda*bits, both arms fully planned)
instead of the SATD proxy. Only meaningful with
Self::tune_sub8x8_split. Costs two extra macroblock plans per split
candidate — a probe, not a shipping speed point.
tune_intra_rd: boolPROBE (Great Gate P3 RD-pricing #2): price the INTRA-vs-INTER decision
by SSD_recon + lambda*bits (both candidates planned for real) instead
of the SATD proxy plus the fitted Self::tune_intra_penalty. That
penalty is itself a correction for this proxy’s bias, so if the probe
wins the penalty should be re-swept (probably toward 0 — the P2 lambda
campaign already found 0 better on all three clips). Costs one extra
trial-encode plus one extra MB plan per coded macroblock.
tune_shape_rd: boolPROBE (Great Gate P3 RD-pricing #3): price the PARTITION SHAPE decision
(16x16 / 16x8 / 8x16 / P_8x8) by SSD_recon + lambda*bits instead of
the SATD proxy. The third SATD-priced default-on site. Costs one full
macroblock plan per candidate shape.
tune_rd_lambda_mb: boolPrice the inter RD trials with the macroblock’s ACTUAL quantizer rather
than the slice’s frame-level one. AQ (aq_strength, default 1.0) and
mb-tree rewrite QP per macroblock; a frame-level lambda misprices rate by
2^((qp_frame-qp_mb)/3) at every RD site, worst on the high-variance
macroblocks AQ moves furthest. false restores the frame-lambda form for
A/B (the arm must PIN the value, never rely on an absent override).
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_spread_min: f64Minimum dispersion of mb-tree’s own propagation offsets for it to apply
at all (the DIFFERENTIATION LATCH — see mbtree.rs). Below this the
offsets carry no information and are zeroed, which is byte-identical to
mb-tree off. 0.0 disables the latch (ungated, pre-gate behaviour).
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
42fn main() {
43 let mut args = std::env::args().skip(1);
44 let path = args.next().expect("clip");
45 let nframes: usize = args.next().and_then(|s| s.parse().ok()).unwrap_or(30);
46 let (w, h, frames) = read_y4m(&path, nframes);
47 let name = std::path::Path::new(&path).file_stem().unwrap().to_string_lossy().to_string();
48 let cfg = EncoderConfig::new(w, h);
49 let (bi, gmc, mg, dc, screen, grain) = rusty_h264_encoder::bframes_gate_signals(&cfg, &frames);
50 println!(
51 "{name:<24} bi={bi:>7.3} gmc={gmc:>8.3} mgain={mg:.3} dcfrac={dc:.3} screen={} grain={}",
52 screen as u8, grain as u8
53 );
54}44fn main() {
45 let mut args = std::env::args().skip(1);
46 let path = args.next().expect("clip");
47 let nframes: usize = args.next().and_then(|s| s.parse().ok()).unwrap_or(60);
48 let (w, h, frames) = read_y4m(&path, nframes);
49 let name = std::path::Path::new(&path).file_stem().unwrap().to_string_lossy().to_string();
50 let cfg = EncoderConfig::new(w, h); // defaults: scenecut 40 → threshold 0.60
51 let ratios = rusty_h264_encoder::scene_cut_ratios(&cfg, &frames);
52 let max = ratios.iter().cloned().fold(0.0f64, f64::max);
53 let thresh = 1.0 - cfg.scenecut as f64 / 100.0;
54 let flat = ratios.iter().filter(|&&r| r >= thresh).count();
55 // v2 spike rule: high AND a jump over the recent baseline (min of the two
56 // previous pair ratios) — a cut is a discontinuity, chaos is a plateau.
57 let mut spike = 0usize;
58 for i in 0..ratios.len() {
59 let base = match i {
60 0 => 1.0,
61 1 => ratios[0],
62 _ => ratios[i - 1].min(ratios[i - 2]),
63 };
64 if ratios[i] >= thresh && ratios[i] >= base + 0.25 {
65 spike += 1;
66 }
67 }
68 println!(
69 "{name:<24} pairs={} max_ratio={max:.3} flat@{thresh:.2}={flat} spike={spike}",
70 ratios.len()
71 );
72}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}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