use crate::encoder::EncoderConfig;
use crate::vp9::common::quant::{ac_quant, quantizer_to_qindex};
const BPER_MB_NORMBITS: u32 = 9;
const FRAME_OVERHEAD_BITS: i64 = 200;
const MIN_BPB_FACTOR: f64 = 0.005;
const MAX_BPB_FACTOR: f64 = 50.0;
const KF_BOOST: f64 = 10.0;
const BALANCE_DRAIN_DIVISOR: f64 = 30.0;
const CORRECTION_ADJ_MIN: f64 = 0.9;
const CORRECTION_ADJ_MAX: f64 = 1.1;
pub fn convert_qindex_to_q(qindex: i32) -> f64 {
ac_quant(qindex, 0) as f64 / 4.0
}
pub fn bits_per_mb(is_keyframe: bool, qindex: i32, correction_factor: f64) -> i64 {
let q = convert_qindex_to_q(qindex);
let mut enumerator: i64 = if is_keyframe { 2_700_000 } else { 1_800_000 };
enumerator += ((enumerator as f64 * q) as i64) >> 12;
(enumerator as f64 * correction_factor / q) as i64
}
pub fn estimate_bits_at_q(is_keyframe: bool, qindex: i32, mbs: i64, correction_factor: f64) -> i64 {
let bpm = bits_per_mb(is_keyframe, qindex, correction_factor);
let est = ((bpm.max(0) as u64 * mbs as u64) >> BPER_MB_NORMBITS) as i64;
est.max(FRAME_OVERHEAD_BITS)
}
#[derive(Clone, Debug)]
pub struct RateControl {
target_bps: f64,
fps: f64,
mbs: i64,
qmin: i32,
qmax: i32,
bits_balance: f64,
kf_correction: f64,
inter_correction: f64,
}
impl RateControl {
pub fn new(cfg: &EncoderConfig) -> Self {
let mut qmin = quantizer_to_qindex(cfg.min_quantizer as i32);
let mut qmax = quantizer_to_qindex(cfg.max_quantizer as i32);
if qmin > qmax {
std::mem::swap(&mut qmin, &mut qmax);
}
let mb_cols = (mi_cols(cfg.width) as i64 + 1) >> 1;
let mb_rows = (mi_rows(cfg.height) as i64 + 1) >> 1;
Self {
target_bps: cfg.bitrate_kbps as f64 * 1000.0,
fps: (cfg.framerate.max(1)) as f64,
mbs: (mb_cols * mb_rows).max(1),
qmin,
qmax,
bits_balance: 0.0,
kf_correction: 1.0,
inter_correction: 1.0,
}
}
pub fn qindex_window(&self) -> (i32, i32) {
(self.qmin, self.qmax)
}
fn avg_frame_bits(&self) -> f64 {
self.target_bps / self.fps
}
pub fn frame_target_bits(&self, is_keyframe: bool) -> i64 {
let avg = self.avg_frame_bits();
let target = if is_keyframe {
avg * KF_BOOST
} else {
let drain = (self.bits_balance / BALANCE_DRAIN_DIVISOR).clamp(-avg / 2.0, avg / 2.0);
avg + drain
};
target.max(FRAME_OVERHEAD_BITS as f64) as i64
}
pub fn select_qindex(&self, is_keyframe: bool, target_bits: i64) -> u8 {
let cf = self.correction(is_keyframe);
for q in self.qmin..=self.qmax {
if estimate_bits_at_q(is_keyframe, q, self.mbs, cf) <= target_bits {
return q as u8;
}
}
self.qmax as u8
}
pub fn update_after_encode(
&mut self,
is_keyframe: bool,
qindex: u8,
target_bits: i64,
actual_bits: i64,
) {
let cf = self.correction(is_keyframe);
let projected = estimate_bits_at_q(is_keyframe, qindex as i32, self.mbs, cf).max(1);
let adj =
(actual_bits as f64 / projected as f64).clamp(CORRECTION_ADJ_MIN, CORRECTION_ADJ_MAX);
let new_cf = (cf * adj).clamp(MIN_BPB_FACTOR, MAX_BPB_FACTOR);
if is_keyframe {
self.kf_correction = new_cf;
} else {
self.inter_correction = new_cf;
self.bits_balance += (target_bits - actual_bits) as f64;
}
}
pub fn update_bitrate_kbps(&mut self, kbps: u32) {
self.target_bps = kbps as f64 * 1000.0;
self.bits_balance *= 0.5;
}
fn correction(&self, is_keyframe: bool) -> f64 {
if is_keyframe {
self.kf_correction
} else {
self.inter_correction
}
}
}
fn mi_cols(width: u32) -> u32 {
(width + 7) >> 3
}
fn mi_rows(height: u32) -> u32 {
(height + 7) >> 3
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg(bitrate_kbps: u32) -> EncoderConfig {
EncoderConfig {
width: 640,
height: 480,
framerate: 30,
bitrate_kbps,
keyframe_interval: 150,
min_quantizer: 40,
max_quantizer: 60,
cpu_used: 7,
}
}
#[test]
fn qindex_window_from_min_max_q() {
let rc = RateControl::new(&cfg(500));
assert_eq!(rc.qindex_window(), (160, 240));
}
#[test]
fn mb_count_640x480() {
let rc = RateControl::new(&cfg(500));
assert_eq!(rc.mbs, 1200);
}
#[test]
fn bits_per_mb_decreases_with_qindex() {
let mut prev = i64::MAX;
for q in (0..=255).step_by(8) {
let b = bits_per_mb(false, q, 1.0);
assert!(b <= prev, "bpm not monotone at q{q}: {b} > {prev}");
prev = b;
}
}
#[test]
fn keyframe_costs_more_than_inter() {
assert!(bits_per_mb(true, 160, 1.0) > bits_per_mb(false, 160, 1.0));
}
#[test]
fn select_qindex_monotone_in_target() {
let rc = RateControl::new(&cfg(500));
let mut prev_q = 255u8;
for &target in &[1_000i64, 5_000, 20_000, 80_000, 400_000, 2_000_000] {
let q = rc.select_qindex(false, target);
assert!(
q <= prev_q,
"q not monotone: target {target} → q{q} > {prev_q}"
);
assert!((rc.qmin as u8..=rc.qmax as u8).contains(&q));
prev_q = q;
}
}
#[test]
fn select_qindex_clamps_to_window() {
let rc = RateControl::new(&cfg(500));
assert_eq!(rc.select_qindex(false, 1), rc.qmax as u8);
assert_eq!(rc.select_qindex(false, 1_000_000_000), rc.qmin as u8);
}
#[test]
fn keyframe_target_boosted() {
let rc = RateControl::new(&cfg(500));
let avg = rc.avg_frame_bits();
let kf = rc.frame_target_bits(true) as f64;
assert!(
kf > avg * 5.0,
"keyframe target {kf} not boosted over avg {avg}"
);
}
#[test]
fn balance_accounting_credits_underspend() {
let mut rc = RateControl::new(&cfg(500));
let target = rc.frame_target_bits(false);
rc.update_after_encode(false, 200, target, target / 4);
let next = rc.frame_target_bits(false);
assert!(next > target, "underspend should raise next target");
}
#[test]
fn balance_accounting_debits_overspend() {
let mut rc = RateControl::new(&cfg(500));
let target = rc.frame_target_bits(false);
rc.update_after_encode(false, 200, target, target * 4);
let next = rc.frame_target_bits(false);
assert!(next < target, "overspend should lower next target");
}
#[test]
fn keyframe_does_not_move_balance() {
let mut rc = RateControl::new(&cfg(500));
let before = rc.bits_balance;
let target = rc.frame_target_bits(true);
rc.update_after_encode(true, 200, target, target * 3);
assert_eq!(
rc.bits_balance, before,
"keyframe must not touch the balance"
);
}
#[test]
fn correction_factor_clamped_per_frame() {
let mut rc = RateControl::new(&cfg(500));
let cf0 = rc.inter_correction;
rc.update_after_encode(false, 200, 1_000, 1_000_000_000);
assert!(rc.inter_correction <= cf0 * CORRECTION_ADJ_MAX + 1e-9);
let cf1 = rc.inter_correction;
rc.update_after_encode(false, 200, 1_000_000_000, 1);
assert!(rc.inter_correction >= cf1 * CORRECTION_ADJ_MIN - 1e-9);
}
#[test]
fn correction_factor_stays_in_bounds() {
let mut rc = RateControl::new(&cfg(500));
for _ in 0..1000 {
rc.update_after_encode(false, 240, 1_000, 1_000_000_000);
}
assert!(rc.inter_correction <= MAX_BPB_FACTOR + 1e-9);
for _ in 0..1000 {
rc.update_after_encode(false, 160, 1_000_000_000, 1);
}
assert!(rc.inter_correction >= MIN_BPB_FACTOR - 1e-9);
}
#[test]
fn update_bitrate_raises_targets_and_damps_balance() {
let mut rc = RateControl::new(&cfg(300));
rc.update_after_encode(false, 200, 1_000, 100_000);
let balance_before = rc.bits_balance;
let lo_avg = rc.avg_frame_bits();
rc.update_bitrate_kbps(1200);
assert!((rc.avg_frame_bits() - 4.0 * lo_avg).abs() < 1e-6);
assert_eq!(rc.bits_balance, balance_before * 0.5);
assert_ne!(rc.bits_balance, 0.0);
}
#[test]
fn higher_bitrate_picks_lower_or_equal_qindex() {
let lo = RateControl::new(&cfg(300));
let hi = RateControl::new(&cfg(1200));
let q_lo = lo.select_qindex(false, lo.frame_target_bits(false));
let q_hi = hi.select_qindex(false, hi.frame_target_bits(false));
assert!(q_hi <= q_lo, "higher bitrate should not raise qindex");
}
}