Skip to main content

rusty_h264_encoder/
rc.rs

1//! Average-bitrate rate control with a look-ahead complexity model.
2//!
3//! A frame-level controller that varies the per-frame quantization parameter to
4//! converge the average bitrate on a target. It combines a **look-ahead
5//! complexity estimate** (a cheap pre-encode score of *this* frame — see
6//! [`crate::lookahead`]) with a **leaky-bucket buffer** (correct accumulated
7//! over/undershoot). Using the upcoming frame's own complexity, rather than a
8//! lagging average of past frames, spends bits where they are needed and keeps
9//! quality steadier across complexity changes. The decoder needs no cooperation:
10//! each frame's QP rides in its `slice_qp_delta`, which conformant decoders honour.
11//!
12//! The model rests on the observation that coded bits are roughly inversely
13//! proportional to the quantizer step `Qstep`, and proportional to a frame's
14//! complexity. So `k = bits · Qstep / complexity` is a slowly-varying constant we
15//! learn (per frame type), then invert against the look-ahead complexity to pick
16//! a QP for the frame's bit budget.
17
18/// H.264 quantizer step for a QP: `Qstep` doubles every 6 QP (spec §8.6.1).
19fn qstep(qp: f64) -> f64 {
20    0.625 * 2f64.powf(qp / 6.0)
21}
22
23/// Quantizer-curve compression (x264's `qcomp`): 0 = constant bits per frame
24/// (varying quality), 1 = constant quality (bits ∝ complexity). The default
25/// blend spends ~`complexity^qcomp` bits, smoothing quality across complexity
26/// changes without fully sacrificing rate efficiency.
27const QCOMP: f64 = 0.6;
28
29/// Frame-level average-bitrate controller.
30#[derive(Debug, Clone)]
31pub struct RateControl {
32    /// Channel drain per coded frame, `bitrate / framerate` (bits).
33    target_per_frame: f64,
34    /// Leaky-bucket capacity (bits); ~one second of output.
35    buffer_size: f64,
36    /// Current buffer occupancy (bits); steered toward half-full.
37    fullness: f64,
38    /// Base/fallback QP (the configured `qp`), used before the model calibrates.
39    base_qp: f64,
40    qp_min: f64,
41    qp_max: f64,
42    /// Learned `bits · Qstep / complexity` per frame type (`0` = uninitialized).
43    /// I- and P-frame complexity scores live in different domains (spatial vs
44    /// motion-compensated SATD), so their constants are tracked separately.
45    k_p: f64,
46    k_i: f64,
47    /// Smoothed look-ahead complexity per frame type, the reference point for
48    /// the complexity-proportional bit allocation (`0` = uninitialized).
49    avg_c_p: f64,
50    avg_c_i: f64,
51    /// Last QP actually used, to limit frame-to-frame swing.
52    last_qp: f64,
53}
54
55impl RateControl {
56    /// Builds a controller for `bitrate` bits/sec at `framerate` fps, with `qp`
57    /// as the base/fallback quality. QP is clamped to a sane window around it.
58    pub fn new(bitrate: u32, framerate: f32, qp: u8) -> Self {
59        let bitrate = bitrate as f64;
60        let framerate = (framerate as f64).max(1.0);
61        let target_per_frame = bitrate / framerate;
62        let buffer_size = bitrate.max(target_per_frame * 2.0); // ≥ ~1s, ≥ 2 frames
63        RateControl {
64            target_per_frame,
65            buffer_size,
66            fullness: buffer_size * 0.5,
67            base_qp: qp as f64,
68            qp_min: (qp as f64 - 18.0).max(10.0),
69            qp_max: (qp as f64 + 18.0).min(51.0),
70            k_p: 0.0,
71            k_i: 0.0,
72            avg_c_p: 0.0,
73            avg_c_i: 0.0,
74            last_qp: qp as f64,
75        }
76    }
77
78    /// Picks the QP for a frame given its look-ahead `complexity` score (`is_idr`
79    /// selects the I model and a quality bump, since every later frame predicts
80    /// from the IDR).
81    pub fn pick_qp(&self, is_idr: bool, complexity: f64) -> u8 {
82        // Buffer-adjusted bit budget: spend less when the bucket is filling,
83        // draining any accumulated deviation over roughly a buffer's worth.
84        let deviation = self.fullness - self.buffer_size * 0.5;
85        let frames_to_correct = (self.buffer_size / self.target_per_frame).max(4.0);
86        let buf_target =
87            (self.target_per_frame - deviation / frames_to_correct).max(self.target_per_frame * 0.2);
88
89        // Complexity-proportional allocation: a frame `r×` the average complexity
90        // gets `r^qcomp ×` the budget (clamped so one frame can't drain the
91        // buffer). This is what holds quality steady across complexity changes.
92        let avg = if is_idr { self.avg_c_i } else { self.avg_c_p };
93        let budget = if avg > 0.0 {
94            buf_target * (complexity / avg).clamp(0.25, 4.0).powf(QCOMP)
95        } else {
96            buf_target
97        };
98
99        let k = if is_idr { self.k_i } else { self.k_p };
100        let qp = if k <= 0.0 {
101            // Not calibrated yet: lean on the base QP, nudged by buffer state.
102            self.base_qp + deviation / self.buffer_size * 8.0 - if is_idr { 2.0 } else { 0.0 }
103        } else {
104            // Predict this frame's bits·Qstep from its look-ahead complexity, then
105            // invert against the budget: Qstep = (k · complexity) / budget.
106            4.0 + 6.0 * (k * complexity / budget).log2()
107        };
108
109        // Limit per-frame swing for stable quality, then clamp to the window. A
110        // wider swing than the reactive model: the look-ahead means the change is
111        // driven by real complexity, not lag, so let it track more aggressively.
112        qp.clamp(self.last_qp - 6.0, self.last_qp + 6.0)
113            .clamp(self.qp_min, self.qp_max)
114            .round() as u8
115    }
116
117    /// Feeds back the bits a frame actually cost at its chosen QP and complexity,
118    /// recalibrating `k = bits · Qstep / complexity` and the average complexity.
119    pub fn update(&mut self, is_idr: bool, bits: usize, qp: u8, complexity: f64) {
120        let k_new = bits as f64 * qstep(qp as f64) / complexity.max(1.0);
121        let ema = |old: f64, new: f64| if old <= 0.0 { new } else { 0.5 * old + 0.5 * new };
122        if is_idr {
123            self.k_i = ema(self.k_i, k_new);
124            self.avg_c_i = ema(self.avg_c_i, complexity);
125        } else {
126            self.k_p = ema(self.k_p, k_new);
127            self.avg_c_p = ema(self.avg_c_p, complexity);
128        }
129
130        // Leaky bucket: add what we coded, drain the channel allotment.
131        self.fullness =
132            (self.fullness + bits as f64 - self.target_per_frame).clamp(0.0, self.buffer_size);
133        self.last_qp = qp as f64;
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn qstep_doubles_every_six_qp() {
143        assert!((qstep(28.0) / qstep(22.0) - 2.0).abs() < 1e-9);
144    }
145
146    #[test]
147    fn raises_qp_when_overshooting() {
148        let mut rc = RateControl::new(1_000_000, 30.0, 26);
149        let c = 1.0e6; // fixed look-ahead complexity
150        let first = rc.pick_qp(true, c);
151        // Feed back frames far larger than the per-frame budget repeatedly.
152        for _ in 0..30 {
153            let qp = rc.pick_qp(false, c);
154            rc.update(false, (rc.target_per_frame as usize) * 4, qp, c);
155        }
156        assert!(rc.pick_qp(false, c) > first, "QP should climb to curb overshoot");
157    }
158
159    #[test]
160    fn lowers_qp_when_undershooting() {
161        let mut rc = RateControl::new(1_000_000, 30.0, 40);
162        let c = 1.0e6;
163        let start = rc.pick_qp(false, c);
164        for _ in 0..30 {
165            let qp = rc.pick_qp(false, c);
166            rc.update(false, (rc.target_per_frame as usize) / 8, qp, c);
167        }
168        assert!(rc.pick_qp(false, c) < start, "QP should fall to use the budget");
169    }
170
171    #[test]
172    fn complex_frame_not_given_more_quality_than_simple() {
173        // Once calibrated, with qcomp < 1 a more complex frame is coded at no
174        // higher quality (no lower QP) than a simpler one at the same budget.
175        let mut rc = RateControl::new(2_000_000, 30.0, 26);
176        for _ in 0..20 {
177            let qp = rc.pick_qp(false, 1.0e6);
178            rc.update(false, rc.target_per_frame as usize, qp, 1.0e6);
179        }
180        let simple = rc.pick_qp(false, 0.5e6);
181        let complex = rc.pick_qp(false, 4.0e6);
182        assert!(complex >= simple);
183    }
184}