Skip to main content

alpha2_native/
rate_control.rs

1//! How a resolved rate control reaches one concrete encoder.
2//!
3//! The sinks receive numbers ([`VideoRateControl`]) and never a quality
4//! level. What differs between encoders is only the spelling: `h264_vulkan`
5//! takes `rc_mode`, NVENC takes `rc`, `MediaFoundation` takes `rate_control`.
6//! Everything that spells it lives here, so the software sink and the
7//! GPU-resident sink hold a mode the same way.
8
9use alpha2_media::{BitrateMode, VideoRateControl};
10use rsmpeg::avutil::AVDictionary;
11use std::ffi::CStr;
12
13/// The private option that puts `encoder_name` into constant-bitrate mode,
14/// when its family has one.
15///
16/// Encoders without one are held to a constant rate by the generic
17/// `minrate`/`maxrate`/`bufsize` alone, which is how libavcodec's own
18/// encoders (`mpeg4` among them) implement it.
19pub(crate) fn constant_mode_option(encoder_name: &str) -> Option<(&'static CStr, &'static CStr)> {
20    if encoder_name.ends_with("_vulkan") {
21        Some((c"rc_mode", c"cbr"))
22    } else if encoder_name.ends_with("_nvenc") || encoder_name.ends_with("_amf") {
23        Some((c"rc", c"cbr"))
24    } else if encoder_name.ends_with("_vaapi") || encoder_name.ends_with("_d3d12va") {
25        Some((c"rc_mode", c"CBR"))
26    } else if encoder_name.ends_with("_mf") {
27        Some((c"rate_control", c"cbr"))
28    } else if encoder_name.ends_with("_videotoolbox") {
29        Some((c"constant_bit_rate", c"1"))
30    } else {
31        None
32    }
33}
34
35/// What to add to an open failure of `encoder_name` under `rate`, when the
36/// constant mode is the likely reason: `VideoToolbox` has a constant bit rate
37/// only from macOS 13, D3D12 Video Encode only on some devices (an Intel Iris
38/// Xe refuses it with `HRESULT` 0x887A0020 and takes a constant QP), and
39/// `FFmpeg` reports either absence as a generic error.
40pub(crate) fn constant_rate_hint(encoder_name: &str, rate: VideoRateControl) -> &'static str {
41    if rate.mode != BitrateMode::Constant {
42        ""
43    } else if encoder_name.ends_with("_videotoolbox") {
44        " (a constant bit rate was asked for, which VideoToolbox has only on macOS 13 and later)"
45    } else if encoder_name.ends_with("_d3d12va") {
46        " (a constant bit rate was asked for, which D3D12 Video Encode has only on some devices; \
47         ask for a quantizer instead)"
48    } else {
49        ""
50    }
51}
52
53/// Adds what `rate` needs beyond `AVCodecContext::bit_rate` to `options`.
54///
55/// A variable rate is the encoder's own bitrate-driven default and needs
56/// nothing more. A constant rate pins the rate from both sides against a
57/// one-second buffer and, where the encoder family has a mode switch, sets it:
58/// `reject_unused_encode_options` then names any encoder that ignored it,
59/// rather than letting a constant request quietly run variable.
60pub(crate) fn rate_control_options(
61    encoder_name: &str,
62    rate: VideoRateControl,
63    options: AVDictionary,
64) -> AVDictionary {
65    match rate.mode {
66        BitrateMode::Variable => options,
67        BitrateMode::Constant => {
68            let bitrate = i64::try_from(rate.bitrate).unwrap_or(i64::MAX);
69            let options = options
70                .set_int(c"maxrate", bitrate, 0)
71                .set_int(c"minrate", bitrate, 0)
72                .set_int(c"bufsize", bitrate, 0);
73            match constant_mode_option(encoder_name) {
74                Some((key, value)) => options.set(key, value, 0),
75                None => options,
76            }
77        }
78    }
79}
80
81/// A way of controlling an encoder's rate, independent of how a family
82/// spells it.
83#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
84#[non_exhaustive]
85pub enum RateControlMechanism {
86    /// Constant bitrate.
87    ConstantBitrate,
88    /// Variable (average) bitrate.
89    VariableBitrate,
90    /// Constant quantizer: `FFmpeg`'s `cqp`/`constqp`, the family of `crf`.
91    ConstantQuantizer,
92}
93
94impl RateControlMechanism {
95    /// Every mechanism, in the order the probe reports them.
96    pub const ALL: [Self; 3] = [
97        Self::ConstantBitrate,
98        Self::VariableBitrate,
99        Self::ConstantQuantizer,
100    ];
101
102    /// Stable identifier for diagnostics.
103    #[must_use]
104    pub const fn as_str(self) -> &'static str {
105        match self {
106            Self::ConstantBitrate => "cbr",
107            Self::VariableBitrate => "vbr",
108            Self::ConstantQuantizer => "cqp",
109        }
110    }
111}
112
113/// The options that ask one encoder for one mechanism.
114#[derive(Clone, Debug, Eq, PartialEq)]
115pub(crate) struct MechanismRequest {
116    /// Private and generic `AVOption`s, in the order they are set.
117    pub options: Vec<(&'static CStr, String)>,
118    /// `AVCodecContext::bit_rate`; zero for a quantizer mode, which must not
119    /// carry a target it would then be free to prefer.
120    pub bit_rate: i64,
121}
122
123impl MechanismRequest {
124    /// The options as `key=value` pairs, for a report.
125    pub fn describe(&self) -> String {
126        let mut parts = self
127            .options
128            .iter()
129            .map(|(key, value)| format!("{}={value}", key.to_string_lossy()))
130            .collect::<Vec<_>>();
131        if self.bit_rate > 0 {
132            parts.insert(0, format!("b={}", self.bit_rate));
133        }
134        parts.join(" ")
135    }
136
137    /// The options as a dictionary to open a context with.
138    pub fn dictionary(&self, mut options: AVDictionary) -> AVDictionary {
139        for (key, value) in &self.options {
140            let value = std::ffi::CString::new(value.as_str()).unwrap_or_default();
141            options = options.set(key, &value, 0);
142        }
143        options
144    }
145}
146
147/// How `encoder_name` is asked for `mechanism`, or `None` where alpha2 knows no
148/// spelling for it on that family.
149///
150/// `quantizer` is on the codec's own scale: 0-51 for H.264/HEVC, 0-255 for
151/// NVENC and Vulkan AV1, 0-52 for every D3D12VA codec, AV1 included. Whether
152/// the device accepts the mode is not decided here: that is what the probe
153/// and the open-time fallback find out.
154pub(crate) fn mechanism_request(
155    encoder_name: &str,
156    mechanism: RateControlMechanism,
157    bitrate: u64,
158    quantizer: u32,
159) -> Option<MechanismRequest> {
160    let target = i64::try_from(bitrate).unwrap_or(i64::MAX);
161    let constant = |switch: Option<(&'static CStr, &'static str)>| {
162        let mut options = vec![
163            (c"maxrate", bitrate.to_string()),
164            (c"minrate", bitrate.to_string()),
165            (c"bufsize", bitrate.to_string()),
166        ];
167        if let Some((key, value)) = switch {
168            options.push((key, value.to_owned()));
169        }
170        MechanismRequest {
171            options,
172            bit_rate: target,
173        }
174    };
175    let switched = |key: &'static CStr, value: &str| MechanismRequest {
176        options: vec![(key, value.to_owned())],
177        bit_rate: target,
178    };
179    let fixed_quantizer = |options: Vec<(&'static CStr, String)>| MechanismRequest {
180        options,
181        bit_rate: 0,
182    };
183    let q = quantizer.to_string();
184    let family = if encoder_name.ends_with("_vulkan") {
185        "vulkan"
186    } else if encoder_name.ends_with("_nvenc") {
187        "nvenc"
188    } else if encoder_name.ends_with("_vaapi") {
189        "vaapi"
190    } else if encoder_name.ends_with("_d3d12va") {
191        "d3d12va"
192    } else if encoder_name.ends_with("_amf") {
193        "amf"
194    } else if encoder_name.ends_with("_mf") {
195        "mf"
196    } else if encoder_name.ends_with("_videotoolbox") {
197        "videotoolbox"
198    } else if encoder_name.ends_with("_qsv") {
199        "qsv"
200    } else if matches!(encoder_name, "libx264" | "libx265") {
201        "x26x"
202    } else if matches!(
203        encoder_name,
204        "libvpx" | "libvpx-vp9" | "libaom-av1" | "libsvtav1"
205    ) {
206        "crf"
207    } else {
208        "libavcodec"
209    };
210    Some(match (family, mechanism) {
211        (_, RateControlMechanism::ConstantBitrate) => constant(
212            constant_mode_option(encoder_name)
213                .map(|(key, value)| (key, value.to_str().unwrap_or_default())),
214        ),
215        ("vulkan", RateControlMechanism::VariableBitrate) => switched(c"rc_mode", "vbr"),
216        ("nvenc", RateControlMechanism::VariableBitrate) => switched(c"rc", "vbr"),
217        ("vaapi" | "d3d12va", RateControlMechanism::VariableBitrate) => switched(c"rc_mode", "VBR"),
218        ("amf", RateControlMechanism::VariableBitrate) => switched(c"rc", "vbr_peak"),
219        ("mf", RateControlMechanism::VariableBitrate) => switched(c"rate_control", "u_vbr"),
220        (_, RateControlMechanism::VariableBitrate) => MechanismRequest {
221            options: Vec::new(),
222            bit_rate: target,
223        },
224        ("vulkan", RateControlMechanism::ConstantQuantizer) => {
225            fixed_quantizer(vec![(c"rc_mode", "cqp".to_owned()), (c"qp", q)])
226        }
227        ("nvenc", RateControlMechanism::ConstantQuantizer) => {
228            fixed_quantizer(vec![(c"rc", "constqp".to_owned()), (c"qp", q)])
229        }
230        // D3D12VA derives its I and B quantizers from `qp` through `i_qfactor`/`b_qfactor` and
231        // their offsets, which stay at the encoder's defaults: 1 and 0 for HEVC and AV1, and
232        // libavcodec's 0.8 and 1.25 + 1.25 for H.264.
233        ("vaapi" | "d3d12va", RateControlMechanism::ConstantQuantizer) => {
234            fixed_quantizer(vec![(c"rc_mode", "CQP".to_owned()), (c"qp", q)])
235        }
236        ("amf", RateControlMechanism::ConstantQuantizer) => fixed_quantizer(vec![
237            (c"rc", "cqp".to_owned()),
238            (c"qp_i", q.clone()),
239            (c"qp_p", q),
240        ]),
241        // x264 and x265 take a true constant QP as `qp`; their `crf` is another scale (libx264 at
242        // `crf` 23-41 ran 0.8-2.1 dB below `qp` at the same number on the 1080p showcase).
243        ("x26x", RateControlMechanism::ConstantQuantizer) => fixed_quantizer(vec![(c"qp", q)]),
244        ("crf", RateControlMechanism::ConstantQuantizer) => fixed_quantizer(vec![(c"crf", q)]),
245        // These families open with `flags=+qscale global_quality=N` but none reads N as a QP, so a
246        // quantizer visibly falls back to bitrate. VideoToolbox reads it as 0-1 quality, higher
247        // better (an M4 wrote `very-high` smaller than `very-low`); MediaFoundation passes it
248        // unscaled as `AVEncVideoEncodeQP`; libavcodec's `mpeg4` reads each frame's `quality`,
249        // which no sink sets.
250        _ => return None,
251    })
252}
253
254/// Which form of a resolved rate control an encoder actually ran.
255#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
256#[non_exhaustive]
257pub enum AppliedRateControl {
258    /// The constant quantizer.
259    Quantizer,
260    /// The bitrate: either none was asked for, or the encoder refused it.
261    Bitrate,
262}
263
264impl AppliedRateControl {
265    /// Stable identifier for the export result.
266    #[must_use]
267    pub const fn as_str(self) -> &'static str {
268        match self {
269            Self::Quantizer => "quantizer",
270            Self::Bitrate => "bitrate",
271        }
272    }
273}
274
275/// The constant-quantizer request for `encoder_name`, when `rate` carries a
276/// quantizer; `Err` names why it cannot be asked for at all.
277pub(crate) fn quantizer_request(
278    encoder_name: &str,
279    rate: VideoRateControl,
280) -> Option<Result<MechanismRequest, String>> {
281    let quantizer = rate.quantizer?;
282    Some(
283        mechanism_request(
284            encoder_name,
285            RateControlMechanism::ConstantQuantizer,
286            rate.bitrate,
287            quantizer,
288        )
289        .ok_or_else(|| format!("{encoder_name} has no constant-quantizer option alpha2 knows")),
290    )
291}