collective

Browse / github.com/storytold/effectcraft / patch

av1enc: fix the bitrate overshoot of the bitrate mode rate control

av1enc: rate control overshoots the target bitrate by 20 % (12025 vs 10000 kbit/s) ## Observation (quality, not speed) In bitrate mode a target of 10000 kbit/s gave 12025 kbit/s (+20 %) over 60 frames on a synthetic pan. Found during the speed audit. ## To do Reproduce with the generator in `tests/common` over several contents and lengths (30-300 frames), find where the model overshoots (first key frame, inter frame prediction of bits per quantiser, no feedback from the actual size), and add a feedback term; the result must be checked against the bitrate window of the export. Related: the four-fold first-frame encoding (separate issue). Files: `crates/av1enc/src/lib.rs`. A

Status
open
Score
0.00
Adopters
0 other operators
Author
vangel76 (imported from pull request #2408, open)
Base
991904888a20b7da72b2a4880b21c7d912154736
Size
+159 −26 in 3 files
Review
pending
Shared
2026-10-28
Id
ba1e484b150e9572b468c07c

How it went

No other operator has reported using it yet.

Diff

raw · crates/av1enc/README.md crates/av1enc/src/lib.rs crates/av1enc/tests/it/roundtrip.rs

diff --git a/crates/av1enc/README.md b/crates/av1enc/README.md--- a/crates/av1enc/README.md+++ b/crates/av1enc/README.md@@ -48,8 +48,10 @@ reconstruction, so prediction never drifts.   reconstruction loop; full coefficient coding (all_zero, tx type, eob_pt, eob_extra,   coeff_base_eob, coeff_base, coeff_br, dc_sign, Golomb) with the exact contexts. - **Rate control**: `ConstantQ(base_q_idx)`; `Bitrate { kbps }` adapts `base_q_idx` per frame-  towards the average target (key frames get a lower quantizer and a larger share; the first-  key frame is searched in a few passes).+  towards the average target (key frames get a lower quantizer and a larger share of the budget of+  their group of `keyint` frames, the inter frames the rest; the first key frame is searched in a+  few passes, a later key frame that misses its budget by more than 19 % is coded again; the+  deviation from the budget is paid back over 16 frames).  ## Limits diff --git a/crates/av1enc/src/lib.rs b/crates/av1enc/src/lib.rs--- a/crates/av1enc/src/lib.rs+++ b/crates/av1enc/src/lib.rs@@ -24,6 +24,7 @@ use bitw::{BitWriter, obu}; use decide::{Decider, FrameGeom, RefFrame}; use loopfilter::{LfInfo, loop_filter}; use pred::Plane;+use tables::AC_QLOOKUP; use writer::TileWriter;  /// Rate control.@@ -136,10 +137,109 @@ struct RateState {     frame_bits: f64,     /// Quantizer index for inter frames (continuous).     q: f64,+    /// Key frame quantizer index minus the inter frame one.+    key_off: f64,+    /// Quantizer index steps per halving of the frame size, measured from consecutive inter+    /// frames (the size/quantizer curve is much steeper than the default on noisy content).+    slope: f64,+    /// Quantizer index and size (in bits) of the previous inter frame.+    prev_inter: Option<(f64, f64)>,     /// Accumulated (spent - budget) bits.     debt: f64, } +/// Quantizer index steps per halving of the frame size before any measurement.+const RC_SLOPE: f64 = 28.0;+/// Fraction of the quantizer step the size error asks for that is applied per frame.+const RC_GAIN: f64 = 0.8;+/// Frames over which an accumulated deviation from the budget is paid back.+const RC_PAYBACK: f64 = 16.0;++impl RateState {+    /// The nominal bit budget and the starting quantizer index of a key or inter frame. A key+    /// frame gets a larger share, and the inter frames of the group get the rest, so that a whole+    /// group of `keyint` frames adds up to `keyint * frame_bits`.+    fn frame_budget(&self, keyint: u32, key: bool) -> (f64, f64) {+        let keyint = keyint.max(1) as f64;+        let boost = (1.0 + (keyint.clamp(1.0, 60.0) - 1.0).sqrt() * 0.75).min(0.5 * keyint + 0.5);+        if key {+            (self.frame_bits * boost, (self.q + self.key_off).clamp(1.0, 255.0))+        } else if keyint > 1.0 {+            (self.frame_bits * (keyint - boost) / (keyint - 1.0), self.q)+        } else {+            (self.frame_bits, self.q)+        }+    }++    /// Adapts the quantizers to the size of the frame just coded at `q_used` against `target`.+    fn update(&mut self, key: bool, first: bool, q_used: f64, bits: f64, target: f64) {+        let err = (bits / target).log2().clamp(-1.5, 1.5);+        if first && key {+            // four passes ran: the key frame quantizer is known, the inter one follows from the offset+            self.q = (q_used - self.key_off).clamp(1.0, 255.0);+        } else if key {+            self.key_off = q_used - self.q + RC_GAIN * self.slope * err;+        } else {+            if let Some((pq, pbits)) = self.prev_inter {+                let (dq, dr) = (q_used - pq, (pbits / bits).log2());+                if dq.abs() >= 3.0 && dr.abs() >= 0.15 && dq * dr > 0.0 {+                    self.slope = (0.6 * self.slope + 0.4 * (dq / dr)).clamp(3.0, 45.0);+                }+            }+            self.prev_inter = Some((q_used, bits));+            self.q = (q_used + RC_GAIN * self.slope * err).clamp(1.0, 255.0);+        }+    }+}++/// A key frame after the first is coded again when its size is more than this many octaves off+/// the target (19 %).+const RC_KEY_TOLERANCE: f64 = 0.25;+/// Octaves of frame size per octave of quantizer step assumed for the first correction of a frame.+const RC_PROBE_SLOPE: f64 = 1.0;+/// Largest change of the quantizer step (octaves) between two passes on one frame.+const RC_PROBE_MAX_STEP: f64 = 2.0;++/// The quantizer index whose step size is closest to `2^x`.+fn q_for_step(bit_depth: u8, x: f64) -> u8 {+    let table = &AC_QLOOKUP[((bit_depth - 8) / 2) as usize];+    let mut best = 1;+    for q in 1..=255usize {+        if ((table[q] as f64).log2() - x).abs() < ((table[best] as f64).log2() - x).abs() {+            best = q;+        }+    }+    best as u8+}++/// The next quantizer to try for a frame, from the `(quantizer, log2(size / target))`+/// pairs coded so far. The size follows the quantizer step (not the index, which is linear in the+/// step only below about 60) in a near log-log line: false position between the closest pair of+/// passes that enclose the target, otherwise a step from the latest one with the slope of the+/// last two. `None` when no untried quantizer lies between the enclosing passes.+fn next_probe_q(bit_depth: u8, tried: &[(u8, f64)]) -> Option<u8> {+    let table = &AC_QLOOKUP[((bit_depth - 8) / 2) as usize];+    let x = |q: u8| (table[q as usize] as f64).log2();+    let over = tried.iter().filter(|t| t.1 > 0.0).max_by_key(|t| t.0);+    let under = tried.iter().filter(|t| t.1 <= 0.0).min_by_key(|t| t.0);+    if let (Some(&(q_lo, e_lo)), Some(&(q_hi, e_hi))) = (over, under) {+        if q_hi - q_lo < 2 {+            return None;+        }+        let next = x(q_lo) + (x(q_hi) - x(q_lo)) * e_lo / (e_lo - e_hi);+        return Some(q_for_step(bit_depth, next).clamp(q_lo + 1, q_hi - 1));+    }+    let &(q, err) = tried.last()?;+    let mut slope = RC_PROBE_SLOPE;+    if let [.., (q0, e0), (q1, e1)] = tried+        && (x(*q1) - x(*q0)).abs() > 0.05+        && (e0 - e1) / (x(*q1) - x(*q0)) > 0.0+    {+        slope = ((e0 - e1) / (x(*q1) - x(*q0))).clamp(0.25, 3.0);+    }+    Some(q_for_step(bit_depth, x(q) + (err / slope).clamp(-RC_PROBE_MAX_STEP, RC_PROBE_MAX_STEP)))+}+ /// AV1 encoder. Frames come out in input order (no hidden/reordered frames). pub struct Encoder {     cfg: EncoderConfig,@@ -185,7 +285,7 @@ impl Encoder {                 // initial guess from the bits per sample                 let bpp = frame_bits / (cfg.width as f64 * cfg.height as f64);                 let q = (110.0 - 40.0 * (bpp / 0.1).log2()).clamp(20.0, 230.0);-                Some(RateState { frame_bits, q, debt: 0.0 })+                Some(RateState { frame_bits, q, key_off: -24.0, slope: RC_SLOPE, prev_inter: None, debt: 0.0 })             }         };         let mut enc = Encoder { cfg, geom, level, seq_obu: Vec::new(), frame_num: 0, refr: None, rc };@@ -469,38 +569,52 @@ impl Encoder {      fn encode_rate_controlled(&mut self, src: &[Plane; 3], key: bool, rc: &mut RateState) -> (Vec<u8>, RefFrame) {         let first = self.frame_num == 0;-        let (frame_bits, debt, q_inter) = (rc.frame_bits, rc.debt, rc.q);-        // key frames get a lower quantizer and a larger share of the budget-        let key_boost = 1.0 + (self.cfg.keyint.clamp(1, 60) as f64 - 1.0).sqrt() * 0.75;-        let budget = if key { frame_bits * key_boost } else { frame_bits };-        // pay back a fraction of the accumulated deviation-        let target = (budget - debt * 0.25).max(budget * 0.25);-        let mut q = if key { q_inter - 24.0 } else { q_inter };-        let mut best: Option<(Vec<u8>, RefFrame, f64)> = None;-        let passes = if first { 4 } else { 1 };-        for pass in 0..passes {+        let (budget, q_start) = rc.frame_budget(self.cfg.keyint, key);+        // pay back the accumulated deviation over the next RC_PAYBACK frames+        let target = (budget - rc.debt / RC_PAYBACK).clamp(budget * 0.35, budget * 2.0);+        let mut q = q_start;+        let mut best: Option<(Vec<u8>, RefFrame, f64, f64)> = None;+        // The first frame is coded four times to find its quantizer. A later key frame is coded+        // again (up to twice) if its size is far off, since a key frame is a large part of the+        // budget of its group.+        let (max_passes, tolerance) = if first {+            (4, 0.0)+        } else if key {+            (3, RC_KEY_TOLERANCE)+        } else {+            (1, 0.0)+        };+        let mut tried: Vec<(u8, f64)> = Vec::new();+        for pass in 0..max_passes {             let qi = q.round().clamp(1.0, 255.0) as u8;+            if !first && tried.iter().any(|t| t.0 == qi) {+                break;+            }             let (obu, rf) = self.encode_frame(src, key, qi);             let bits = obu.len() as f64 * 8.0;             let err = (bits / target).log2();-            let better = best.as_ref().is_none_or(|b| err.abs() < (b.2 / target).log2().abs());-            if better {-                best = Some((obu, rf, bits));+            tried.push((qi, err));+            if best.as_ref().is_none_or(|b| err.abs() < (b.2 / target).log2().abs()) {+                best = Some((obu, rf, bits, qi as f64));             }-            if pass + 1 < passes {-                q = (q + 30.0 * err).clamp(1.0, 255.0);+            if err.abs() <= tolerance {+                break;+            }+            if first {+                if pass + 1 < max_passes {+                    q = (q + 30.0 * err).clamp(1.0, 255.0);+                }+            } else {+                match next_probe_q(self.cfg.bit_depth, &tried) {+                    Some(next) => q = next as f64,+                    None => break,+                }             }         }         // At least one pass ran, so `best` is set.-        let Some((obu, rf, bits)) = best else { return self.encode_frame(src, key, q.round().clamp(1.0, 255.0) as u8) };+        let Some((obu, rf, bits, q_used)) = best else { return self.encode_frame(src, key, q.round().clamp(1.0, 255.0) as u8) };         rc.debt += bits - budget;-        // adapt the inter quantizer: about 30 index steps per doubling of the rate-        let err = (bits / target).log2().clamp(-1.0, 1.0);-        if first && key {-            rc.q = (q + 24.0).clamp(1.0, 255.0);-        } else {-            rc.q = (rc.q + 12.0 * err).clamp(1.0, 255.0);-        }+        rc.update(key, first, q_used, bits, target);         (obu, rf)     } diff --git a/crates/av1enc/tests/it/roundtrip.rs b/crates/av1enc/tests/it/roundtrip.rs--- a/crates/av1enc/tests/it/roundtrip.rs+++ b/crates/av1enc/tests/it/roundtrip.rs@@ -80,6 +80,23 @@ fn bitrate_mode_tracks_target() {     assert!(kbps > 400.0 / 2.0 && kbps < 400.0 * 2.0, "{kbps} kbit/s"); } +/// Over several key frame intervals the average rate stays within 5 % of the target, for a+/// range of targets (the key frame budget is taken out of the inter frames of its group).+#[test]+fn bitrate_mode_is_accurate_over_many_groups() {+    let n = 180;+    for (kbps, keyint, bd) in [(40, 30, 8), (100, 30, 8), (200, 60, 8), (150, 20, 10)] {+        let pics: Vec<Pic> = (0..n).map(|t| synth(192, 128, bd, t)).collect();+        let mut c = cfg(192, 128, bd as u8, 0, keyint);+        c.rate = RateControl::Bitrate { kbps };+        let mut enc = effectcraft_av1enc::Encoder::new(c).unwrap();+        let bits: usize = pics.iter().map(|p| enc.encode(&p.frame()).data.len() * 8).sum();+        let got = bits as f64 / (n as f64 / 30.0) / 1000.0;+        eprintln!("{bd} bit, keyint {keyint}: {got:.1} kbit/s for {kbps}");+        assert!((got / kbps as f64 - 1.0).abs() < 0.05, "{got:.1} kbit/s for a {kbps} kbit/s target ({bd} bit, keyint {keyint})");+    }+}+ #[test] fn headers() {     let enc = effectcraft_av1enc::Encoder::new(cfg(1920, 1080, 10, 100, 60)).unwrap();