Skip to main content

pixelflux/encoders/
session.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! What the full-frame software sessions share: the planar picture a packed host frame is
8//! converted into, at 8 or 10 bits, the constant quantizer that follows the session quality index, the
9//! rate-control settings a live change is compared against, and the frames an encoder has
10//! been handed but not yet answered.
11
12use std::collections::VecDeque;
13
14use super::frame_rate::FrameRate;
15use super::software::{convert_to_yuv_mt, convert_to_yuv10_mt};
16use super::{QP_HYSTERESIS_LIMIT, vbv_bits};
17use crate::RustCaptureSettings;
18
19/// A planar picture, 4:2:0 or 4:4:4, with tightly packed rows: 8-bit samples in `y`, `u`, and
20/// `v`, or 10-bit ones in `y16`, `u16`, and `v16`, the planes of the other depth staying empty.
21pub struct Planes {
22    pub width: usize,
23    pub height: usize,
24    pub i444: bool,
25    pub bit_depth: u32,
26    pub y: Vec<u8>,
27    pub u: Vec<u8>,
28    pub v: Vec<u8>,
29    pub y16: Vec<u16>,
30    pub u16: Vec<u16>,
31    pub v16: Vec<u16>,
32}
33
34impl Planes {
35    pub fn new(width: usize, height: usize, i444: bool, bit_depth: u32) -> Self {
36        let (cw, ch) = if i444 {
37            (width, height)
38        } else {
39            (width.div_ceil(2), height.div_ceil(2))
40        };
41        let (narrow, wide) = if bit_depth > 8 { (0, 1) } else { (1, 0) };
42        Self {
43            width,
44            height,
45            i444,
46            bit_depth,
47            y: vec![0; width * height * narrow],
48            u: vec![0; cw * ch * narrow],
49            v: vec![0; cw * ch * narrow],
50            y16: vec![0; width * height * wide],
51            u16: vec![0; cw * ch * wide],
52            v16: vec![0; cw * ch * wide],
53        }
54    }
55
56    pub fn chroma_width(&self) -> usize {
57        if self.i444 {
58            self.width
59        } else {
60            self.width.div_ceil(2)
61        }
62    }
63
64    /// The planes as a library reads them, whichever depth they hold.
65    pub fn pointers(&mut self) -> [*mut u8; 3] {
66        if self.bit_depth > 8 {
67            [
68                self.y16.as_mut_ptr().cast(),
69                self.u16.as_mut_ptr().cast(),
70                self.v16.as_mut_ptr().cast(),
71            ]
72        } else {
73            [
74                self.y.as_mut_ptr(),
75                self.u.as_mut_ptr(),
76                self.v.as_mut_ptr(),
77            ]
78        }
79    }
80
81    /// The bytes a sample takes.
82    pub fn sample_bytes(&self) -> usize {
83        if self.bit_depth > 8 { 2 } else { 1 }
84    }
85
86    /// The bytes all three planes hold.
87    pub fn byte_len(&self) -> usize {
88        self.y.len()
89            + self.u.len()
90            + self.v.len()
91            + 2 * (self.y16.len() + self.u16.len() + self.v16.len())
92    }
93
94    /// Convert a packed host frame (`stride` bytes per row, R,G,B,A when `rgba`, else B,G,R,A)
95    /// into the planes across `threads` bands, with the BT.709 matrix at `full_range`, or BT.601
96    /// where `bt601` names a codec whose bitstream can carry no other, which has no 10 bits.
97    pub fn convert(
98        &mut self,
99        pixels: &[u8],
100        stride: usize,
101        rgba: bool,
102        full_range: bool,
103        bt601: bool,
104        threads: usize,
105    ) -> Result<(), String> {
106        let cw = self.chroma_width();
107        if self.bit_depth > 8 {
108            check_host_frame(pixels, stride, self.width, self.height)?;
109            convert_to_yuv10_mt(
110                pixels,
111                stride,
112                self.width,
113                self.height,
114                rgba,
115                self.i444,
116                full_range,
117                &mut self.y16,
118                &mut self.u16,
119                &mut self.v16,
120                (self.width, cw),
121                threads,
122            );
123            return Ok(());
124        }
125        convert_into(
126            pixels,
127            stride,
128            self.width,
129            self.height,
130            rgba,
131            self.i444,
132            full_range,
133            bt601,
134            threads,
135            &mut self.y,
136            &mut self.u,
137            &mut self.v,
138            (self.width, cw),
139        )
140    }
141}
142
143/// Convert a packed host frame into planes a library owns, at `strides` (luma, chroma) bytes
144/// per row, as `Planes::convert` does into its own.
145#[allow(clippy::too_many_arguments)]
146pub fn convert_into(
147    pixels: &[u8],
148    stride: usize,
149    width: usize,
150    height: usize,
151    rgba: bool,
152    i444: bool,
153    full_range: bool,
154    bt601: bool,
155    threads: usize,
156    y: &mut [u8],
157    u: &mut [u8],
158    v: &mut [u8],
159    strides: (usize, usize),
160) -> Result<(), String> {
161    check_host_frame(pixels, stride, width, height)?;
162    convert_to_yuv_mt(
163        pixels,
164        stride as u32,
165        width,
166        height,
167        rgba,
168        i444,
169        full_range,
170        bt601,
171        y,
172        u,
173        v,
174        strides,
175        threads,
176    )
177    .map_err(|e| format!("rgb-to-yuv conversion failed: {e:?}"))
178}
179
180/// Whether `pixels` holds a `width` x `height` packed picture at `stride` bytes per row.
181pub fn check_host_frame(
182    pixels: &[u8],
183    stride: usize,
184    width: usize,
185    height: usize,
186) -> Result<(), String> {
187    let row_bytes = width * 4;
188    let needed = if height == 0 {
189        0
190    } else {
191        stride.checked_mul(height - 1).ok_or("stride overflow")? + row_bytes
192    };
193    if stride < row_bytes || pixels.len() < needed {
194        return Err("Input buffer too small".into());
195    }
196    Ok(())
197}
198
199/// The constant quantizer of a session, moved toward the one the quality index selects: a
200/// decrease sharpens the picture and applies at once, an increase waits out
201/// `QP_HYSTERESIS_LIMIT` consecutive requests so transient motion does not make quality blink.
202/// The domain is the codec's own (`Codec::quantizer`).
203pub struct Quality {
204    pub current: u32,
205    counter: u32,
206}
207
208impl Quality {
209    pub fn new(quantizer: u32) -> Self {
210        Self {
211            current: quantizer,
212            counter: 0,
213        }
214    }
215
216    /// The quantizer to program now for `target`, once the hysteresis admits the change.
217    pub fn update(&mut self, target: u32) -> Option<u32> {
218        if target == self.current {
219            self.counter = 0;
220            return None;
221        }
222        if target > self.current {
223            self.counter += 1;
224            if self.counter <= QP_HYSTERESIS_LIMIT {
225                return None;
226            }
227        }
228        self.counter = 0;
229        self.current = target;
230        Some(target)
231    }
232}
233
234/// The rate-control and frame-rate settings a session was configured with, so a live change
235/// re-programs the encoder only when a value it reads actually moved.
236#[derive(Clone, Copy, Debug, PartialEq)]
237pub struct RateSettings {
238    pub cbr: bool,
239    pub bitrate_kbps: i32,
240    pub vbv_multiplier: f64,
241    pub keyframe_interval_s: f64,
242    pub fps: FrameRate,
243    pub min_qp: i32,
244    pub max_qp: i32,
245}
246
247impl RateSettings {
248    pub fn new(settings: &RustCaptureSettings) -> Self {
249        Self {
250            cbr: settings.video_cbr_mode,
251            bitrate_kbps: settings.video_bitrate_kbps,
252            vbv_multiplier: settings.video_vbv_multiplier,
253            keyframe_interval_s: settings.keyframe_interval_s,
254            fps: FrameRate::of(settings.target_fps),
255            min_qp: settings.video_min_qp,
256            max_qp: settings.video_max_qp,
257        }
258    }
259
260    /// The settings as they stand now, if a rate or frame-rate value the encoder was programmed
261    /// with changed: in CBR a bitrate or VBV multiplier, in any mode the frame rate.
262    pub fn changed(&self, settings: &RustCaptureSettings) -> Option<Self> {
263        let next = Self {
264            cbr: self.cbr,
265            min_qp: self.min_qp,
266            max_qp: self.max_qp,
267            ..Self::new(settings)
268        };
269        let rate_moved = self.cbr
270            && (next.bitrate_kbps != self.bitrate_kbps
271                || next.vbv_multiplier != self.vbv_multiplier);
272        (rate_moved || next.fps != self.fps).then_some(next)
273    }
274
275    /// The constant-rate target in bits per second.
276    pub fn bps(&self) -> u64 {
277        (self.bitrate_kbps.max(0) as u64) * 1000
278    }
279
280    /// The VBV buffer, in bits, the constant-rate target is held to.
281    pub fn vbv(&self) -> u32 {
282        vbv_bits(
283            self.bps().min(u32::MAX as u64) as u32,
284            self.fps.fps(),
285            self.keyframe_interval_s,
286            self.vbv_multiplier,
287        )
288    }
289}
290
291/// The frames handed to an encoder and not yet answered, by the timestamp each was submitted
292/// with, so a packet names the frame it encodes rather than the one submitted alongside it.
293/// An encoder that pipelines answers a frame several later, and one that drops a frame never
294/// answers it at all, so taking a timestamp discards every submission it passed over.
295#[derive(Default)]
296pub struct Pending(VecDeque<(u64, u16)>);
297
298impl Pending {
299    /// Record that the frame with wire id `frame_id` went in at `pts`.
300    pub fn push(&mut self, pts: u64, frame_id: u16) {
301        self.0.push_back((pts, frame_id));
302    }
303
304    /// Drop the last submission, for a frame the encoder refused outright.
305    pub fn undo(&mut self) {
306        self.0.pop_back();
307    }
308
309    pub fn is_empty(&self) -> bool {
310        self.0.is_empty()
311    }
312
313    /// The wire id submitted at `pts`, dropping it and everything older; `None` where no
314    /// submission still held carries that timestamp.
315    pub fn take(&mut self, pts: u64) -> Option<u16> {
316        let at = self.0.iter().position(|&(p, _)| p == pts)?;
317        let id = self.0[at].1;
318        self.0.drain(..=at);
319        Some(id)
320    }
321}
322
323/// The encode threads a software session spreads across: one less than the host's cores,
324/// between one and eight.
325pub fn encode_threads() -> i32 {
326    std::thread::available_parallelism()
327        .map(|n| n.get())
328        .unwrap_or(1)
329        .saturating_sub(1)
330        .clamp(1, 8) as i32
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn a_quality_increase_applies_at_once_and_a_drop_waits() {
339        let mut q = Quality::new(30);
340        assert_eq!(q.update(30), None);
341        assert_eq!(q.update(20), Some(20), "a lower quantizer applies at once");
342        for _ in 0..QP_HYSTERESIS_LIMIT {
343            assert_eq!(q.update(40), None);
344        }
345        assert_eq!(
346            q.update(40),
347            Some(40),
348            "a higher one waits out the hysteresis"
349        );
350        assert_eq!(q.update(45), None);
351        assert_eq!(
352            q.update(40),
353            None,
354            "a request back at the current value resets the count"
355        );
356        assert_eq!(q.current, 40);
357    }
358
359    #[test]
360    fn a_rate_change_is_noticed_only_where_the_encoder_reads_it() {
361        let mut settings = RustCaptureSettings {
362            target_fps: 30.0,
363            video_bitrate_kbps: 4000,
364            ..Default::default()
365        };
366        let rate = RateSettings::new(&settings);
367        assert_eq!(rate.changed(&settings), None);
368        settings.video_bitrate_kbps = 8000;
369        assert_eq!(
370            rate.changed(&settings),
371            None,
372            "a bitrate moves nothing in constant-quality mode"
373        );
374        settings.target_fps = 60.0;
375        assert_eq!(
376            rate.changed(&settings).map(|r| r.fps),
377            Some(FrameRate { num: 60, den: 1 })
378        );
379        let ntsc = RateSettings::new(&RustCaptureSettings {
380            target_fps: 60000.0 / 1001.0,
381            ..settings.clone()
382        });
383        assert_eq!(
384            ntsc.fps,
385            FrameRate {
386                num: 60000,
387                den: 1001
388            }
389        );
390        assert!(
391            ntsc.changed(&settings).is_some(),
392            "60 fps moves a session at 59.94"
393        );
394        settings.video_cbr_mode = true;
395        let cbr = RateSettings::new(&settings);
396        settings.video_bitrate_kbps = 9000;
397        assert_eq!(cbr.changed(&settings).map(|r| r.bitrate_kbps), Some(9000));
398        assert_eq!(cbr.bps(), 8_000_000);
399    }
400
401    /// A packet names the frame it encodes however deep the encoder pipelines, the frames it
402    /// passed over go with it, and a timestamp never submitted names nothing.
403    #[test]
404    fn a_packet_names_the_frame_it_encodes() {
405        let mut pending = Pending::default();
406        assert!(pending.is_empty());
407        for (pts, id) in [(0u64, 7u16), (1, 8), (2, 9)] {
408            pending.push(pts, id);
409        }
410        assert_eq!(pending.take(99), None, "a timestamp never submitted");
411        assert_eq!(
412            pending.take(1),
413            Some(8),
414            "the encoder answered two frames in"
415        );
416        assert_eq!(
417            pending.take(0),
418            None,
419            "the frame it passed over went with it"
420        );
421        assert_eq!(pending.take(2), Some(9));
422        assert!(pending.is_empty());
423        pending.push(3, 10);
424        pending.undo();
425        assert_eq!(pending.take(3), None, "a refused frame was never submitted");
426    }
427
428    #[test]
429    fn a_frame_shorter_than_its_stride_says_so() {
430        assert!(check_host_frame(&[0; 16 * 4 * 4], 64, 16, 4).is_ok());
431        assert!(check_host_frame(&[0; 16 * 4 * 4 - 1], 64, 16, 4).is_err());
432        assert!(
433            check_host_frame(&[0; 16 * 4 * 4], 60, 16, 4).is_err(),
434            "a stride shorter than a row"
435        );
436        let mut planes = Planes::new(4, 2, false, 8);
437        assert_eq!((planes.u.len(), planes.chroma_width()), (2, 2));
438        assert!(
439            planes
440                .convert(&[0; 4 * 4 * 2], 16, false, false, false, 1)
441                .is_ok()
442        );
443        assert!(
444            Planes::new(4, 2, true, 8)
445                .convert(&[0; 4 * 4], 16, false, true, false, 1)
446                .is_err()
447        );
448    }
449}